Merge "Make dependencies in SceneContainerStarteable lazy" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 705a4df..efd8578 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -430,10 +430,7 @@
 aconfig_declarations {
     name: "com.android.media.flags.bettertogether-aconfig",
     package: "com.android.media.flags",
-    srcs: [
-        "media/java/android/media/flags/media_better_together.aconfig",
-        "media/java/android/media/flags/fade_manager_configuration.aconfig",
-    ],
+    srcs: ["media/java/android/media/flags/media_better_together.aconfig"],
 }
 
 java_aconfig_library {
diff --git a/Android.bp b/Android.bp
index 91f03e003..bb93048 100644
--- a/Android.bp
+++ b/Android.bp
@@ -97,6 +97,7 @@
         // AIDL sources from external directories
         ":android.hardware.biometrics.common-V4-java-source",
         ":android.hardware.biometrics.fingerprint-V3-java-source",
+        ":android.hardware.biometrics.face-V4-java-source",
         ":android.hardware.gnss-V2-java-source",
         ":android.hardware.graphics.common-V3-java-source",
         ":android.hardware.keymaster-V4-java-source",
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 03f3f0f..f330ad1 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -65,7 +65,7 @@
 // depend on it.
 java_genrule {
     name: "framework-minus-apex.ravenwood",
-    defaults: ["hoststubgen-for-prototype-only-genrule"],
+    defaults: ["ravenwood-internal-only-visibility-genrule"],
     cmd: "cp $(in) $(out)",
     srcs: [
         ":framework-minus-apex.ravenwood-base{ravenwood.jar}",
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 83db4cb..900c902 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -1828,7 +1828,9 @@
                     /* system_measured_source_download_bytes */0,
                     /* system_measured_source_upload_bytes */ 0,
                     /* system_measured_calling_download_bytes */0,
-                    /* system_measured_calling_upload_bytes */ 0);
+                    /* system_measured_calling_upload_bytes */ 0,
+                    jobStatus.getJob().getIntervalMillis(),
+                    jobStatus.getJob().getFlexMillis());
 
             // If the job is immediately ready to run, then we can just immediately
             // put it in the pending list and try to schedule it.  This is especially
@@ -2269,7 +2271,9 @@
                     /* system_measured_source_download_bytes */ 0,
                     /* system_measured_source_upload_bytes */ 0,
                     /* system_measured_calling_download_bytes */0,
-                    /* system_measured_calling_upload_bytes */ 0);
+                    /* system_measured_calling_upload_bytes */ 0,
+                    cancelled.getJob().getIntervalMillis(),
+                    cancelled.getJob().getFlexMillis());
         }
         // If this is a replacement, bring in the new version of the job
         if (incomingJob != null) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index 6449edc..3addf9f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -534,7 +534,9 @@
                     /* system_measured_source_download_bytes */ 0,
                     /* system_measured_source_upload_bytes */ 0,
                     /* system_measured_calling_download_bytes */ 0,
-                    /* system_measured_calling_upload_bytes */ 0);
+                    /* system_measured_calling_upload_bytes */ 0,
+                    job.getJob().getIntervalMillis(),
+                    job.getJob().getFlexMillis());
             sEnqueuedJwiAtJobStart.logSampleWithUid(job.getUid(), job.getWorkCount());
             final String sourcePackage = job.getSourcePackageName();
             if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
@@ -1616,7 +1618,9 @@
                 TrafficStats.getUidRxBytes(completedJob.getUid())
                         - mInitialDownloadedBytesFromCalling,
                 TrafficStats.getUidTxBytes(completedJob.getUid())
-                        - mInitialUploadedBytesFromCalling);
+                        - mInitialUploadedBytesFromCalling,
+                completedJob.getJob().getIntervalMillis(),
+                completedJob.getJob().getFlexMillis());
         if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
             Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler",
                     getId());
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
index 913a76a..4d4e340 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
@@ -591,6 +591,16 @@
         if (idle) {
             newBucket = IDLE_BUCKET_CUTOFF;
             reason = REASON_MAIN_FORCED_BY_USER;
+            final AppUsageHistory appHistory = getAppUsageHistory(packageName, userId,
+                    elapsedRealtime);
+            // Wipe all expiry times that could raise the bucket on reevaluation.
+            if (appHistory.bucketExpiryTimesMs != null) {
+                for (int i = appHistory.bucketExpiryTimesMs.size() - 1; i >= 0; --i) {
+                    if (appHistory.bucketExpiryTimesMs.keyAt(i) < newBucket) {
+                        appHistory.bucketExpiryTimesMs.removeAt(i);
+                    }
+                }
+            }
         } else {
             newBucket = STANDBY_BUCKET_ACTIVE;
             // This is to pretend that the app was just used, don't freeze the state anymore.
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index bd5c006..e589c21 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -59,6 +59,7 @@
 import static com.android.server.usage.AppIdleHistory.STANDBY_BUCKET_UNKNOWN;
 
 import android.annotation.CurrentTimeMillisLong;
+import android.annotation.DurationMillisLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -2146,6 +2147,15 @@
         }
     }
 
+    /**
+     * Flush the handler.
+     * Returns true if successfully flushed within the timeout, otherwise return false.
+     */
+    @VisibleForTesting
+    boolean flushHandler(@DurationMillisLong long timeoutMillis) {
+        return mHandler.runWithScissors(() -> {}, timeoutMillis);
+    }
+
     @Override
     public void flushToDisk() {
         synchronized (mAppIdleLock) {
diff --git a/core/api/current.txt b/core/api/current.txt
index 12a6f74..008521a 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -35874,19 +35874,19 @@
     field public static final String NAMESPACE = "data2";
   }
 
-  public static final class ContactsContract.CommonDataKinds.Im implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
-    method public static CharSequence getProtocolLabel(android.content.res.Resources, int, CharSequence);
-    method public static int getProtocolLabelResource(int);
-    method public static CharSequence getTypeLabel(android.content.res.Resources, int, @Nullable CharSequence);
-    method public static int getTypeLabelResource(int);
-    field public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im";
-    field public static final String CUSTOM_PROTOCOL = "data6";
+  @Deprecated public static final class ContactsContract.CommonDataKinds.Im implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
+    method @Deprecated public static CharSequence getProtocolLabel(android.content.res.Resources, int, CharSequence);
+    method @Deprecated public static int getProtocolLabelResource(int);
+    method @Deprecated public static CharSequence getTypeLabel(android.content.res.Resources, int, @Nullable CharSequence);
+    method @Deprecated public static int getTypeLabelResource(int);
+    field @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im";
+    field @Deprecated public static final String CUSTOM_PROTOCOL = "data6";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX = "android.provider.extra.ADDRESS_BOOK_INDEX";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX_COUNTS = "android.provider.extra.ADDRESS_BOOK_INDEX_COUNTS";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX_TITLES = "android.provider.extra.ADDRESS_BOOK_INDEX_TITLES";
-    field public static final String PROTOCOL = "data5";
+    field @Deprecated public static final String PROTOCOL = "data5";
     field @Deprecated public static final int PROTOCOL_AIM = 0; // 0x0
-    field public static final int PROTOCOL_CUSTOM = -1; // 0xffffffff
+    field @Deprecated public static final int PROTOCOL_CUSTOM = -1; // 0xffffffff
     field @Deprecated public static final int PROTOCOL_GOOGLE_TALK = 5; // 0x5
     field @Deprecated public static final int PROTOCOL_ICQ = 6; // 0x6
     field @Deprecated public static final int PROTOCOL_JABBER = 7; // 0x7
@@ -35895,9 +35895,9 @@
     field @Deprecated public static final int PROTOCOL_QQ = 4; // 0x4
     field @Deprecated public static final int PROTOCOL_SKYPE = 3; // 0x3
     field @Deprecated public static final int PROTOCOL_YAHOO = 2; // 0x2
-    field public static final int TYPE_HOME = 1; // 0x1
-    field public static final int TYPE_OTHER = 3; // 0x3
-    field public static final int TYPE_WORK = 2; // 0x2
+    field @Deprecated public static final int TYPE_HOME = 1; // 0x1
+    field @Deprecated public static final int TYPE_OTHER = 3; // 0x3
+    field @Deprecated public static final int TYPE_WORK = 2; // 0x2
   }
 
   public static final class ContactsContract.CommonDataKinds.Nickname implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
@@ -36012,17 +36012,17 @@
     field public static final int TYPE_SPOUSE = 14; // 0xe
   }
 
-  public static final class ContactsContract.CommonDataKinds.SipAddress implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
-    method public static CharSequence getTypeLabel(android.content.res.Resources, int, @Nullable CharSequence);
-    method public static int getTypeLabelResource(int);
-    field public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/sip_address";
+  @Deprecated public static final class ContactsContract.CommonDataKinds.SipAddress implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
+    method @Deprecated public static CharSequence getTypeLabel(android.content.res.Resources, int, @Nullable CharSequence);
+    method @Deprecated public static int getTypeLabelResource(int);
+    field @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/sip_address";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX = "android.provider.extra.ADDRESS_BOOK_INDEX";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX_COUNTS = "android.provider.extra.ADDRESS_BOOK_INDEX_COUNTS";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX_TITLES = "android.provider.extra.ADDRESS_BOOK_INDEX_TITLES";
-    field public static final String SIP_ADDRESS = "data1";
-    field public static final int TYPE_HOME = 1; // 0x1
-    field public static final int TYPE_OTHER = 3; // 0x3
-    field public static final int TYPE_WORK = 2; // 0x2
+    field @Deprecated public static final String SIP_ADDRESS = "data1";
+    field @Deprecated public static final int TYPE_HOME = 1; // 0x1
+    field @Deprecated public static final int TYPE_OTHER = 3; // 0x3
+    field @Deprecated public static final int TYPE_WORK = 2; // 0x2
   }
 
   public static final class ContactsContract.CommonDataKinds.StructuredName implements android.provider.ContactsContract.DataColumnsWithJoins {
@@ -43403,6 +43403,7 @@
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_PARAMETERS_USED_FOR_NTN_LTE_SIGNAL_BAR_INT = "parameters_used_for_ntn_lte_signal_bar_int";
     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 @FlaggedApi("com.android.internal.telephony.flags.hide_prefer_3g_item") public static final String KEY_PREFER_3G_VISIBILITY_BOOL = "prefer_3g_visibility_bool";
     field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT = "premium_capability_maximum_daily_notification_count_int";
     field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT = "premium_capability_maximum_monthly_notification_count_int";
     field public static final String KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG = "premium_capability_network_setup_time_millis_long";
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 8ce3a8d..51e61e6 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3350,7 +3350,7 @@
   }
 
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public interface VirtualCameraCallback {
-    method public void onProcessCaptureRequest(int, long, @Nullable android.companion.virtual.camera.VirtualCameraMetadata);
+    method public default void onProcessCaptureRequest(int, long);
     method public void onStreamClosed(int);
     method public void onStreamConfigured(int, @NonNull android.view.Surface, @NonNull android.companion.virtual.camera.VirtualCameraStreamConfig);
   }
@@ -3371,12 +3371,6 @@
     method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback);
   }
 
-  @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraMetadata implements android.os.Parcelable {
-    method public int describeContents();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraMetadata> CREATOR;
-  }
-
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraStreamConfig implements android.os.Parcelable {
     ctor public VirtualCameraStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int);
     method public int describeContents();
@@ -6478,6 +6472,7 @@
     method public void clearAudioServerStateCallback();
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean clearPreferredDevicesForCapturePreset(int);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int dispatchAudioFocusChange(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy);
+    method @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public int dispatchAudioFocusChangeWithFade(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy, @NonNull java.util.List<android.media.AudioFocusInfo>, @Nullable android.media.FadeManagerConfiguration);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int[] getActiveAssistantServicesUids();
     method @IntRange(from=0) public long getAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int[] getAssistantServicesUids();
@@ -6677,6 +6672,69 @@
     field public static final int CONTENT_ID_NONE = 0; // 0x0
   }
 
+  @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") public final class FadeManagerConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public java.util.List<android.media.AudioAttributes> getAudioAttributesWithVolumeShaperConfigs();
+    method public long getFadeInDelayForOffenders();
+    method public long getFadeInDurationForAudioAttributes(@NonNull android.media.AudioAttributes);
+    method public long getFadeInDurationForUsage(int);
+    method @Nullable public android.media.VolumeShaper.Configuration getFadeInVolumeShaperConfigForAudioAttributes(@NonNull android.media.AudioAttributes);
+    method @Nullable public android.media.VolumeShaper.Configuration getFadeInVolumeShaperConfigForUsage(int);
+    method public long getFadeOutDurationForAudioAttributes(@NonNull android.media.AudioAttributes);
+    method public long getFadeOutDurationForUsage(int);
+    method @Nullable public android.media.VolumeShaper.Configuration getFadeOutVolumeShaperConfigForAudioAttributes(@NonNull android.media.AudioAttributes);
+    method @Nullable public android.media.VolumeShaper.Configuration getFadeOutVolumeShaperConfigForUsage(int);
+    method public int getFadeState();
+    method @NonNull public java.util.List<java.lang.Integer> getFadeableUsages();
+    method @NonNull public java.util.List<android.media.AudioAttributes> getUnfadeableAudioAttributes();
+    method @NonNull public java.util.List<java.lang.Integer> getUnfadeableContentTypes();
+    method @NonNull public java.util.List<java.lang.Integer> getUnfadeablePlayerTypes();
+    method @NonNull public java.util.List<java.lang.Integer> getUnfadeableUids();
+    method public boolean isAudioAttributesUnfadeable(@NonNull android.media.AudioAttributes);
+    method public boolean isContentTypeUnfadeable(int);
+    method public boolean isFadeEnabled();
+    method public boolean isPlayerTypeUnfadeable(int);
+    method public boolean isUidUnfadeable(int);
+    method public boolean isUsageFadeable(int);
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.media.FadeManagerConfiguration> CREATOR;
+    field public static final long DURATION_NOT_SET = 0L; // 0x0L
+    field public static final int FADE_STATE_DISABLED = 0; // 0x0
+    field public static final int FADE_STATE_ENABLED_AUTO = 2; // 0x2
+    field public static final int FADE_STATE_ENABLED_DEFAULT = 1; // 0x1
+    field public static final String TAG = "FadeManagerConfiguration";
+    field public static final int VOLUME_SHAPER_SYSTEM_FADE_ID = 2; // 0x2
+  }
+
+  public static final class FadeManagerConfiguration.Builder {
+    ctor public FadeManagerConfiguration.Builder();
+    ctor public FadeManagerConfiguration.Builder(long, long);
+    ctor public FadeManagerConfiguration.Builder(@NonNull android.media.FadeManagerConfiguration);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder addFadeableUsage(int);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder addUnfadeableAudioAttributes(@NonNull android.media.AudioAttributes);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder addUnfadeableContentType(int);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder addUnfadeableUid(int);
+    method @NonNull public android.media.FadeManagerConfiguration build();
+    method @NonNull public android.media.FadeManagerConfiguration.Builder clearFadeableUsage(int);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableAudioAttributes(@NonNull android.media.AudioAttributes);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableContentType(int);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableUid(int);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInDelayForOffenders(long);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInDurationForAudioAttributes(@NonNull android.media.AudioAttributes, long);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInDurationForUsage(int, long);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInVolumeShaperConfigForAudioAttributes(@NonNull android.media.AudioAttributes, @Nullable android.media.VolumeShaper.Configuration);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInVolumeShaperConfigForUsage(int, @Nullable android.media.VolumeShaper.Configuration);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeOutDurationForAudioAttributes(@NonNull android.media.AudioAttributes, long);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeOutDurationForUsage(int, long);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeOutVolumeShaperConfigForAudioAttributes(@NonNull android.media.AudioAttributes, @Nullable android.media.VolumeShaper.Configuration);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeOutVolumeShaperConfigForUsage(int, @Nullable android.media.VolumeShaper.Configuration);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeState(int);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeableUsages(@NonNull java.util.List<java.lang.Integer>);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setUnfadeableAudioAttributes(@NonNull java.util.List<android.media.AudioAttributes>);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setUnfadeableContentTypes(@NonNull java.util.List<java.lang.Integer>);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setUnfadeableUids(@NonNull java.util.List<java.lang.Integer>);
+  }
+
   public class HwAudioSource {
     method public boolean isPlaying();
     method public void start();
@@ -6895,15 +6953,18 @@
 
   public class AudioPolicy {
     method public int attachMixes(@NonNull java.util.List<android.media.audiopolicy.AudioMix>);
+    method @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public int clearFadeManagerConfigurationForFocusLoss();
     method public android.media.AudioRecord createAudioRecordSink(android.media.audiopolicy.AudioMix) throws java.lang.IllegalArgumentException;
     method public android.media.AudioTrack createAudioTrackSource(android.media.audiopolicy.AudioMix) throws java.lang.IllegalArgumentException;
     method public int detachMixes(@NonNull java.util.List<android.media.audiopolicy.AudioMix>);
+    method @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public android.media.FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss();
     method public int getFocusDuckingBehavior();
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioFocusInfo> getFocusStack();
     method public int getStatus();
     method public boolean removeUidDeviceAffinity(int);
     method public boolean removeUserIdDeviceAffinity(int);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean sendFocusLoss(@NonNull android.media.AudioFocusInfo) throws java.lang.IllegalStateException;
+    method @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public int setFadeManagerConfigurationForFocusLoss(@NonNull android.media.FadeManagerConfiguration);
     method public int setFocusDuckingBehavior(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void setRegistration(String);
     method public boolean setUidDeviceAffinity(int, @NonNull java.util.List<android.media.AudioDeviceInfo>);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 42daea2..aaeba66 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -369,11 +369,14 @@
   }
 
   public class NotificationManager {
+    method @FlaggedApi("android.app.modes_api") @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean);
     method public void cleanUpCallersAfter(long);
     method public android.content.ComponentName getEffectsSuppressor();
     method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String);
+    method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean);
     method @RequiresPermission(android.Manifest.permission.MANAGE_NOTIFICATION_LISTENERS) public void setNotificationListenerAccessGranted(@NonNull android.content.ComponentName, boolean, boolean);
     method @RequiresPermission(android.Manifest.permission.MANAGE_TOAST_RATE_LIMITING) public void setToastRateLimitingEnabled(boolean);
+    method @FlaggedApi("android.app.modes_api") public boolean updateAutomaticZenRule(@NonNull String, @NonNull android.app.AutomaticZenRule, boolean);
     method public void updateNotificationChannel(@NonNull String, int, @NonNull android.app.NotificationChannel);
   }
 
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index ea9bb39..37692d3 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -537,8 +537,8 @@
 
     /**
      * Returns whether the given user requires credential entry at this time. This is used to
-     * intercept activity launches for locked work apps due to work challenge being triggered or
-     * when the profile user is yet to be unlocked.
+     * intercept activity launches for apps corresponding to locked profiles due to separate
+     * challenge being triggered or when the profile user is yet to be unlocked.
      */
     public abstract boolean shouldConfirmCredentials(@UserIdInt int userId);
 
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 9438571..b7db5f5 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -167,7 +167,7 @@
     void requestInterruptionFilterFromListener(in INotificationListener token, int interruptionFilter);
     int getInterruptionFilterFromListener(in INotificationListener token);
     void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim);
-    void setInterruptionFilter(String pkg, int interruptionFilter);
+    void setInterruptionFilter(String pkg, int interruptionFilter, boolean fromUser);
 
     void updateNotificationChannelGroupFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannelGroup group);
     void updateNotificationChannelFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannel channel);
@@ -205,11 +205,11 @@
     @UnsupportedAppUsage
     ZenModeConfig getZenModeConfig();
     NotificationManager.Policy getConsolidatedNotificationPolicy();
-    oneway void setZenMode(int mode, in Uri conditionId, String reason);
+    oneway void setZenMode(int mode, in Uri conditionId, String reason, boolean fromUser);
     oneway void notifyConditions(String pkg, in IConditionProvider provider, in Condition[] conditions);
     boolean isNotificationPolicyAccessGranted(String pkg);
     NotificationManager.Policy getNotificationPolicy(String pkg);
-    void setNotificationPolicy(String pkg, in NotificationManager.Policy policy);
+    void setNotificationPolicy(String pkg, in NotificationManager.Policy policy, boolean fromUser);
     boolean isNotificationPolicyAccessGrantedForPackage(String pkg);
     void setNotificationPolicyAccessGranted(String pkg, boolean granted);
     void setNotificationPolicyAccessGrantedForUser(String pkg, int userId, boolean granted);
@@ -217,12 +217,12 @@
     Map<String, AutomaticZenRule> getAutomaticZenRules();
     // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined.
     List<ZenModeConfig.ZenRule> getZenRules();
-    String addAutomaticZenRule(in AutomaticZenRule automaticZenRule, String pkg);
-    boolean updateAutomaticZenRule(String id, in AutomaticZenRule automaticZenRule);
-    boolean removeAutomaticZenRule(String id);
-    boolean removeAutomaticZenRules(String packageName);
+    String addAutomaticZenRule(in AutomaticZenRule automaticZenRule, String pkg, boolean fromUser);
+    boolean updateAutomaticZenRule(String id, in AutomaticZenRule automaticZenRule, boolean fromUser);
+    boolean removeAutomaticZenRule(String id, boolean fromUser);
+    boolean removeAutomaticZenRules(String packageName, boolean fromUser);
     int getRuleInstanceCount(in ComponentName owner);
-    void setAutomaticZenRuleState(String id, in Condition condition);
+    void setAutomaticZenRuleState(String id, in Condition condition, boolean fromUser);
 
     byte[] getBackupPayload(int user);
     void applyRestore(in byte[] payload, int user);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index a510c77..476232c 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -7733,11 +7733,12 @@
             } else if (mPictureIcon.getType() == Icon.TYPE_BITMAP) {
                 // If the icon contains a bitmap, use the old extra so that listeners which look
                 // for that extra can still find the picture. Don't include the new extra in
-                // that case, to avoid duplicating data.
+                // that case, to avoid duplicating data. Leave the unused extra set to null to avoid
+                // crashing apps that came to expect it to be present but null.
                 extras.putParcelable(EXTRA_PICTURE, mPictureIcon.getBitmap());
-                extras.remove(EXTRA_PICTURE_ICON);
+                extras.putParcelable(EXTRA_PICTURE_ICON, null);
             } else {
-                extras.remove(EXTRA_PICTURE);
+                extras.putParcelable(EXTRA_PICTURE, null);
                 extras.putParcelable(EXTRA_PICTURE_ICON, mPictureIcon);
             }
         }
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index d23b16d..f76a45b 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1184,14 +1184,20 @@
      */
     @UnsupportedAppUsage
     public void setZenMode(int mode, Uri conditionId, String reason) {
+        setZenMode(mode, conditionId, reason, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    public void setZenMode(int mode, Uri conditionId, String reason, boolean fromUser) {
         INotificationManager service = getService();
         try {
-            service.setZenMode(mode, conditionId, reason);
+            service.setZenMode(mode, conditionId, reason, fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
+
     /**
      * @hide
      */
@@ -1325,9 +1331,19 @@
      * @return The id of the newly created rule; null if the rule could not be created.
      */
     public String addAutomaticZenRule(AutomaticZenRule automaticZenRule) {
+        return addAutomaticZenRule(automaticZenRule, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    @NonNull
+    public String addAutomaticZenRule(@NonNull AutomaticZenRule automaticZenRule,
+            boolean fromUser) {
         INotificationManager service = getService();
         try {
-            return service.addAutomaticZenRule(automaticZenRule, mContext.getPackageName());
+            return service.addAutomaticZenRule(automaticZenRule,
+                    mContext.getPackageName(), fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1347,9 +1363,17 @@
      * @return Whether the rule was successfully updated.
      */
     public boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule) {
+        return updateAutomaticZenRule(id, automaticZenRule, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public boolean updateAutomaticZenRule(@NonNull String id,
+            @NonNull AutomaticZenRule automaticZenRule, boolean fromUser) {
         INotificationManager service = getService();
         try {
-            return service.updateAutomaticZenRule(id, automaticZenRule);
+            return service.updateAutomaticZenRule(id, automaticZenRule, fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1367,9 +1391,20 @@
      * @param condition The new state of this rule
      */
     public void setAutomaticZenRuleState(@NonNull String id, @NonNull Condition condition) {
+        if (Flags.modesApi()) {
+            setAutomaticZenRuleState(id, condition,
+                    /* fromUser= */ condition.source == Condition.SOURCE_USER_ACTION);
+        } else {
+            setAutomaticZenRuleState(id, condition, /* fromUser= */ false);
+        }
+    }
+
+    /** @hide */
+    public void setAutomaticZenRuleState(@NonNull String id, @NonNull Condition condition,
+            boolean fromUser) {
         INotificationManager service = getService();
         try {
-            service.setAutomaticZenRuleState(id, condition);
+            service.setAutomaticZenRuleState(id, condition, fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1388,9 +1423,16 @@
      * @return Whether the rule was successfully deleted.
      */
     public boolean removeAutomaticZenRule(String id) {
+        return removeAutomaticZenRule(id, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public boolean removeAutomaticZenRule(@NonNull String id, boolean fromUser) {
         INotificationManager service = getService();
         try {
-            return service.removeAutomaticZenRule(id);
+            return service.removeAutomaticZenRule(id, fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1402,9 +1444,14 @@
      * @hide
      */
     public boolean removeAutomaticZenRules(String packageName) {
+        return removeAutomaticZenRules(packageName, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    public boolean removeAutomaticZenRules(String packageName, boolean fromUser) {
         INotificationManager service = getService();
         try {
-            return service.removeAutomaticZenRules(packageName);
+            return service.removeAutomaticZenRules(packageName, fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1685,10 +1732,15 @@
      */
     // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public void setNotificationPolicy(@NonNull Policy policy) {
+        setNotificationPolicy(policy, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    public void setNotificationPolicy(@NonNull Policy policy, boolean fromUser) {
         checkRequired("policy", policy);
         INotificationManager service = getService();
         try {
-            service.setNotificationPolicy(mContext.getOpPackageName(), policy);
+            service.setNotificationPolicy(mContext.getOpPackageName(), policy, fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -2685,9 +2737,16 @@
      */
     // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter) {
+        setInterruptionFilter(interruptionFilter, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter,
+            boolean fromUser) {
         final INotificationManager service = getService();
         try {
-            service.setInterruptionFilter(mContext.getOpPackageName(), interruptionFilter);
+            service.setInterruptionFilter(mContext.getOpPackageName(), interruptionFilter,
+                    fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
index fac44b5..44942d6 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
+++ b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,11 +16,11 @@
 package android.companion.virtual.camera;
 
 import android.companion.virtual.camera.VirtualCameraStreamConfig;
-import android.companion.virtual.camera.VirtualCameraMetadata;
 import android.view.Surface;
 
 /**
- * Interface for the virtual camera service and system server to talk back to the virtual camera owner.
+ * Interface for the virtual camera service and system server to talk back to the virtual camera
+ * owner.
  *
  * @hide
  */
@@ -40,8 +40,7 @@
             in VirtualCameraStreamConfig streamConfig);
 
     /**
-     * The client application is requesting a camera frame for the given streamId with the provided
-     * metadata.
+     * The client application is requesting a camera frame for the given streamId and frameId.
      *
      * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
      * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
@@ -52,16 +51,14 @@
      *     VirtualCameraStreamConfig)}
      * @param frameId The frameId that is being requested. Each request will have a different
      *     frameId, that will be increasing for each call with a particular streamId.
-     * @param metadata The metadata requested for the frame. The virtual camera should do its best
-     *     to honor the requested metadata.
      */
-    oneway void onProcessCaptureRequest(
-            int streamId, long frameId, in VirtualCameraMetadata metadata);
+    oneway void onProcessCaptureRequest(int streamId, long frameId);
 
     /**
      * The stream previously configured when {@link #onStreamConfigured(int, Surface,
      * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be
-     * freed. The Surface was disposed on the client side and should not be used anymore by the virtual camera owner
+     * freed. The Surface was disposed on the client side and should not be used anymore by the
+     * virtual camera owner.
      *
      * @param streamId The id of the stream that was closed.
      */
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
index a18ae03..5b658b8 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@
 
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.companion.virtual.flags.Flags;
 import android.view.Surface;
@@ -50,8 +49,7 @@
             @NonNull VirtualCameraStreamConfig streamConfig);
 
     /**
-     * The client application is requesting a camera frame for the given streamId with the provided
-     * metadata.
+     * The client application is requesting a camera frame for the given streamId and frameId.
      *
      * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
      * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
@@ -62,12 +60,8 @@
      *     VirtualCameraStreamConfig)}
      * @param frameId The frameId that is being requested. Each request will have a different
      *     frameId, that will be increasing for each call with a particular streamId.
-     * @param metadata The metadata requested for the frame. The virtual camera should do its best
-     *     to honor the requested metadata but the consumer won't be informed about the metadata set
-     *     for a particular frame. If null, the requested frame can be anything the producer sends.
      */
-    void onProcessCaptureRequest(
-            int streamId, long frameId, @Nullable VirtualCameraMetadata metadata);
+    default void onProcessCaptureRequest(int streamId, long frameId) {}
 
     /**
      * The stream previously configured when {@link #onStreamConfigured(int, Surface,
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
index f1eb240..a939251 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -224,9 +224,8 @@
         }
 
         @Override
-        public void onProcessCaptureRequest(
-                int streamId, long frameId, VirtualCameraMetadata metadata) {
-            mExecutor.execute(() -> mCallback.onProcessCaptureRequest(streamId, frameId, metadata));
+        public void onProcessCaptureRequest(int streamId, long frameId) {
+            mExecutor.execute(() -> mCallback.onProcessCaptureRequest(streamId, frameId));
         }
 
         @Override
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java b/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java
deleted file mode 100644
index 1ba36d0..0000000
--- a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.companion.virtual.camera;
-
-import android.annotation.FlaggedApi;
-import android.annotation.NonNull;
-import android.annotation.SystemApi;
-import android.companion.virtual.flags.Flags;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-/**
- * Data structure used to store camera metadata compatible with VirtualCamera.
- *
- * @hide
- */
-@SystemApi
-@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
-public final class VirtualCameraMetadata implements Parcelable {
-
-    /** @hide */
-    public VirtualCameraMetadata(@NonNull Parcel in) {}
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {}
-
-    @NonNull
-    public static final Creator<VirtualCameraMetadata> CREATOR =
-            new Creator<>() {
-                @Override
-                @NonNull
-                public VirtualCameraMetadata createFromParcel(Parcel in) {
-                    return new VirtualCameraMetadata(in);
-                }
-
-                @Override
-                @NonNull
-                public VirtualCameraMetadata[] newArray(int size) {
-                    return new VirtualCameraMetadata[size];
-                }
-            };
-}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
index e198821..1272f16 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
@@ -24,6 +24,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.util.Objects;
+
 /**
  * The configuration of a single virtual camera stream.
  *
@@ -98,6 +100,19 @@
         return mHeight;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        VirtualCameraStreamConfig that = (VirtualCameraStreamConfig) o;
+        return mWidth == that.mWidth && mHeight == that.mHeight && mFormat == that.mFormat;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mWidth, mHeight, mFormat);
+    }
+
     /** Returns the {@link ImageFormat} of this stream. */
     @ImageFormat.Format
     public int getFormat() {
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index b04b7ba..60d5c14 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -115,3 +115,11 @@
     description: "Feature flag to fix duplicated PackageManager flag values"
     bug: "314815969"
 }
+
+flag {
+    name: "provide_info_of_apk_in_apex"
+    namespace: "package_manager_service"
+    description: "Feature flag to provide the information of APK-in-APEX"
+    bug: "306329516"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index f71e853..ffd7212 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -715,6 +715,14 @@
         boolean startOffload();
 
         void stopOffload();
+
+        /**
+         * Called when {@link DisplayOffloadSession} tries to block screen turning on.
+         *
+         * @param unblocker a {@link Runnable} executed upon all work required before screen turning
+         *                  on is done.
+         */
+        void onBlockingScreenOn(Runnable unblocker);
     }
 
     /** A session token that associates a internal display with a {@link DisplayOffloader}. */
@@ -734,6 +742,15 @@
          */
         void updateBrightness(float brightness);
 
+        /**
+         * Called while display is turning to state ON to leave a small period for displayoffload
+         * session to finish some work.
+         *
+         * @param unblocker a {@link Runnable} used by displayoffload session to notify
+         *                  {@link DisplayManager} that it can continue turning screen on.
+         */
+        boolean blockScreenOn(Runnable unblocker);
+
         /** Returns whether displayoffload supports the given display state. */
         static boolean isSupportedOffloadState(int displayState) {
             return Display.isSuspendedState(displayState);
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl b/core/java/android/hardware/face/FaceSensorConfigurations.aidl
similarity index 74%
rename from core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
rename to core/java/android/hardware/face/FaceSensorConfigurations.aidl
index 6c1f0fc..26367b3 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
+++ b/core/java/android/hardware/face/FaceSensorConfigurations.aidl
@@ -13,12 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.hardware.face;
 
-package android.companion.virtual.camera;
-
-/**
- * Data structure used to store {@link android.hardware.camera2.CameraMetadata} compatible with
- * VirtualCamera.
- * @hide
- */
-parcelable VirtualCameraMetadata;
+parcelable FaceSensorConfigurations;
\ No newline at end of file
diff --git a/core/java/android/hardware/face/FaceSensorConfigurations.java b/core/java/android/hardware/face/FaceSensorConfigurations.java
new file mode 100644
index 0000000..6ef692f
--- /dev/null
+++ b/core/java/android/hardware/face/FaceSensorConfigurations.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.face;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.biometrics.face.IFace;
+import android.hardware.biometrics.face.SensorProps;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Slog;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Provides the sensor props for face sensor, if available.
+ * @hide
+ */
+public class FaceSensorConfigurations implements Parcelable {
+    private static final String TAG = "FaceSensorConfigurations";
+
+    private final boolean mResetLockoutRequiresChallenge;
+    private final Map<String, SensorProps[]> mSensorPropsMap;
+
+    public static final Creator<FaceSensorConfigurations> CREATOR =
+            new Creator<FaceSensorConfigurations>() {
+                @Override
+                public FaceSensorConfigurations createFromParcel(Parcel in) {
+                    return new FaceSensorConfigurations(in);
+                }
+
+                @Override
+                public FaceSensorConfigurations[] newArray(int size) {
+                    return new FaceSensorConfigurations[size];
+                }
+            };
+
+    public FaceSensorConfigurations(boolean resetLockoutRequiresChallenge) {
+        mResetLockoutRequiresChallenge = resetLockoutRequiresChallenge;
+        mSensorPropsMap = new HashMap<>();
+    }
+
+    protected FaceSensorConfigurations(Parcel in) {
+        mResetLockoutRequiresChallenge = in.readByte() != 0;
+        mSensorPropsMap = in.readHashMap(null, String.class, SensorProps[].class);
+    }
+
+    /**
+     * Process AIDL instances to extract sensor props and add it to the sensor map.
+     * @param aidlInstances available face AIDL instances
+     * @param getIFace function that provides the daemon for the specific instance
+     */
+    public void addAidlConfigs(@NonNull String[] aidlInstances,
+            @NonNull Function<String, IFace> getIFace) {
+        for (String aidlInstance : aidlInstances) {
+            final String fqName = IFace.DESCRIPTOR + "/" + aidlInstance;
+            IFace face = getIFace.apply(fqName);
+            try {
+                if (face != null) {
+                    mSensorPropsMap.put(aidlInstance, face.getSensorProps());
+                } else {
+                    Slog.e(TAG, "Unable to get declared service: " + fqName);
+                }
+            } catch (RemoteException e) {
+                Log.d(TAG, "Unable to get sensor properties!");
+            }
+        }
+    }
+
+    /**
+     * Parse through HIDL configuration and add it to the sensor map.
+     */
+    public void addHidlConfigs(@NonNull String[] hidlConfigStrings,
+            @NonNull Context context) {
+        final List<HidlFaceSensorConfig> hidlFaceSensorConfigs = new ArrayList<>();
+        for (String hidlConfig: hidlConfigStrings) {
+            final HidlFaceSensorConfig hidlFaceSensorConfig = new HidlFaceSensorConfig();
+            try {
+                hidlFaceSensorConfig.parse(hidlConfig, context);
+            } catch (Exception e) {
+                Log.e(TAG, "HIDL sensor configuration format is incorrect.");
+                continue;
+            }
+            if (hidlFaceSensorConfig.getModality() == TYPE_FACE) {
+                hidlFaceSensorConfigs.add(hidlFaceSensorConfig);
+            }
+        }
+        final String hidlHalInstanceName = "defaultHIDL";
+        mSensorPropsMap.put(hidlHalInstanceName, hidlFaceSensorConfigs.toArray(
+                new SensorProps[hidlFaceSensorConfigs.size()]));
+    }
+
+    /**
+     * Returns true if any face sensors have been added.
+     */
+    public boolean hasSensorConfigurations() {
+        return mSensorPropsMap.size() > 0;
+    }
+
+    /**
+     * Returns true if there is only a single face sensor configuration available.
+     */
+    public boolean isSingleSensorConfigurationPresent() {
+        return mSensorPropsMap.size() == 1;
+    }
+
+    /**
+     * Return sensor props for the given instance. If instance is not available,
+     * then null is returned.
+     */
+    @Nullable
+    public Pair<String, SensorProps[]> getSensorPairForInstance(String instance) {
+        if (mSensorPropsMap.containsKey(instance)) {
+            return new Pair<>(instance, mSensorPropsMap.get(instance));
+        }
+
+        return null;
+    }
+
+    /**
+     * Return the first pair of instance and sensor props, which does not correspond to the given
+     * If instance is not available, then null is returned.
+     */
+    @Nullable
+    public Pair<String, SensorProps[]> getSensorPairNotForInstance(String instance) {
+        Optional<String> notAVirtualInstance = mSensorPropsMap.keySet().stream().filter(
+                (instanceName) -> !instanceName.equals(instance)).findFirst();
+        return notAVirtualInstance.map(this::getSensorPairForInstance).orElseGet(
+                this::getSensorPair);
+    }
+
+    /**
+     * Returns the first pair of instance and sensor props that has been added to the map.
+     */
+    @Nullable
+    public Pair<String, SensorProps[]> getSensorPair() {
+        Optional<String> optionalInstance = mSensorPropsMap.keySet().stream().findFirst();
+        return optionalInstance.map(this::getSensorPairForInstance).orElse(null);
+
+    }
+
+    public boolean getResetLockoutRequiresChallenge() {
+        return mResetLockoutRequiresChallenge;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeByte((byte) (mResetLockoutRequiresChallenge ? 1 : 0));
+        dest.writeMap(mSensorPropsMap);
+    }
+}
diff --git a/core/java/android/hardware/face/HidlFaceSensorConfig.java b/core/java/android/hardware/face/HidlFaceSensorConfig.java
new file mode 100644
index 0000000..cab146d
--- /dev/null
+++ b/core/java/android/hardware/face/HidlFaceSensorConfig.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.face;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.common.CommonProps;
+import android.hardware.biometrics.common.SensorStrength;
+import android.hardware.biometrics.face.SensorProps;
+
+import com.android.internal.R;
+
+/**
+ * Parse HIDL face sensor config and map it to SensorProps.aidl to match AIDL.
+ * See core/res/res/values/config.xml config_biometric_sensors
+ * @hide
+ */
+public final class HidlFaceSensorConfig extends SensorProps {
+    private int mSensorId;
+    private int mModality;
+    private int mStrength;
+
+    /**
+     * Parse through the config string and map it to SensorProps.aidl.
+     * @throws IllegalArgumentException when config string has unexpected format
+     */
+    public void parse(@NonNull String config, @NonNull Context context)
+            throws IllegalArgumentException {
+        final String[] elems = config.split(":");
+        if (elems.length < 3) {
+            throw new IllegalArgumentException();
+        }
+        mSensorId = Integer.parseInt(elems[0]);
+        mModality = Integer.parseInt(elems[1]);
+        mStrength = Integer.parseInt(elems[2]);
+        mapHidlToAidlFaceSensorConfigurations(context);
+    }
+
+    @BiometricAuthenticator.Modality
+    public int getModality() {
+        return mModality;
+    }
+
+    private void mapHidlToAidlFaceSensorConfigurations(@NonNull Context context) {
+        commonProps = new CommonProps();
+        commonProps.sensorId = mSensorId;
+        commonProps.sensorStrength = authenticatorStrengthToPropertyStrength(mStrength);
+        halControlsPreview = context.getResources().getBoolean(
+                R.bool.config_faceAuthSupportsSelfIllumination);
+        commonProps.maxEnrollmentsPerUser = context.getResources().getInteger(
+                R.integer.config_faceMaxTemplatesPerUser);
+        commonProps.componentInfo = null;
+        supportsDetectInteraction = false;
+    }
+
+    private byte authenticatorStrengthToPropertyStrength(
+            @BiometricManager.Authenticators.Types int strength) {
+        switch (strength) {
+            case BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE:
+                return SensorStrength.CONVENIENCE;
+            case BiometricManager.Authenticators.BIOMETRIC_WEAK:
+                return SensorStrength.WEAK;
+            case BiometricManager.Authenticators.BIOMETRIC_STRONG:
+                return SensorStrength.STRONG;
+            default:
+                throw new IllegalArgumentException("Unknown strength: " + strength);
+        }
+    }
+}
diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl
index 7080133..0096877 100644
--- a/core/java/android/hardware/face/IFaceService.aidl
+++ b/core/java/android/hardware/face/IFaceService.aidl
@@ -26,6 +26,7 @@
 import android.hardware.face.Face;
 import android.hardware.face.FaceAuthenticateOptions;
 import android.hardware.face.FaceSensorPropertiesInternal;
+import android.hardware.face.FaceSensorConfigurations;
 import android.view.Surface;
 
 /**
@@ -167,6 +168,10 @@
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     void registerAuthenticators(in List<FaceSensorPropertiesInternal> hidlSensors);
 
+    //Register all available face sensors.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void registerAuthenticatorsLegacy(in FaceSensorConfigurations faceSensorConfigurations);
+
     // Adds a callback which gets called when the service registers all of the face
     // authenticators. The callback is automatically removed after it's invoked.
     void addAuthenticatorsRegisteredCallback(IFaceAuthenticatorsRegisteredCallback callback);
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.aidl
similarity index 74%
copy from core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
copy to core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.aidl
index 6c1f0fc..ebb05dc 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
+++ b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.aidl
@@ -13,12 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.hardware.fingerprint;
 
-package android.companion.virtual.camera;
-
-/**
- * Data structure used to store {@link android.hardware.camera2.CameraMetadata} compatible with
- * VirtualCamera.
- * @hide
- */
-parcelable VirtualCameraMetadata;
+parcelable FingerprintSensorConfigurations;
\ No newline at end of file
diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
new file mode 100644
index 0000000..f214494a
--- /dev/null
+++ b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.fingerprint;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.biometrics.fingerprint.IFingerprint;
+import android.hardware.biometrics.fingerprint.SensorProps;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Provides the sensor props for fingerprint sensor, if available.
+ * @hide
+ */
+
+public class FingerprintSensorConfigurations implements Parcelable {
+    private static final String TAG = "FingerprintSensorConfigurations";
+
+    private final Map<String, SensorProps[]> mSensorPropsMap;
+    private final boolean mResetLockoutRequiresHardwareAuthToken;
+
+    public static final Creator<FingerprintSensorConfigurations> CREATOR =
+            new Creator<>() {
+                @Override
+                public FingerprintSensorConfigurations createFromParcel(Parcel in) {
+                    return new FingerprintSensorConfigurations(in);
+                }
+
+                @Override
+                public FingerprintSensorConfigurations[] newArray(int size) {
+                    return new FingerprintSensorConfigurations[size];
+                }
+            };
+
+    public FingerprintSensorConfigurations(boolean resetLockoutRequiresHardwareAuthToken) {
+        mResetLockoutRequiresHardwareAuthToken = resetLockoutRequiresHardwareAuthToken;
+        mSensorPropsMap = new HashMap<>();
+    }
+
+    /**
+     * Process AIDL instances to extract sensor props and add it to the sensor map.
+     * @param aidlInstances available face AIDL instances
+     * @param getIFingerprint function that provides the daemon for the specific instance
+     */
+    public void addAidlSensors(@NonNull String[] aidlInstances,
+            @NonNull Function<String, IFingerprint> getIFingerprint) {
+        for (String aidlInstance : aidlInstances) {
+            try {
+                final String fqName = IFingerprint.DESCRIPTOR + "/" + aidlInstance;
+                final IFingerprint fp = getIFingerprint.apply(fqName);
+                if (fp != null) {
+                    SensorProps[] props = fp.getSensorProps();
+                    mSensorPropsMap.put(aidlInstance, props);
+                } else {
+                    Log.d(TAG, "IFingerprint null for instance " + aidlInstance);
+                }
+            } catch (RemoteException e) {
+                Log.d(TAG, "Unable to get sensor properties!");
+            }
+        }
+    }
+
+    /**
+     * Parse through HIDL configuration and add it to the sensor map.
+     */
+    public void addHidlSensors(@NonNull String[] hidlConfigStrings,
+            @NonNull Context context) {
+        final List<HidlFingerprintSensorConfig> hidlFingerprintSensorConfigs = new ArrayList<>();
+        for (String hidlConfigString : hidlConfigStrings) {
+            final HidlFingerprintSensorConfig hidlFingerprintSensorConfig =
+                    new HidlFingerprintSensorConfig();
+            try {
+                hidlFingerprintSensorConfig.parse(hidlConfigString, context);
+            } catch (Exception e) {
+                Log.e(TAG, "HIDL sensor configuration format is incorrect.");
+                continue;
+            }
+            if (hidlFingerprintSensorConfig.getModality() == TYPE_FINGERPRINT) {
+                hidlFingerprintSensorConfigs.add(hidlFingerprintSensorConfig);
+            }
+        }
+        final String hidlHalInstanceName = "defaultHIDL";
+        mSensorPropsMap.put(hidlHalInstanceName,
+                hidlFingerprintSensorConfigs.toArray(
+                        new HidlFingerprintSensorConfig[hidlFingerprintSensorConfigs.size()]));
+    }
+
+    protected FingerprintSensorConfigurations(Parcel in) {
+        mResetLockoutRequiresHardwareAuthToken = in.readByte() != 0;
+        mSensorPropsMap = in.readHashMap(null /* loader */, String.class, SensorProps[].class);
+    }
+
+    /**
+     * Returns true if any fingerprint sensors have been added.
+     */
+    public boolean hasSensorConfigurations() {
+        return mSensorPropsMap.size() > 0;
+    }
+
+    /**
+     * Returns true if there is only a single fingerprint sensor configuration available.
+     */
+    public boolean isSingleSensorConfigurationPresent() {
+        return mSensorPropsMap.size() == 1;
+    }
+
+    /**
+     * Return sensor props for the given instance. If instance is not available,
+     * then null is returned.
+     */
+    @Nullable
+    public Pair<String, SensorProps[]> getSensorPairForInstance(String instance) {
+        if (mSensorPropsMap.containsKey(instance)) {
+            return new Pair<>(instance, mSensorPropsMap.get(instance));
+        }
+
+        return null;
+    }
+
+    /**
+     * Return the first pair of instance and sensor props, which does not correspond to the given
+     * If instance is not available, then null is returned.
+     */
+    @Nullable
+    public Pair<String, SensorProps[]> getSensorPairNotForInstance(String instance) {
+        Optional<String> notAVirtualInstance = mSensorPropsMap.keySet().stream().filter(
+                (instanceName) -> !instanceName.equals(instance)).findFirst();
+        return notAVirtualInstance.map(this::getSensorPairForInstance).orElseGet(
+                this::getSensorPair);
+    }
+
+    /**
+     * Returns the first pair of instance and sensor props that has been added to the map.
+     */
+    @Nullable
+    public Pair<String, SensorProps[]> getSensorPair() {
+        Optional<String> optionalInstance = mSensorPropsMap.keySet().stream().findFirst();
+        return optionalInstance.map(this::getSensorPairForInstance).orElse(null);
+
+    }
+
+    public boolean getResetLockoutRequiresHardwareAuthToken() {
+        return mResetLockoutRequiresHardwareAuthToken;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeByte((byte) (mResetLockoutRequiresHardwareAuthToken ? 1 : 0));
+        dest.writeMap(mSensorPropsMap);
+    }
+}
diff --git a/core/java/android/hardware/fingerprint/HidlFingerprintSensorConfig.java b/core/java/android/hardware/fingerprint/HidlFingerprintSensorConfig.java
new file mode 100644
index 0000000..d481153
--- /dev/null
+++ b/core/java/android/hardware/fingerprint/HidlFingerprintSensorConfig.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.fingerprint;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.common.CommonProps;
+import android.hardware.biometrics.common.SensorStrength;
+import android.hardware.biometrics.fingerprint.FingerprintSensorType;
+import android.hardware.biometrics.fingerprint.SensorLocation;
+import android.hardware.biometrics.fingerprint.SensorProps;
+
+import com.android.internal.R;
+import com.android.internal.util.ArrayUtils;
+
+/**
+ * Parse HIDL fingerprint sensor config and map it to SensorProps.aidl to match AIDL.
+ * See core/res/res/values/config.xml config_biometric_sensors
+ * @hide
+ */
+public final class HidlFingerprintSensorConfig extends SensorProps {
+    private int mSensorId;
+    private int mModality;
+    private int mStrength;
+
+    /**
+     * Parse through the config string and map it to SensorProps.aidl.
+     * @throws IllegalArgumentException when config string has unexpected format
+     */
+    public void parse(@NonNull String config, @NonNull Context context)
+            throws IllegalArgumentException {
+        final String[] elems = config.split(":");
+        if (elems.length < 3) {
+            throw new IllegalArgumentException();
+        }
+        mSensorId = Integer.parseInt(elems[0]);
+        mModality = Integer.parseInt(elems[1]);
+        mStrength = Integer.parseInt(elems[2]);
+        mapHidlToAidlSensorConfiguration(context);
+    }
+
+    @BiometricAuthenticator.Modality
+    public int getModality() {
+        return mModality;
+    }
+
+    private void mapHidlToAidlSensorConfiguration(@NonNull Context context) {
+        commonProps = new CommonProps();
+        commonProps.componentInfo = null;
+        commonProps.sensorId = mSensorId;
+        commonProps.sensorStrength = authenticatorStrengthToPropertyStrength(mStrength);
+        commonProps.maxEnrollmentsPerUser = context.getResources().getInteger(
+                R.integer.config_fingerprintMaxTemplatesPerUser);
+        halControlsIllumination = false;
+        sensorLocations = new SensorLocation[1];
+
+        final int[] udfpsProps = context.getResources().getIntArray(
+                com.android.internal.R.array.config_udfps_sensor_props);
+        final boolean isUdfps = !ArrayUtils.isEmpty(udfpsProps);
+        // config_is_powerbutton_fps indicates whether device has a power button fingerprint sensor.
+        final boolean isPowerbuttonFps = context.getResources().getBoolean(
+                R.bool.config_is_powerbutton_fps);
+
+        if (isUdfps) {
+            sensorType = FingerprintSensorType.UNKNOWN;
+        } else if (isPowerbuttonFps) {
+            sensorType = FingerprintSensorType.POWER_BUTTON;
+        } else {
+            sensorType = FingerprintSensorType.REAR;
+        }
+
+        if (isUdfps && udfpsProps.length == 3) {
+            setSensorLocation(udfpsProps[0], udfpsProps[1], udfpsProps[2]);
+        } else {
+            setSensorLocation(540 /* sensorLocationX */, 1636 /* sensorLocationY */,
+                    130 /* sensorRadius */);
+        }
+
+    }
+
+    private void setSensorLocation(int sensorLocationX,
+            int sensorLocationY, int sensorRadius) {
+        sensorLocations[0] = new SensorLocation();
+        sensorLocations[0].display = "";
+        sensorLocations[0].sensorLocationX = sensorLocationX;
+        sensorLocations[0].sensorLocationY = sensorLocationY;
+        sensorLocations[0].sensorRadius = sensorRadius;
+    }
+
+    private byte authenticatorStrengthToPropertyStrength(
+            @BiometricManager.Authenticators.Types int strength) {
+        switch (strength) {
+            case BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE:
+                return SensorStrength.CONVENIENCE;
+            case BiometricManager.Authenticators.BIOMETRIC_WEAK:
+                return SensorStrength.WEAK;
+            case BiometricManager.Authenticators.BIOMETRIC_STRONG:
+                return SensorStrength.STRONG;
+            default:
+                throw new IllegalArgumentException("Unknown strength: " + strength);
+        }
+    }
+}
diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
index f594c00..606b171 100644
--- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl
+++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
@@ -31,6 +31,7 @@
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.FingerprintSensorConfigurations;
 import java.util.List;
 
 /**
@@ -173,6 +174,10 @@
     @EnforcePermission("MANAGE_FINGERPRINT")
     void removeClientActiveCallback(IFingerprintClientActiveCallback callback);
 
+    //Register all available fingerprint sensors.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void registerAuthenticatorsLegacy(in FingerprintSensorConfigurations fingerprintSensorConfigurations);
+
     // Registers all HIDL and AIDL sensors. Only HIDL sensor properties need to be provided, because
     // AIDL sensor properties are retrieved directly from the available HALs. If no HIDL HALs exist,
     // hidlSensors must be non-null and empty. See AuthService.java
diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java
index 4bfff16..7d00b80 100644
--- a/core/java/android/provider/ContactsContract.java
+++ b/core/java/android/provider/ContactsContract.java
@@ -6911,7 +6911,11 @@
          * <td></td>
          * </tr>
          * </table>
+         *
+         * @deprecated This field may not be well supported by some contacts apps and is discouraged
+         * to use.
          */
+        @Deprecated
         public static final class Im implements DataColumnsWithJoins, CommonColumns, ContactCounts {
             /**
              * This utility class cannot be instantiated
@@ -7721,7 +7725,11 @@
          * <td></td>
          * </tr>
          * </table>
+         *
+         * @deprecated This field may not be well supported by some contacts apps and is discouraged
+         * to use.
          */
+        @Deprecated
         public static final class SipAddress implements DataColumnsWithJoins, CommonColumns,
                 ContactCounts {
             /**
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 875031f..23c8393 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -960,8 +960,8 @@
                 mKeyphraseMetadata = new KeyphraseMetadata(1, mText, fakeSupportedLocales,
                         AlwaysOnHotwordDetector.RECOGNITION_MODE_VOICE_TRIGGER);
             }
+            notifyStateChangedLocked();
         }
-        notifyStateChanged(availability);
     }
 
     /**
@@ -1371,8 +1371,8 @@
 
             mAvailability = STATE_INVALID;
             mIsAvailabilityOverriddenByTestApi = false;
+            notifyStateChangedLocked();
         }
-        notifyStateChanged(STATE_INVALID);
         super.destroy();
     }
 
@@ -1402,8 +1402,6 @@
      */
     // TODO(b/281608561): remove the enrollment flow from AlwaysOnHotwordDetector
     void onSoundModelsChanged() {
-        boolean notifyError = false;
-
         synchronized (mLock) {
             if (mAvailability == STATE_INVALID
                     || mAvailability == STATE_HARDWARE_UNAVAILABLE
@@ -1444,9 +1442,6 @@
                     // calling stopRecognition where there is no started session.
                     Log.w(TAG, "Failed to stop recognition after enrollment update: code="
                             + result);
-
-                    // Execute a refresh availability task - which should then notify of a change.
-                    new RefreshAvailabilityTask().execute();
                 } catch (Exception e) {
                     Slog.w(TAG, "Failed to stop recognition after enrollment update", e);
                     if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
@@ -1455,14 +1450,14 @@
                                         + Log.getStackTraceString(e),
                                 FailureSuggestedAction.RECREATE_DETECTOR));
                     } else {
-                        notifyError = true;
+                        updateAndNotifyStateChangedLocked(STATE_ERROR);
                     }
+                    return;
                 }
             }
-        }
 
-        if (notifyError) {
-            updateAndNotifyStateChanged(STATE_ERROR);
+            // Execute a refresh availability task - which should then notify of a change.
+            new RefreshAvailabilityTask().execute();
         }
     }
 
@@ -1578,11 +1573,10 @@
         }
     }
 
-    private void updateAndNotifyStateChanged(int availability) {
-        synchronized (mLock) {
-            updateAvailabilityLocked(availability);
-        }
-        notifyStateChanged(availability);
+    @GuardedBy("mLock")
+    private void updateAndNotifyStateChangedLocked(int availability) {
+        updateAvailabilityLocked(availability);
+        notifyStateChangedLocked();
     }
 
     @GuardedBy("mLock")
@@ -1596,17 +1590,17 @@
         }
     }
 
-    private void notifyStateChanged(int newAvailability) {
+    @GuardedBy("mLock")
+    private void notifyStateChangedLocked() {
         Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED);
-        message.arg1 = newAvailability;
+        message.arg1 = mAvailability;
         message.sendToTarget();
     }
 
+    @GuardedBy("mLock")
     private void sendUnknownFailure(String failureMessage) {
-        synchronized (mLock) {
-            // update but do not call onAvailabilityChanged callback for STATE_ERROR
-            updateAvailabilityLocked(STATE_ERROR);
-        }
+        // update but do not call onAvailabilityChanged callback for STATE_ERROR
+        updateAvailabilityLocked(STATE_ERROR);
         Message.obtain(mHandler, MSG_DETECTION_UNKNOWN_FAILURE, failureMessage).sendToTarget();
     }
 
@@ -1822,17 +1816,19 @@
                             availability = STATE_KEYPHRASE_UNENROLLED;
                         }
                     }
+                    updateAndNotifyStateChangedLocked(availability);
                 }
-                updateAndNotifyStateChanged(availability);
             } catch (Exception e) {
                 // Any exception here not caught will crash the process because AsyncTask does not
                 // bubble up the exceptions to the client app, so we must propagate it to the app.
                 Slog.w(TAG, "Failed to refresh availability", e);
-                if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
-                    sendUnknownFailure(
-                            "Failed to refresh availability: " + Log.getStackTraceString(e));
-                } else {
-                    updateAndNotifyStateChanged(STATE_ERROR);
+                synchronized (mLock) {
+                    if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
+                        sendUnknownFailure(
+                                "Failed to refresh availability: " + Log.getStackTraceString(e));
+                    } else {
+                        updateAndNotifyStateChangedLocked(STATE_ERROR);
+                    }
                 }
             }
 
diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java
index b7d9705..adc54f5 100644
--- a/core/java/android/service/voice/VisualQueryDetector.java
+++ b/core/java/android/service/voice/VisualQueryDetector.java
@@ -94,7 +94,9 @@
      */
     public void updateState(@Nullable PersistableBundle options,
             @Nullable SharedMemory sharedMemory) {
-        mInitializationDelegate.updateState(options, sharedMemory);
+        synchronized (mInitializationDelegate.getLock()) {
+            mInitializationDelegate.updateState(options, sharedMemory);
+        }
     }
 
 
@@ -116,18 +118,21 @@
         if (DEBUG) {
             Slog.i(TAG, "#startRecognition");
         }
-        // check if the detector is active with the initialization delegate
-        mInitializationDelegate.startRecognition();
+        synchronized (mInitializationDelegate.getLock()) {
+            // check if the detector is active with the initialization delegate
+            mInitializationDelegate.startRecognition();
 
-        try {
-            mManagerService.startPerceiving(new BinderCallback(mExecutor, mCallback));
-        } catch (SecurityException e) {
-            Slog.e(TAG, "startRecognition failed: " + e);
-            return false;
-        } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
+            try {
+                mManagerService.startPerceiving(new BinderCallback(
+                        mExecutor, mCallback, mInitializationDelegate.getLock()));
+            } catch (SecurityException e) {
+                Slog.e(TAG, "startRecognition failed: " + e);
+                return false;
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+            return true;
         }
-        return true;
     }
 
     /**
@@ -140,15 +145,17 @@
         if (DEBUG) {
             Slog.i(TAG, "#stopRecognition");
         }
-        // check if the detector is active with the initialization delegate
-        mInitializationDelegate.startRecognition();
+        synchronized (mInitializationDelegate.getLock()) {
+            // check if the detector is active with the initialization delegate
+            mInitializationDelegate.stopRecognition();
 
-        try {
-            mManagerService.stopPerceiving();
-        } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
+            try {
+                mManagerService.stopPerceiving();
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+            return true;
         }
-        return true;
     }
 
     /**
@@ -160,12 +167,16 @@
         if (DEBUG) {
             Slog.i(TAG, "#destroy");
         }
-        mInitializationDelegate.destroy();
+        synchronized (mInitializationDelegate.getLock()) {
+            mInitializationDelegate.destroy();
+        }
     }
 
     /** @hide */
     public void dump(String prefix, PrintWriter pw) {
-        // TODO: implement this
+        synchronized (mInitializationDelegate.getLock()) {
+            mInitializationDelegate.dump(prefix, pw);
+        }
     }
 
     /** @hide */
@@ -175,7 +186,9 @@
 
     /** @hide */
     void registerOnDestroyListener(Consumer<AbstractDetector> onDestroyListener) {
-        mInitializationDelegate.registerOnDestroyListener(onDestroyListener);
+        synchronized (mInitializationDelegate.getLock()) {
+            mInitializationDelegate.registerOnDestroyListener(onDestroyListener);
+        }
     }
 
     /**
@@ -282,6 +295,15 @@
         public boolean isUsingSandboxedDetectionService() {
             return true;
         }
+
+        @Override
+        public void dump(String prefix, PrintWriter pw) {
+            // No-op
+        }
+
+        private Object getLock() {
+            return mLock;
+        }
     }
 
     private static class BinderCallback
@@ -289,31 +311,43 @@
         private final Executor mExecutor;
         private final VisualQueryDetector.Callback mCallback;
 
-        BinderCallback(Executor executor, VisualQueryDetector.Callback callback) {
+        private final Object mLock;
+
+        BinderCallback(Executor executor, VisualQueryDetector.Callback callback, Object lock) {
             this.mExecutor = executor;
             this.mCallback = callback;
+            this.mLock = lock;
         }
 
         /** Called when the detected result is valid. */
         @Override
         public void onQueryDetected(@NonNull String partialQuery) {
             Slog.v(TAG, "BinderCallback#onQueryDetected");
-            Binder.withCleanCallingIdentity(() -> mExecutor.execute(
-                    () -> mCallback.onQueryDetected(partialQuery)));
+            Binder.withCleanCallingIdentity(() -> {
+                synchronized (mLock) {
+                    mCallback.onQueryDetected(partialQuery);
+                }
+            });
         }
 
         @Override
         public void onQueryFinished() {
             Slog.v(TAG, "BinderCallback#onQueryFinished");
-            Binder.withCleanCallingIdentity(() -> mExecutor.execute(
-                    () -> mCallback.onQueryFinished()));
+            Binder.withCleanCallingIdentity(() -> {
+                synchronized (mLock) {
+                    mCallback.onQueryFinished();
+                }
+            });
         }
 
         @Override
         public void onQueryRejected() {
             Slog.v(TAG, "BinderCallback#onQueryRejected");
-            Binder.withCleanCallingIdentity(() -> mExecutor.execute(
-                    () -> mCallback.onQueryRejected()));
+            Binder.withCleanCallingIdentity(() -> {
+                synchronized (mLock) {
+                    mCallback.onQueryRejected();
+                }
+            });
         }
 
         /** Called when the detection fails due to an error. */
diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java
index 31d759e..18080e4 100644
--- a/core/java/android/view/DisplayEventReceiver.java
+++ b/core/java/android/view/DisplayEventReceiver.java
@@ -287,6 +287,16 @@
     }
 
     /**
+     * Called when a display hdcp levels change event is received.
+     *
+     * @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.
+     * @param connectedLevel the new connected HDCP level
+     * @param maxLevel the maximum HDCP level
+     */
+    public void onHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, int maxLevel) {
+    }
+
+    /**
      * Represents a mapping between a UID and an override frame rate
      */
     public static class FrameRateOverride {
@@ -374,4 +384,11 @@
         onFrameRateOverridesChanged(timestampNanos, physicalDisplayId, overrides);
     }
 
+    // Called from native code.
+    @SuppressWarnings("unused")
+    private void dispatchHdcpLevelsChanged(long physicalDisplayId, int connectedLevel,
+            int maxLevel) {
+        onHdcpLevelsChanged(physicalDisplayId, connectedLevel, maxLevel);
+    }
+
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index e83488e..9f6395e 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -528,8 +528,6 @@
     // Set to true to stop input during an Activity Transition.
     boolean mPausedForTransition = false;
 
-    boolean mLastInCompatMode = false;
-
     SurfaceHolder.Callback2 mSurfaceHolderCallback;
     BaseSurfaceHolder mSurfaceHolder;
     boolean mIsCreating;
@@ -1375,11 +1373,6 @@
                 }
                 if (DEBUG_LAYOUT) Log.d(mTag, "WindowLayout in setView:" + attrs);
 
-                if (!compatibilityInfo.supportsScreen()) {
-                    attrs.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
-                    mLastInCompatMode = true;
-                }
-
                 mSoftInputMode = attrs.softInputMode;
                 mWindowAttributesChanged = true;
                 mAttachInfo.mRootView = view;
@@ -1913,10 +1906,6 @@
             // Keep track of the actual window flags supplied by the client.
             mClientWindowLayoutFlags = attrs.flags;
 
-            // Preserve compatible window flag if exists.
-            final int compatibleWindowFlag = mWindowAttributes.privateFlags
-                    & WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
-
             // Preserve system UI visibility.
             final int systemUiVisibility = mWindowAttributes.systemUiVisibility;
             final int subtreeSystemUiVisibility = mWindowAttributes.subtreeSystemUiVisibility;
@@ -1948,8 +1937,7 @@
             mWindowAttributes.subtreeSystemUiVisibility = subtreeSystemUiVisibility;
             mWindowAttributes.insetsFlags.appearance = appearance;
             mWindowAttributes.insetsFlags.behavior = behavior;
-            mWindowAttributes.privateFlags |= compatibleWindowFlag
-                    | appearanceAndBehaviorPrivateFlags;
+            mWindowAttributes.privateFlags |= appearanceAndBehaviorPrivateFlags;
 
             if (mWindowAttributes.preservePreviousSurfaceInsets) {
                 // Restore old surface insets.
@@ -3150,21 +3138,6 @@
         final boolean shouldOptimizeMeasure = shouldOptimizeMeasure(lp);
 
         WindowManager.LayoutParams params = null;
-        CompatibilityInfo compatibilityInfo =
-                mDisplay.getDisplayAdjustments().getCompatibilityInfo();
-        if (compatibilityInfo.supportsScreen() == mLastInCompatMode) {
-            params = lp;
-            mFullRedrawNeeded = true;
-            mLayoutRequested = true;
-            if (mLastInCompatMode) {
-                params.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
-                mLastInCompatMode = false;
-            } else {
-                params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
-                mLastInCompatMode = true;
-            }
-        }
-
         Rect frame = mWinFrame;
         if (mFirst) {
             mFullRedrawNeeded = true;
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 1712fd3..07a347a 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -3143,13 +3143,6 @@
         @UnsupportedAppUsage
         public static final int PRIVATE_FLAG_NO_MOVE_ANIMATION = 1 << 6;
 
-        /** Window flag: special flag to limit the size of the window to be
-         * original size ([320x480] x density). Used to create window for applications
-         * running under compatibility mode.
-         *
-         * {@hide} */
-        public static final int PRIVATE_FLAG_COMPATIBLE_WINDOW = 1 << 7;
-
         /** Window flag: a special option intended for system dialogs.  When
          * this flag is set, the window will demand focus unconditionally when
          * it is created.
@@ -3345,7 +3338,6 @@
                 SYSTEM_FLAG_SHOW_FOR_ALL_USERS,
                 PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION,
                 PRIVATE_FLAG_NO_MOVE_ANIMATION,
-                PRIVATE_FLAG_COMPATIBLE_WINDOW,
                 PRIVATE_FLAG_SYSTEM_ERROR,
                 PRIVATE_FLAG_OPTIMIZE_MEASURE,
                 PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
@@ -3399,10 +3391,6 @@
                         equals = PRIVATE_FLAG_NO_MOVE_ANIMATION,
                         name = "NO_MOVE_ANIMATION"),
                 @ViewDebug.FlagToString(
-                        mask = PRIVATE_FLAG_COMPATIBLE_WINDOW,
-                        equals = PRIVATE_FLAG_COMPATIBLE_WINDOW,
-                        name = "COMPATIBLE_WINDOW"),
-                @ViewDebug.FlagToString(
                         mask = PRIVATE_FLAG_SYSTEM_ERROR,
                         equals = PRIVATE_FLAG_SYSTEM_ERROR,
                         name = "SYSTEM_ERROR"),
diff --git a/core/java/android/window/BackMotionEvent.java b/core/java/android/window/BackMotionEvent.java
index c475723..7cda3a3 100644
--- a/core/java/android/window/BackMotionEvent.java
+++ b/core/java/android/window/BackMotionEvent.java
@@ -36,6 +36,7 @@
     private final float mProgress;
     private final float mVelocityX;
     private final float mVelocityY;
+    private final boolean mTriggerBack;
 
     @BackEvent.SwipeEdge
     private final int mSwipeEdge;
@@ -54,6 +55,7 @@
      *                  Value in pixels/second. {@link Float#NaN} if was not computed.
      * @param velocityY Y velocity computed from the touch point of this event.
      *                  Value in pixels/second. {@link Float#NaN} if was not computed.
+     * @param triggerBack Indicates whether the back arrow is in the triggered state or not
      * @param swipeEdge Indicates which edge the swipe starts from.
      * @param departingAnimationTarget The remote animation target of the departing
      *                                 application window.
@@ -64,6 +66,7 @@
             float progress,
             float velocityX,
             float velocityY,
+            boolean triggerBack,
             @BackEvent.SwipeEdge int swipeEdge,
             @Nullable RemoteAnimationTarget departingAnimationTarget) {
         mTouchX = touchX;
@@ -71,6 +74,7 @@
         mProgress = progress;
         mVelocityX = velocityX;
         mVelocityY = velocityY;
+        mTriggerBack = triggerBack;
         mSwipeEdge = swipeEdge;
         mDepartingAnimationTarget = departingAnimationTarget;
     }
@@ -81,6 +85,7 @@
         mProgress = in.readFloat();
         mVelocityX = in.readFloat();
         mVelocityY = in.readFloat();
+        mTriggerBack = in.readBoolean();
         mSwipeEdge = in.readInt();
         mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR);
     }
@@ -110,6 +115,7 @@
         dest.writeFloat(mProgress);
         dest.writeFloat(mVelocityX);
         dest.writeFloat(mVelocityY);
+        dest.writeBoolean(mTriggerBack);
         dest.writeInt(mSwipeEdge);
         dest.writeTypedObject(mDepartingAnimationTarget, flags);
     }
@@ -157,6 +163,15 @@
     }
 
     /**
+     * Returns whether the back arrow is in the triggered state or not
+     *
+     * @return boolean indicating whether the back arrow is in the triggered state or not
+     */
+    public boolean getTriggerBack() {
+        return mTriggerBack;
+    }
+
+    /**
      * Returns the screen edge that the swipe starts from.
      */
     @BackEvent.SwipeEdge
@@ -182,6 +197,7 @@
                 + ", mProgress=" + mProgress
                 + ", mVelocityX=" + mVelocityX
                 + ", mVelocityY=" + mVelocityY
+                + ", mTriggerBack=" + mTriggerBack
                 + ", mSwipeEdge" + mSwipeEdge
                 + ", mDepartingAnimationTarget" + mDepartingAnimationTarget
                 + "}";
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 52ad49a..216acdc 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -66,4 +66,12 @@
     description: "Predictive back for system animations"
     bug: "309545085"
     is_fixed_read_only: true
+}
+
+flag {
+    name: "activity_snapshot_by_default"
+    namespace: "systemui"
+    description: "Enable record activity snapshot by default"
+    bug: "259497289"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index c24d21d..17aad43 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -653,6 +653,8 @@
             sizeof("-Xps-save-resolved-classes-delay-ms:")-1 + PROPERTY_VALUE_MAX];
     char profileMinSavePeriodOptsBuf[sizeof("-Xps-min-save-period-ms:")-1 + PROPERTY_VALUE_MAX];
     char profileMinFirstSaveOptsBuf[sizeof("-Xps-min-first-save-ms:") - 1 + PROPERTY_VALUE_MAX];
+    char profileInlineCacheThresholdOptsBuf[
+            sizeof("-Xps-inline-cache-threshold:") - 1 + PROPERTY_VALUE_MAX];
     char madviseWillNeedFileSizeVdex[
             sizeof("-XMadviseWillNeedVdexFileSize:")-1 + PROPERTY_VALUE_MAX];
     char madviseWillNeedFileSizeOdex[
@@ -898,6 +900,9 @@
     parseRuntimeOption("dalvik.vm.ps-min-first-save-ms", profileMinFirstSaveOptsBuf,
             "-Xps-min-first-save-ms:");
 
+    parseRuntimeOption("dalvik.vm.ps-inline-cache-threshold", profileInlineCacheThresholdOptsBuf,
+            "-Xps-inline-cache-threshold:");
+
     property_get("ro.config.low_ram", propBuf, "");
     if (strcmp(propBuf, "true") == 0) {
       addOption("-XX:LowMemoryMode");
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index fef8ad7..f007cc5 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -41,6 +41,7 @@
     jmethodID dispatchHotplugConnectionError;
     jmethodID dispatchModeChanged;
     jmethodID dispatchFrameRateOverrides;
+    jmethodID dispatchHdcpLevelsChanged;
 
     struct {
         jclass clazz;
@@ -96,6 +97,8 @@
     void dispatchFrameRateOverrides(nsecs_t timestamp, PhysicalDisplayId displayId,
                                     std::vector<FrameRateOverride> overrides) override;
     void dispatchNullEvent(nsecs_t timestamp, PhysicalDisplayId displayId) override {}
+    void dispatchHdcpLevelsChanged(PhysicalDisplayId displayId, int connectedLevel,
+                                   int maxLevel) override;
 };
 
 NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak,
@@ -294,6 +297,22 @@
     mMessageQueue->raiseAndClearException(env, "dispatchModeChanged");
 }
 
+void NativeDisplayEventReceiver::dispatchHdcpLevelsChanged(PhysicalDisplayId displayId,
+                                                           int connectedLevel, int maxLevel) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+
+    ScopedLocalRef<jobject> receiverObj(env, GetReferent(env, mReceiverWeakGlobal));
+    if (receiverObj.get()) {
+        ALOGV("receiver %p ~ Invoking hdcp levels changed handler.", this);
+        env->CallVoidMethod(receiverObj.get(),
+                            gDisplayEventReceiverClassInfo.dispatchHdcpLevelsChanged,
+                            displayId.value, connectedLevel, maxLevel);
+        ALOGV("receiver %p ~ Returned from hdcp levels changed handler.", this);
+    }
+
+    mMessageQueue->raiseAndClearException(env, "dispatchHdcpLevelsChanged");
+}
+
 static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject vsyncEventDataWeak,
                         jobject messageQueueObj, jint vsyncSource, jint eventRegistration,
                         jlong layerHandle) {
@@ -385,6 +404,9 @@
             GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz,
                              "dispatchFrameRateOverrides",
                              "(JJ[Landroid/view/DisplayEventReceiver$FrameRateOverride;)V");
+    gDisplayEventReceiverClassInfo.dispatchHdcpLevelsChanged =
+            GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchHdcpLevelsChanged",
+                             "(JII)V");
 
     jclass frameRateOverrideClazz =
             FindClassOrDie(env, "android/view/DisplayEventReceiver$FrameRateOverride");
diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto
index c5889ba..75cfba0 100644
--- a/core/proto/android/server/activitymanagerservice.proto
+++ b/core/proto/android/server/activitymanagerservice.proto
@@ -429,6 +429,7 @@
         optional string base_dir = 1;
         optional string res_dir = 2;
         optional string data_dir = 3;
+        optional int32 targetSdkVersion = 4;
     }
     optional AppInfo appinfo = 8;
     optional ProcessRecordProto app = 9;
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index 1577d9c1c1..5b0502d 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -1244,29 +1244,33 @@
     }
 
     @Test
-    public void testBigPictureStyle_setExtras_pictureIconNull_noPictureIconKey() {
+    public void testBigPictureStyle_setExtras_pictureIconNull_pictureIconKeyNull() {
         Notification.BigPictureStyle bpStyle = new Notification.BigPictureStyle();
         bpStyle.bigPicture((Bitmap) null);
 
         Bundle extras = new Bundle();
         bpStyle.addExtras(extras);
 
-        assertThat(extras.containsKey(EXTRA_PICTURE_ICON)).isFalse();
+        assertThat(extras.containsKey(EXTRA_PICTURE_ICON)).isTrue();
+        final Parcelable pictureIcon = extras.getParcelable(EXTRA_PICTURE_ICON);
+        assertThat(pictureIcon).isNull();
     }
 
     @Test
-    public void testBigPictureStyle_setExtras_pictureIconNull_noPictureKey() {
+    public void testBigPictureStyle_setExtras_pictureIconNull_pictureKeyNull() {
         Notification.BigPictureStyle bpStyle = new Notification.BigPictureStyle();
         bpStyle.bigPicture((Bitmap) null);
 
         Bundle extras = new Bundle();
         bpStyle.addExtras(extras);
 
-        assertThat(extras.containsKey(EXTRA_PICTURE)).isFalse();
+        assertThat(extras.containsKey(EXTRA_PICTURE)).isTrue();
+        final Parcelable picture = extras.getParcelable(EXTRA_PICTURE);
+        assertThat(picture).isNull();
     }
 
     @Test
-    public void testBigPictureStyle_setExtras_pictureIconTypeBitmap_noPictureIconKey() {
+    public void testBigPictureStyle_setExtras_pictureIconTypeBitmap_pictureIconKeyNull() {
         Bitmap bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
         Notification.BigPictureStyle bpStyle = new Notification.BigPictureStyle();
         bpStyle.bigPicture(bitmap);
@@ -1274,11 +1278,13 @@
         Bundle extras = new Bundle();
         bpStyle.addExtras(extras);
 
-        assertThat(extras.containsKey(EXTRA_PICTURE_ICON)).isFalse();
+        assertThat(extras.containsKey(EXTRA_PICTURE_ICON)).isTrue();
+        final Parcelable pictureIcon = extras.getParcelable(EXTRA_PICTURE_ICON);
+        assertThat(pictureIcon).isNull();
     }
 
     @Test
-    public void testBigPictureStyle_setExtras_pictureIconTypeIcon_noPictureKey() {
+    public void testBigPictureStyle_setExtras_pictureIconTypeIcon_pictureKeyNull() {
         Icon icon = Icon.createWithResource(mContext, R.drawable.btn_plus);
         Notification.BigPictureStyle bpStyle = new Notification.BigPictureStyle();
         bpStyle.bigPicture(icon);
@@ -1286,7 +1292,9 @@
         Bundle extras = new Bundle();
         bpStyle.addExtras(extras);
 
-        assertThat(extras.containsKey(EXTRA_PICTURE)).isFalse();
+        assertThat(extras.containsKey(EXTRA_PICTURE)).isTrue();
+        final Parcelable picture = extras.getParcelable(EXTRA_PICTURE);
+        assertThat(picture).isNull();
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/hardware/face/FaceSensorConfigurationsTest.java b/core/tests/coretests/src/android/hardware/face/FaceSensorConfigurationsTest.java
new file mode 100644
index 0000000..da3a465
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/face/FaceSensorConfigurationsTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.face;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.biometrics.face.IFace;
+import android.hardware.biometrics.face.SensorProps;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.function.Function;
+
+@Presubmit
+@SmallTest
+public class FaceSensorConfigurationsTest {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+    @Mock
+    private Context mContext;
+    @Mock
+    private Resources mResources;
+    @Mock
+    private IFace mFace;
+    @Mock
+    private Function<String, IFace> mGetIFace;
+
+    private final String[] mAidlInstances = new String[]{"default", "virtual"};
+    private String[] mHidlConfigStrings = new String[]{"0:2:15", "0:8:15"};
+    private FaceSensorConfigurations mFaceSensorConfigurations;
+
+    @Before
+    public void setUp() throws RemoteException {
+        when(mGetIFace.apply(anyString())).thenReturn(mFace);
+        when(mFace.getSensorProps()).thenReturn(new SensorProps[]{});
+        when(mContext.getResources()).thenReturn(mResources);
+    }
+
+    @Test
+    public void testAidlInstanceSensorProps() {
+        mFaceSensorConfigurations = new FaceSensorConfigurations(false);
+        mFaceSensorConfigurations.addAidlConfigs(mAidlInstances, mGetIFace);
+
+        assertThat(mFaceSensorConfigurations.hasSensorConfigurations()).isTrue();
+        assertThat(!mFaceSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue();
+        assertThat(mFaceSensorConfigurations.getResetLockoutRequiresChallenge())
+                .isFalse();
+    }
+
+    @Test
+    public void testHidlConfigStrings() {
+        mFaceSensorConfigurations = new FaceSensorConfigurations(true);
+        mFaceSensorConfigurations.addHidlConfigs(mHidlConfigStrings, mContext);
+
+        assertThat(mFaceSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue();
+        assertThat(mFaceSensorConfigurations.getResetLockoutRequiresChallenge())
+                .isTrue();
+    }
+
+    @Test
+    public void testHidlConfigStrings_incorrectFormat() {
+        mHidlConfigStrings = new String[]{"0:8:15", "0:2", "0:face:15"};
+        mFaceSensorConfigurations = new FaceSensorConfigurations(true);
+        mFaceSensorConfigurations.addHidlConfigs(mHidlConfigStrings, mContext);
+
+        assertThat(mFaceSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue();
+        assertThat(mFaceSensorConfigurations.getResetLockoutRequiresChallenge())
+                .isTrue();
+    }
+}
diff --git a/core/tests/coretests/src/android/hardware/fingerprint/FingerprintSensorConfigurationsTest.java b/core/tests/coretests/src/android/hardware/fingerprint/FingerprintSensorConfigurationsTest.java
new file mode 100644
index 0000000..613089c
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/fingerprint/FingerprintSensorConfigurationsTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.fingerprint;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.biometrics.fingerprint.IFingerprint;
+import android.hardware.biometrics.fingerprint.SensorProps;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.function.Function;
+
+
+@Presubmit
+@SmallTest
+public class FingerprintSensorConfigurationsTest {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+    @Mock
+    private Context mContext;
+    @Mock
+    private Resources mResources;
+    @Mock
+    private IFingerprint mFingerprint;
+    @Mock
+    private Function<String, IFingerprint> mGetIFingerprint;
+
+    private final String[] mAidlInstances = new String[]{"default", "virtual"};
+    private String[] mHidlConfigStrings = new String[]{"0:2:15", "0:8:15"};
+    private FingerprintSensorConfigurations mFingerprintSensorConfigurations;
+
+    @Before
+    public void setUp() throws RemoteException {
+        when(mGetIFingerprint.apply(anyString())).thenReturn(mFingerprint);
+        when(mFingerprint.getSensorProps()).thenReturn(new SensorProps[]{});
+        when(mContext.getResources()).thenReturn(mResources);
+    }
+
+    @Test
+    public void testAidlInstanceSensorProps() {
+        mFingerprintSensorConfigurations = new FingerprintSensorConfigurations(
+                true /* resetLockoutRequiresHardwareAuthToken */);
+        mFingerprintSensorConfigurations.addAidlSensors(mAidlInstances, mGetIFingerprint);
+
+        assertThat(mFingerprintSensorConfigurations.hasSensorConfigurations()).isTrue();
+        assertThat(!mFingerprintSensorConfigurations.isSingleSensorConfigurationPresent())
+                .isTrue();
+        assertThat(mFingerprintSensorConfigurations.getResetLockoutRequiresHardwareAuthToken())
+                .isTrue();
+    }
+
+    @Test
+    public void testHidlConfigStrings() {
+        mFingerprintSensorConfigurations = new FingerprintSensorConfigurations(
+                false /* resetLockoutRequiresHardwareAuthToken */);
+        mFingerprintSensorConfigurations.addHidlSensors(mHidlConfigStrings, mContext);
+
+        assertThat(mFingerprintSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue();
+        assertThat(mFingerprintSensorConfigurations.getResetLockoutRequiresHardwareAuthToken())
+                .isFalse();
+    }
+
+    @Test
+    public void testHidlConfigStrings_incorrectFormat() {
+        mHidlConfigStrings = new String[]{"0:8:15", "0:2", "0:fingerprint:15"};
+        mFingerprintSensorConfigurations = new FingerprintSensorConfigurations(
+                false /* resetLockoutRequiresHardwareAuthToken */);
+        mFingerprintSensorConfigurations.addHidlSensors(mHidlConfigStrings, mContext);
+
+        assertThat(mFingerprintSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue();
+        assertThat(mFingerprintSensorConfigurations.getResetLockoutRequiresHardwareAuthToken())
+                .isFalse();
+    }
+}
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index 68c0693..a709d7b 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -84,6 +84,7 @@
             /* progress = */ 0,
             /* velocityX = */ 0,
             /* velocityY = */ 0,
+            /* triggerBack = */ false,
             /* swipeEdge = */ BackEvent.EDGE_LEFT,
             /* departingAnimationTarget = */ null);
 
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 69a6e6d..c6f920f 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -518,6 +518,8 @@
         <permission name="android.permission.ACCESS_BROADCAST_RADIO"/>
         <!-- Permission required for CTS test - CtsAmbientContextServiceTestCases -->
         <permission name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT"/>
+        <!-- Permission required for CTS test - CtsWearableSensingServiceTestCases -->
+        <permission name="android.permission.MANAGE_WEARABLE_SENSING_SERVICE"/>
         <!-- Permission required for CTS test - CtsTelephonyProviderTestCases -->
         <permission name="android.permission.WRITE_APN_SETTINGS"/>
         <!-- Permission required for GTS test - GtsStatsdHostTestCases -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java
index e06d3ef..5b0de50 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java
@@ -21,5 +21,4 @@
  */
 class BackAnimationConstants {
     static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.20f;
-    static final float PROGRESS_COMMIT_THRESHOLD = 0.1f;
 }
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 d8c691b..a498236 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
@@ -70,9 +70,11 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 
+import java.io.PrintWriter;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
@@ -124,6 +126,7 @@
     private final Context mContext;
     private final ContentResolver mContentResolver;
     private final ShellController mShellController;
+    private final ShellCommandHandler mShellCommandHandler;
     private final ShellExecutor mShellExecutor;
     private final Handler mBgHandler;
 
@@ -180,7 +183,8 @@
             @NonNull @ShellBackgroundThread Handler backgroundHandler,
             Context context,
             @NonNull BackAnimationBackground backAnimationBackground,
-            ShellBackAnimationRegistry shellBackAnimationRegistry) {
+            ShellBackAnimationRegistry shellBackAnimationRegistry,
+            ShellCommandHandler shellCommandHandler) {
         this(
                 shellInit,
                 shellController,
@@ -190,7 +194,8 @@
                 context,
                 context.getContentResolver(),
                 backAnimationBackground,
-                shellBackAnimationRegistry);
+                shellBackAnimationRegistry,
+                shellCommandHandler);
     }
 
     @VisibleForTesting
@@ -203,7 +208,8 @@
             Context context,
             ContentResolver contentResolver,
             @NonNull BackAnimationBackground backAnimationBackground,
-            ShellBackAnimationRegistry shellBackAnimationRegistry) {
+            ShellBackAnimationRegistry shellBackAnimationRegistry,
+            ShellCommandHandler shellCommandHandler) {
         mShellController = shellController;
         mShellExecutor = shellExecutor;
         mActivityTaskManager = activityTaskManager;
@@ -219,6 +225,7 @@
                 .build();
         mShellBackAnimationRegistry = shellBackAnimationRegistry;
         mLatencyTracker = LatencyTracker.getInstance(mContext);
+        mShellCommandHandler = shellCommandHandler;
     }
 
     private void onInit() {
@@ -227,6 +234,7 @@
         createAdapter();
         mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION,
                 this::createExternalInterface, this);
+        mShellCommandHandler.addDumpCallback(this::dump, this);
     }
 
     private void setupAnimationDeveloperSettingsObserver(
@@ -968,4 +976,20 @@
                 };
         mBackAnimationAdapter = new BackAnimationAdapter(runner);
     }
+
+    /**
+     * Description of current BackAnimationController state.
+     */
+    private void dump(PrintWriter pw, String prefix) {
+        pw.println(prefix + "BackAnimationController state:");
+        pw.println(prefix + "  mEnableAnimations=" + mEnableAnimations.get());
+        pw.println(prefix + "  mBackGestureStarted=" + mBackGestureStarted);
+        pw.println(prefix + "  mPostCommitAnimationInProgress=" + mPostCommitAnimationInProgress);
+        pw.println(prefix + "  mShouldStartOnNextMoveEvent=" + mShouldStartOnNextMoveEvent);
+        pw.println(prefix + "  mCurrentTracker state:");
+        mCurrentTracker.dump(pw, prefix + "    ");
+        pw.println(prefix + "  mQueuedTracker state:");
+        mQueuedTracker.dump(pw, prefix + "    ");
+    }
+
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
index 215a6cc..30d5edb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
@@ -18,9 +18,9 @@
 
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.window.BackEvent.EDGE_RIGHT;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
-import static com.android.wm.shell.back.BackAnimationConstants.PROGRESS_COMMIT_THRESHOLD;
 import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
 
@@ -91,7 +91,7 @@
                 }
             };
     private static final float MIN_WINDOW_ALPHA = 0.01f;
-    private static final float WINDOW_X_SHIFT_DP = 96;
+    private static final float WINDOW_X_SHIFT_DP = 48;
     private static final int SCALE_FACTOR = 100;
     // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists.
     private static final float TARGET_COMMIT_PROGRESS = 0.5f;
@@ -126,6 +126,8 @@
     private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
 
     private boolean mBackInProgress = false;
+    private boolean mIsRightEdge;
+    private boolean mTriggerBack = false;
 
     private PointF mTouchPos = new PointF();
     private IRemoteAnimationFinishedCallback mFinishCallback;
@@ -209,6 +211,7 @@
 
     private void finishAnimation() {
         if (mEnteringTarget != null) {
+            mTransaction.setCornerRadius(mEnteringTarget.leash, 0);
             mEnteringTarget.leash.release();
             mEnteringTarget = null;
         }
@@ -241,14 +244,15 @@
 
     private void onGestureProgress(@NonNull BackEvent backEvent) {
         if (!mBackInProgress) {
+            mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
             mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
             mBackInProgress = true;
         }
         mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
 
         float progress = backEvent.getProgress();
-        float springProgress = (progress > PROGRESS_COMMIT_THRESHOLD
-                ? mapLinear(progress, 0.1f, 1, TARGET_COMMIT_PROGRESS, 1)
+        float springProgress = (mTriggerBack
+                ? mapLinear(progress, 0f, 1, TARGET_COMMIT_PROGRESS, 1)
                 : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
         mLeavingProgressSpring.animateToFinalPosition(springProgress);
         mEnteringProgressSpring.animateToFinalPosition(springProgress);
@@ -312,7 +316,7 @@
             transformWithProgress(
                     mEnteringProgress,
                     Math.max(
-                            smoothstep(ENTER_ALPHA_THRESHOLD, 1, mEnteringProgress),
+                            smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress),
                             MIN_WINDOW_ALPHA),  /* alpha */
                     mEnteringTarget.leash,
                     mEnteringRect,
@@ -337,14 +341,13 @@
                     mClosingTarget.leash,
                     mClosingRect,
                     0,
-                    mWindowXShift
+                    mIsRightEdge ? 0 : mWindowXShift
             );
         }
     }
 
     private void transformWithProgress(float progress, float alpha, SurfaceControl surface,
             RectF targetRect, float deltaXMin, float deltaXMax) {
-        final float touchY = mTouchPos.y;
 
         final int width = mStartTaskRect.width();
         final int height = mStartTaskRect.height();
@@ -376,12 +379,14 @@
     private final class Callback extends IOnBackInvokedCallback.Default {
         @Override
         public void onBackStarted(BackMotionEvent backEvent) {
+            mTriggerBack = backEvent.getTriggerBack();
             mProgressAnimator.onBackStarted(backEvent,
                     CrossActivityBackAnimation.this::onGestureProgress);
         }
 
         @Override
         public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
+            mTriggerBack = backEvent.getTriggerBack();
             mProgressAnimator.onBackProgressed(backEvent);
         }
 
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
index 4bd56d4..6213f62 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
@@ -24,6 +24,8 @@
 import android.window.BackEvent;
 import android.window.BackMotionEvent;
 
+import java.io.PrintWriter;
+
 /**
  * Helper class to record the touch location for gesture and generate back events.
  */
@@ -129,6 +131,7 @@
                 /* progress = */ 0,
                 /* velocityX = */ 0,
                 /* velocityY = */ 0,
+                /* triggerBack = */ mTriggerBack,
                 /* swipeEdge = */ mSwipeEdge,
                 /* departingAnimationTarget = */ target);
     }
@@ -204,6 +207,7 @@
                 /* progress = */ progress,
                 /* velocityX = */ mLatestVelocityX,
                 /* velocityY = */ mLatestVelocityY,
+                /* triggerBack = */ mTriggerBack,
                 /* swipeEdge = */ mSwipeEdge,
                 /* departingAnimationTarget = */ null);
     }
@@ -219,6 +223,12 @@
         mNonLinearFactor = nonLinearFactor;
     }
 
+    void dump(PrintWriter pw, String prefix) {
+        pw.println(prefix + "TouchTracker state:");
+        pw.println(prefix + "  mState=" + mState);
+        pw.println(prefix + "  mTriggerBack=" + mTriggerBack);
+    }
+
     enum TouchTrackerState {
         INITIAL, ACTIVE, FINISHED
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index b7f749e..470a825 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -1291,7 +1291,7 @@
             // We only show user education for conversation bubbles right now
             return false;
         }
-        final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION);
+        final boolean seen = getPrefBoolean(ManageEducationView.PREF_MANAGED_EDUCATION);
         final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext))
                 && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null;
         if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
@@ -1342,7 +1342,7 @@
             // We only show user education for conversation bubbles right now
             return false;
         }
-        final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION);
+        final boolean seen = getPrefBoolean(StackEducationView.PREF_STACK_EDUCATION);
         final boolean shouldShow = !seen || BubbleDebugConfig.forceShowUserEducation(mContext);
         if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
             Log.d(TAG, "Show stack edu: " + shouldShow);
@@ -2323,7 +2323,8 @@
         updateOverflowVisibility();
         updatePointerPosition(false /* forIme */);
         mExpandedAnimationController.expandFromStack(() -> {
-            if (mIsExpanded && mExpandedBubble.getExpandedView() != null) {
+            if (mIsExpanded && mExpandedBubble != null
+                    && mExpandedBubble.getExpandedView() != null) {
                 maybeShowManageEdu();
             }
             updateOverflowDotVisibility(true /* expanding */);
@@ -2384,7 +2385,7 @@
         }
         mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
 
-        if (mExpandedBubble.getExpandedView() != null) {
+        if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
             mExpandedBubble.getExpandedView().setContentAlpha(0f);
             mExpandedBubble.getExpandedView().setBackgroundAlpha(0f);
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
index 61e17c8..da71b1c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
@@ -33,15 +33,16 @@
  * User education view to highlight the manage button that allows a user to configure the settings
  * for the bubble. Shown only the first time a user expands a bubble.
  */
-class ManageEducationView(context: Context, positioner: BubblePositioner) : LinearLayout(context) {
+class ManageEducationView(
+    context: Context,
+    private val positioner: BubblePositioner
+) : LinearLayout(context) {
 
-    private val TAG =
-        if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "ManageEducationView"
-        else BubbleDebugConfig.TAG_BUBBLES
+    companion object {
+        const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding"
+        private const val ANIMATE_DURATION: Long = 200
+    }
 
-    private val ANIMATE_DURATION: Long = 200
-
-    private val positioner: BubblePositioner = positioner
     private val manageView by lazy { requireViewById<ViewGroup>(R.id.manage_education_view) }
     private val manageButton by lazy { requireViewById<Button>(R.id.manage_button) }
     private val gotItButton by lazy { requireViewById<Button>(R.id.got_it) }
@@ -128,7 +129,7 @@
                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                 .alpha(1f)
         }
-        setShouldShow(false)
+        updateManageEducationSeen()
     }
 
     /**
@@ -218,13 +219,11 @@
             }
     }
 
-    private fun setShouldShow(shouldShow: Boolean) {
+    private fun updateManageEducationSeen() {
         context
             .getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
             .edit()
-            .putBoolean(PREF_MANAGED_EDUCATION, !shouldShow)
+            .putBoolean(PREF_MANAGED_EDUCATION, true)
             .apply()
     }
 }
-
-const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
index 2cabb65..95f1017 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
@@ -34,19 +34,15 @@
  */
 class StackEducationView(
     context: Context,
-    positioner: BubblePositioner,
-    controller: BubbleController
+    private val positioner: BubblePositioner,
+    private val controller: BubbleController
 ) : LinearLayout(context) {
 
-    private val TAG =
-        if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView"
-        else BubbleDebugConfig.TAG_BUBBLES
-
-    private val ANIMATE_DURATION: Long = 200
-    private val ANIMATE_DURATION_SHORT: Long = 40
-
-    private val positioner: BubblePositioner = positioner
-    private val controller: BubbleController = controller
+    companion object {
+        const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding"
+        private const val ANIMATE_DURATION: Long = 200
+        private const val ANIMATE_DURATION_SHORT: Long = 40
+    }
 
     private val view by lazy { requireViewById<View>(R.id.stack_education_layout) }
     private val titleTextView by lazy { requireViewById<TextView>(R.id.stack_education_title) }
@@ -175,7 +171,7 @@
                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                 .alpha(1f)
         }
-        setShouldShow(false)
+        updateStackEducationSeen()
         return true
     }
 
@@ -196,13 +192,11 @@
             .withEndAction { visibility = GONE }
     }
 
-    private fun setShouldShow(shouldShow: Boolean) {
+    private fun updateStackEducationSeen() {
         context
             .getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
             .edit()
-            .putBoolean(PREF_STACK_EDUCATION, !shouldShow)
+            .putBoolean(PREF_STACK_EDUCATION, true)
             .apply()
     }
 }
-
-const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
index 5b0239f..02af2d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
@@ -563,7 +563,7 @@
                         ? p.x - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR
                         : p.x + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
                 animationForChild(child)
-                        .translationX(fromX, p.y)
+                        .translationX(fromX, p.x)
                         .start();
             } else {
                 float fromY = p.y - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
@@ -634,4 +634,9 @@
                     .start();
         }
     }
+
+    /** Returns true if we're in the middle of a collapse or expand animation. */
+    boolean isAnimating() {
+        return mAnimatingCollapse || mAnimatingExpand;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 3c6bc17..fc97c798 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -363,7 +363,8 @@
             @ShellMainThread ShellExecutor shellExecutor,
             @ShellBackgroundThread Handler backgroundHandler,
             BackAnimationBackground backAnimationBackground,
-            Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry) {
+            Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry,
+            ShellCommandHandler shellCommandHandler) {
         if (BackAnimationController.IS_ENABLED) {
             return shellBackAnimationRegistry.map(
                     (animations) ->
@@ -374,7 +375,8 @@
                                     backgroundHandler,
                                     context,
                                     backAnimationBackground,
-                                    animations));
+                                    animations,
+                                    shellCommandHandler));
         }
         return Optional.empty();
     }
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 ae21c4b..f58aeac 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
@@ -277,11 +277,6 @@
         params.token = appToken;
         params.packageName = activityInfo.packageName;
         params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
-
-        if (!context.getResources().getCompatibilityInfo().supportsScreen()) {
-            params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
-        }
-
         params.setTitle("Splash Screen " + title);
         return params;
     }
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 771876f..9ded6ea 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
@@ -63,6 +63,7 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.sysui.ShellSharedConstants;
@@ -110,6 +111,8 @@
 
     @Mock
     private InputManager mInputManager;
+    @Mock
+    private ShellCommandHandler mShellCommandHandler;
 
     private BackAnimationController mController;
     private TestableContentResolver mContentResolver;
@@ -145,7 +148,8 @@
                         mContext,
                         mContentResolver,
                         mAnimationBackground,
-                        mShellBackAnimationRegistry);
+                        mShellBackAnimationRegistry,
+                        mShellCommandHandler);
         mShellInit.init();
         mShellExecutor.flushAll();
     }
@@ -298,7 +302,8 @@
                         mContext,
                         mContentResolver,
                         mAnimationBackground,
-                        mShellBackAnimationRegistry);
+                        mShellBackAnimationRegistry,
+                        mShellCommandHandler);
         shellInit.init();
         registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java
index 874ef80..91503b1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java
@@ -53,6 +53,7 @@
                 /* progress = */ progress,
                 /* velocityX = */ 0,
                 /* velocityY = */ 0,
+                /* triggerBack = */ false,
                 /* swipeEdge = */ BackEvent.EDGE_LEFT,
                 /* departingAnimationTarget = */ null);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
index c1ff260..60f1d271 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
@@ -16,52 +16,51 @@
 
 package com.android.wm.shell.bubbles.animation;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.annotation.SuppressLint;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Insets;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.testing.AndroidTestingRunner;
 import android.view.View;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
 
 import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.R;
 import com.android.wm.shell.bubbles.BubblePositioner;
 import com.android.wm.shell.bubbles.BubbleStackView;
 
+import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestCase {
 
-    private int mDisplayWidth = 500;
-    private int mDisplayHeight = 1000;
-
-    private Runnable mOnBubbleAnimatedOutAction = mock(Runnable.class);
+    private final Semaphore mBubbleRemovedSemaphore = new Semaphore(0);
+    private final Runnable mOnBubbleAnimatedOutAction = mBubbleRemovedSemaphore::release;
     ExpandedAnimationController mExpandedController;
 
     private int mStackOffset;
     private PointF mExpansionPoint;
     private BubblePositioner mPositioner;
-    private BubbleStackView.StackViewState mStackViewState = new BubbleStackView.StackViewState();
+    private final BubbleStackView.StackViewState mStackViewState =
+            new BubbleStackView.StackViewState();
 
-    @SuppressLint("VisibleForTests")
     @Before
     public void setUp() throws Exception {
         super.setUp();
@@ -70,15 +69,13 @@
                 getContext().getSystemService(WindowManager.class));
         mPositioner.updateInternal(Configuration.ORIENTATION_PORTRAIT,
                 Insets.of(0, 0, 0, 0),
-                new Rect(0, 0, mDisplayWidth, mDisplayHeight));
+                new Rect(0, 0, 500, 1000));
 
         BubbleStackView stackView = mock(BubbleStackView.class);
-        when(stackView.getState()).thenReturn(getStackViewState());
 
         mExpandedController = new ExpandedAnimationController(mPositioner,
                 mOnBubbleAnimatedOutAction,
                 stackView);
-        spyOn(mExpandedController);
 
         addOneMoreThanBubbleLimitBubbles();
         mLayout.setActiveController(mExpandedController);
@@ -86,9 +83,18 @@
         Resources res = mLayout.getResources();
         mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
         mExpansionPoint = new PointF(100, 100);
+
+        getStackViewState();
+        when(stackView.getState()).thenAnswer(i -> getStackViewState());
+        waitForMainThread();
     }
 
-    public BubbleStackView.StackViewState getStackViewState() {
+    @After
+    public void tearDown() {
+        waitForMainThread();
+    }
+
+    private BubbleStackView.StackViewState getStackViewState() {
         mStackViewState.numberOfBubbles = mLayout.getChildCount();
         mStackViewState.selectedIndex = 0;
         mStackViewState.onLeft = mPositioner.isStackOnLeft(mExpansionPoint);
@@ -96,68 +102,71 @@
     }
 
     @Test
-    @Ignore
-    public void testExpansionAndCollapse() throws InterruptedException {
-        Runnable afterExpand = mock(Runnable.class);
-        mExpandedController.expandFromStack(afterExpand);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
+    public void testExpansionAndCollapse() throws Exception {
+        expand();
         testBubblesInCorrectExpandedPositions();
-        verify(afterExpand).run();
+        waitForMainThread();
 
-        Runnable afterCollapse = mock(Runnable.class);
+        final Semaphore semaphore = new Semaphore(0);
+        Runnable afterCollapse = semaphore::release;
         mExpandedController.collapseBackToStack(mExpansionPoint, false, afterCollapse);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
-        testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1);
-        verify(afterExpand).run();
+        assertThat(semaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isTrue();
+        waitForAnimation();
+        testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y);
     }
 
     @Test
-    @Ignore
-    public void testOnChildAdded() throws InterruptedException {
+    public void testOnChildAdded() throws Exception {
         expand();
+        waitForMainThread();
 
         // Add another new view and wait for its animation.
         final View newView = new FrameLayout(getContext());
         mLayout.addView(newView, 0);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
 
+        waitForAnimation();
         testBubblesInCorrectExpandedPositions();
     }
 
     @Test
-    @Ignore
-    public void testOnChildRemoved() throws InterruptedException {
+    public void testOnChildRemoved() throws Exception {
         expand();
+        waitForMainThread();
 
-        // Remove some views and see if the remaining child views still pass the expansion test.
+        // Remove some views and verify the remaining child views still pass the expansion test.
         mLayout.removeView(mViews.get(0));
         mLayout.removeView(mViews.get(3));
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+
+        // Removing a view will invoke onBubbleAnimatedOutAction. Block until it gets called twice.
+        assertThat(mBubbleRemovedSemaphore.tryAcquire(2, 2, TimeUnit.SECONDS)).isTrue();
+
+        waitForAnimation();
         testBubblesInCorrectExpandedPositions();
     }
 
     @Test
-    public void testDragBubbleOutDoesntNPE() throws InterruptedException {
+    public void testDragBubbleOutDoesntNPE() {
         mExpandedController.onGestureFinished();
         mExpandedController.dragBubbleOut(mViews.get(0), 1, 1);
     }
 
     /** Expand the stack and wait for animations to finish. */
     private void expand() throws InterruptedException {
-        mExpandedController.expandFromStack(mock(Runnable.class));
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+        final Semaphore semaphore = new Semaphore(0);
+        Runnable afterExpand = semaphore::release;
+
+        mExpandedController.expandFromStack(afterExpand);
+        assertThat(semaphore.tryAcquire(1, TimeUnit.SECONDS)).isTrue();
     }
 
     /** Check that children are in the correct positions for being stacked. */
-    private void testStackedAtPosition(float x, float y, int offsetMultiplier) {
+    private void testStackedAtPosition(float x, float y) {
         // Make sure the rest of the stack moved again, including the first bubble not moving, and
         // is stacked to the right now that we're on the right side of the screen.
         for (int i = 0; i < mLayout.getChildCount(); i++) {
-            assertEquals(x + i * offsetMultiplier * mStackOffset,
-                    mLayout.getChildAt(i).getTranslationX(), 2f);
-            assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f);
+            assertEquals(x, mLayout.getChildAt(i).getTranslationX(), 2f);
+            assertEquals(y + Math.min(i, 1) * mStackOffset, mLayout.getChildAt(i).getTranslationY(),
+                    2f);
             assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
         }
     }
@@ -175,4 +184,22 @@
                     mLayout.getChildAt(i).getTranslationY(), 2f);
         }
     }
+
+    private void waitForAnimation() throws Exception {
+        final Semaphore semaphore = new Semaphore(0);
+        boolean[] animating = new boolean[]{ true };
+        for (int i = 0; i < 4; i++) {
+            if (animating[0]) {
+                mMainThreadHandler.post(() -> {
+                    if (!mExpandedController.isAnimating()) {
+                        animating[0] = false;
+                        semaphore.release();
+                    }
+                });
+                Thread.sleep(500);
+            }
+        }
+        assertThat(semaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isTrue();
+        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java
index 48ae296..2ed5add 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java
@@ -164,11 +164,17 @@
 
         @Override
         public void cancelAllAnimations() {
+            if (mLayout.getChildCount() == 0) {
+                return;
+            }
             mMainThreadHandler.post(super::cancelAllAnimations);
         }
 
         @Override
         public void cancelAnimationsOnView(View view) {
+            if (mLayout.getChildCount() == 0) {
+                return;
+            }
             mMainThreadHandler.post(() -> super.cancelAnimationsOnView(view));
         }
 
@@ -221,6 +227,9 @@
 
             @Override
             protected void startPathAnimation() {
+                if (mLayout.getChildCount() == 0) {
+                    return;
+                }
                 mMainThreadHandler.post(super::startPathAnimation);
             }
         }
@@ -322,4 +331,9 @@
             e.printStackTrace();
         }
     }
+
+    /** Waits for the main thread to finish processing all pending runnables. */
+    public void waitForMainThread() {
+        runOnMainThreadAndBlock(() -> {});
+    }
 }
diff --git a/location/java/android/location/flags/gnss.aconfig b/location/java/android/location/flags/gnss.aconfig
index b6055e8..f4b1056 100644
--- a/location/java/android/location/flags/gnss.aconfig
+++ b/location/java/android/location/flags/gnss.aconfig
@@ -20,3 +20,10 @@
     description: "Flag for GnssMeasurementRequest WorkSource API"
     bug: "295235160"
 }
+
+flag {
+    name: "release_supl_connection_on_timeout"
+    namespace: "location"
+    description: "Flag for releasing SUPL connection on timeout"
+    bug: "315024652"
+}
diff --git a/media/java/android/media/AudioDeviceInfo.java b/media/java/android/media/AudioDeviceInfo.java
index 5a72b0b..1a3d7b7 100644
--- a/media/java/android/media/AudioDeviceInfo.java
+++ b/media/java/android/media/AudioDeviceInfo.java
@@ -23,6 +23,8 @@
 import android.annotation.TestApi;
 import android.util.SparseIntArray;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
@@ -402,7 +404,9 @@
 
     private final AudioDevicePort mPort;
 
-    AudioDeviceInfo(AudioDevicePort port) {
+    /** @hide */
+    @VisibleForTesting
+    public AudioDeviceInfo(AudioDevicePort port) {
        mPort = port;
     }
 
diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java
index 73bc6f9..2de8eef 100644
--- a/media/java/android/media/AudioDevicePort.java
+++ b/media/java/android/media/AudioDevicePort.java
@@ -20,6 +20,8 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
 
+import com.android.aconfig.annotations.VisibleForTesting;
+
 import java.util.Arrays;
 import java.util.List;
 
@@ -38,6 +40,26 @@
 
 public class AudioDevicePort extends AudioPort {
 
+    /** @hide */
+    // TODO: b/316864909 - Remove this method once there's a way to fake audio device ports further
+    // down the stack.
+    @VisibleForTesting
+    public static AudioDevicePort createForTesting(
+            int type, @NonNull String name, @NonNull String address) {
+        return new AudioDevicePort(
+                new AudioHandle(/* id= */ 0),
+                name,
+                /* samplingRates= */ null,
+                /* channelMasks= */ null,
+                /* channelIndexMasks= */ null,
+                /* formats= */ null,
+                /* gains= */ null,
+                type,
+                address,
+                /* encapsulationModes= */ null,
+                /* encapsulationMetadataTypes= */ null);
+    }
+
     private final int mType;
     private final String mAddress;
     private final int[] mEncapsulationModes;
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 3dfd572..a5a69f9 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -22,6 +22,7 @@
 import static android.media.audio.Flags.autoPublicVolumeApiHardening;
 import static android.media.audio.Flags.automaticBtDeviceType;
 import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
+import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
@@ -5120,6 +5121,71 @@
     }
 
     /**
+     * Notifies an application with a focus listener of gain or loss of audio focus
+     *
+     * <p>This is similar to {@link #dispatchAudioFocusChange(AudioFocusInfo, int, AudioPolicy)} but
+     * with additional functionality  of fade. The players of the application with  audio focus
+     * change, provided they meet the active {@link FadeManagerConfiguration} requirements, are
+     * faded before dispatching the callback to the application. For example, players of the
+     * application losing audio focus will be faded out, whereas players of the application gaining
+     * audio focus will be faded in, if needed.
+     *
+     * <p>The applicability of fade is decided against the supplied active {@link AudioFocusInfo}.
+     * This list cannot be {@code null}. The list can be empty if no other active
+     * {@link AudioFocusInfo} available at the time of the dispatch.
+     *
+     * <p>The {@link FadeManagerConfiguration} supplied here is prioritized over existing fade
+     * configurations. If none supplied, either the {@link FadeManagerConfiguration} set through
+     * {@link AudioPolicy} or the default will be used to determine the fade properties.
+     *
+     * <p>This method can only be used by owners of an {@link AudioPolicy} configured with
+     * {@link AudioPolicy.Builder#setIsAudioFocusPolicy(boolean)} set to true.
+     *
+     * @param afi the recipient of the focus change, that has previously requested audio focus, and
+     *     that was received by the {@code AudioPolicy} through
+     *     {@link AudioPolicy.AudioPolicyFocusListener#onAudioFocusRequest(AudioFocusInfo, int)}
+     * @param focusChange one of focus gain types ({@link #AUDIOFOCUS_GAIN},
+     *     {@link #AUDIOFOCUS_GAIN_TRANSIENT}, {@link #AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} or
+     *     {@link #AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE})
+     *     or one of the focus loss types ({@link AudioManager#AUDIOFOCUS_LOSS},
+     *     {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT},
+     *     or {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK}).
+     *     <br>For the focus gain, the change type should be the same as the app requested
+     * @param ap a valid registered {@link AudioPolicy} configured as a focus policy.
+     * @param otherActiveAfis active {@link AudioFocusInfo} that are granted audio focus at the time
+     *     of dispatch
+     * @param transientFadeMgrConfig {@link FadeManagerConfiguration} that will be used for fading
+     *     players resulting from this dispatch. This is a transient configuration that is only
+     *     valid for this focus change and shall be discarded after processing this request.
+     * @return {@link #AUDIOFOCUS_REQUEST_FAILED} if the focus client didn't have a listener or if
+     *     there was an error sending the request, or {@link #AUDIOFOCUS_REQUEST_GRANTED} if the
+     *     dispatch was successfully sent, or {@link #AUDIOFOCUS_REQUEST_DELAYED} if
+     *     the request was successful but the dispatch of focus change was delayed due to a fade
+     *     operation.
+     * @throws NullPointerException if the {@link AudioFocusInfo} or {@link AudioPolicy} or list of
+     *     other active {@link AudioFocusInfo} are {@code null}.
+     * @hide
+     */
+    @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION)
+    @SystemApi
+    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int dispatchAudioFocusChangeWithFade(@NonNull AudioFocusInfo afi, int focusChange,
+            @NonNull AudioPolicy ap, @NonNull List<AudioFocusInfo> otherActiveAfis,
+            @Nullable FadeManagerConfiguration transientFadeMgrConfig) {
+        Objects.requireNonNull(afi, "AudioFocusInfo cannot be null");
+        Objects.requireNonNull(ap, "AudioPolicy cannot be null");
+        Objects.requireNonNull(otherActiveAfis, "Other active AudioFocusInfo list cannot be null");
+
+        IAudioService service = getService();
+        try {
+            return service.dispatchFocusChangeWithFade(afi, focusChange, ap.cb(), otherActiveAfis,
+                    transientFadeMgrConfig);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * @hide
      * Used internally by telephony package to abandon audio focus, typically after a call or
      * when ringing ends and the call is rejected or not answered.
diff --git a/media/java/android/media/FadeManagerConfiguration.java b/media/java/android/media/FadeManagerConfiguration.java
index 337d4b0..40b0e3e 100644
--- a/media/java/android/media/FadeManagerConfiguration.java
+++ b/media/java/android/media/FadeManagerConfiguration.java
@@ -16,12 +16,13 @@
 
 package android.media;
 
-import static com.android.media.flags.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
+import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
 
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArrayMap;
@@ -93,11 +94,9 @@
  *      Helps with recreating a new instance from another to simply change/add on top of the
  *      existing ones</li>
  * </ul>
- * TODO(b/304835727): Convert into system API so that it can be set through AudioPolicy
- *
  * @hide
  */
-
+@SystemApi
 @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION)
 public final class FadeManagerConfiguration implements Parcelable {
 
@@ -523,6 +522,7 @@
      *
      * @param fadeState one of the fade state in {@link FadeStateEnum}
      * @return human-readable string
+     * @hide
      */
     @NonNull
     public static String fadeStateToString(@FadeStateEnum int fadeState) {
@@ -712,7 +712,8 @@
      *
      * <p><b>Notes:</b>
      * <ul>
-     *     <li>When fade state is set to enabled, the builder expects at least one valid usage to be
+     *     <li>When fade state is set to {@link #FADE_STATE_ENABLED_DEFAULT} or
+     *     {@link #FADE_STATE_ENABLED_AUTO}, the builder expects at least one valid usage to be
      *     set/added. Failure to do so will result in an exception during {@link #build()}</li>
      *     <li>Every usage added to the fadeable list should have corresponding volume shaper
      *     configs defined. This can be achieved by setting either the duration or volume shaper
@@ -720,8 +721,8 @@
      *     {@link #setFadeOutVolumeShaperConfigForUsage(int, VolumeShaper.Configuration)}</li>
      *     <li> It is recommended to set volume shaper configurations individually for fade out and
      *     fade in</li>
-     *     <li>For any incomplete volume shaper configs a volume shaper configuration will be
-     *     created using either the default fade durations or the ones provided as part of the
+     *     <li>For any incomplete volume shaper configurations, a volume shaper configuration will
+     *     be created using either the default fade durations or the ones provided as part of the
      *     {@link #Builder(long, long)}</li>
      *     <li>Additional volume shaper configs can also configured for a given usage
      *     with additional attributes like content-type in order to achieve finer fade controls.
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index a52f0b0..5c268d4 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -28,6 +28,7 @@
 import android.media.AudioRecordingConfiguration;
 import android.media.AudioRoutesInfo;
 import android.media.BluetoothProfileConnectionInfo;
+import android.media.FadeManagerConfiguration;
 import android.media.IAudioDeviceVolumeDispatcher;
 import android.media.IAudioFocusDispatcher;
 import android.media.IAudioModeDispatcher;
@@ -398,6 +399,14 @@
     int dispatchFocusChange(in AudioFocusInfo afi, in int focusChange,
             in IAudioPolicyCallback pcb);
 
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)")
+    int dispatchFocusChangeWithFade(in AudioFocusInfo afi,
+            in int focusChange,
+            in IAudioPolicyCallback pcb,
+            in List<AudioFocusInfo> otherActiveAfis,
+            in FadeManagerConfiguration transientFadeMgrConfig);
+
     oneway void playerHasOpPlayAudio(in int piid, in boolean hasOpPlayAudio);
 
     @EnforcePermission("BLUETOOTH_STACK")
@@ -754,4 +763,16 @@
     oneway void removeLoudnessCodecInfo(int piid, in LoudnessCodecInfo codecInfo);
 
     PersistableBundle getLoudnessParams(int piid, in LoudnessCodecInfo codecInfo);
+
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)")
+    int setFadeManagerConfigurationForFocusLoss(in FadeManagerConfiguration fmcForFocusLoss);
+
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)")
+    int clearFadeManagerConfigurationForFocusLoss();
+
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)")
+    FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss();
 }
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index e168498..b85decc 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -16,6 +16,8 @@
 
 package android.media.audiopolicy;
 
+import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
+
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -34,6 +36,7 @@
 import android.media.AudioManager;
 import android.media.AudioRecord;
 import android.media.AudioTrack;
+import android.media.FadeManagerConfiguration;
 import android.media.IAudioService;
 import android.media.MediaRecorder;
 import android.media.projection.MediaProjection;
@@ -49,6 +52,7 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -612,6 +616,110 @@
         return mRegistrationId;
     }
 
+    /**
+     * Sets a custom {@link FadeManagerConfiguration} to handle fade cycle of players during
+     * {@link android.media.AudioManager#AUDIOFOCUS_LOSS}
+     *
+     * @param fmcForFocusLoss custom {@link FadeManagerConfiguration}
+     * @return {@link AudioManager#SUCCESS} if the update was successful,
+     *     {@link AudioManager#ERROR} otherwise
+     * @throws IllegalStateException if the audio policy is not registered
+     * @hide
+     */
+    @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION)
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    @SystemApi
+    public int setFadeManagerConfigurationForFocusLoss(
+            @NonNull FadeManagerConfiguration fmcForFocusLoss) {
+        Objects.requireNonNull(fmcForFocusLoss,
+                "FadeManagerConfiguration for focus loss cannot be null");
+
+        IAudioService service = getService();
+        synchronized (mLock) {
+            Preconditions.checkState(isAudioPolicyRegisteredLocked(),
+                    "Cannot set FadeManagerConfiguration with unregistered AudioPolicy");
+
+            try {
+                return service.setFadeManagerConfigurationForFocusLoss(fmcForFocusLoss);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Received remote exception for setFadeManagerConfigurationForFocusLoss:",
+                        e);
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Clear the current {@link FadeManagerConfiguration} set to handle fade cycles of players
+     * during {@link android.media.AudioManager#AUDIOFOCUS_LOSS}
+     *
+     * <p>In the absence of custom {@link FadeManagerConfiguration}, the default configurations will
+     * be used to handle fade cycles during audio focus loss.
+     *
+     * @return {@link AudioManager#SUCCESS} if the update was successful,
+     *     {@link AudioManager#ERROR} otherwise
+     * @throws IllegalStateException if the audio policy is not registered
+     * @see #setFadeManagerConfigurationForFocusLoss(FadeManagerConfiguration)
+     * @hide
+     */
+    @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION)
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    @SystemApi
+    public int clearFadeManagerConfigurationForFocusLoss() {
+        IAudioService service = getService();
+        synchronized (mLock) {
+            Preconditions.checkState(isAudioPolicyRegisteredLocked(),
+                    "Cannot clear FadeManagerConfiguration from unregistered AudioPolicy");
+
+            try {
+                return service.clearFadeManagerConfigurationForFocusLoss();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Received remote exception for "
+                                + "clearFadeManagerConfigurationForFocusLoss:", e);
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Get the current fade manager configuration used for fade operations during
+     * {@link android.media.AudioManager#AUDIOFOCUS_LOSS}
+     *
+     * <p>If no custom {@link FadeManagerConfiguration} is set, the default configuration currently
+     * active will be returned.
+     *
+     * @return the active {@link FadeManagerConfiguration} used during audio focus loss
+     * @throws IllegalStateException if the audio policy is not registered
+     * @see #setFadeManagerConfigurationForFocusLoss(FadeManagerConfiguration)
+     * @see #clearFadeManagerConfigurationForFocusLoss()
+     * @hide
+     */
+    @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION)
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    @SystemApi
+    @NonNull
+    public FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss() {
+        IAudioService service = getService();
+        synchronized (mLock) {
+            Preconditions.checkState(isAudioPolicyRegisteredLocked(),
+                    "Cannot get FadeManagerConfiguration from unregistered AudioPolicy");
+
+            try {
+                return service.getFadeManagerConfigurationForFocusLoss();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Received remote exception for getFadeManagerConfigurationForFocusLoss:",
+                        e);
+                throw e.rethrowFromSystemServer();
+
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
+    private boolean isAudioPolicyRegisteredLocked() {
+        return mStatus == POLICY_STATUS_REGISTERED;
+    }
+
     private boolean policyReadyToUse() {
         synchronized (mLock) {
             if (mStatus != POLICY_STATUS_REGISTERED) {
diff --git a/media/java/android/media/flags/fade_manager_configuration.aconfig b/media/java/android/media/flags/fade_manager_configuration.aconfig
deleted file mode 100644
index 100e2235..0000000
--- a/media/java/android/media/flags/fade_manager_configuration.aconfig
+++ /dev/null
@@ -1,8 +0,0 @@
-package: "com.android.media.flags"
-
-flag {
-    namespace: "media_solutions"
-    name: "enable_fade_manager_configuration"
-    description: "Enable Fade Manager Configuration support to determine fade properties"
-    bug: "307354764"
-}
\ No newline at end of file
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java
index fb6bd48..f105ae9 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java
@@ -16,7 +16,7 @@
 
 package com.android.audiopolicytest;
 
-import static com.android.media.flags.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
+import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
 
 import static org.junit.Assert.assertThrows;
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
index 562d20d..7fb959a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
@@ -20,6 +20,7 @@
 import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.app.AlertDialog;
+import android.app.Flags;
 import android.app.NotificationManager;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -143,9 +144,16 @@
                                     Slog.d(TAG, "Invalid manual condition: " + tag.condition);
                                 }
                                 // always triggers priority-only dnd with chosen condition
-                                mNotificationManager.setZenMode(
-                                        Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
-                                        getRealConditionId(tag.condition), TAG);
+                                if (Flags.modesApi()) {
+                                    mNotificationManager.setZenMode(
+                                            Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                            getRealConditionId(tag.condition), TAG,
+                                            /* fromUser= */ true);
+                                } else {
+                                    mNotificationManager.setZenMode(
+                                            Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                            getRealConditionId(tag.condition), TAG);
+                                }
                             }
                         });
 
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index adebdcd..d5814e3 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -59,10 +59,10 @@
         // Note we statically link SettingsProviderLib to do some unit tests.  It's not accessible otherwise
         // because this test is not an instrumentation test. (because the target runs in the system process.)
         "SettingsProviderLib",
-
         "androidx.test.rules",
         "flag-junit",
         "junit",
+        "libaconfig_java_proto_lite",
         "mockito-target-minus-junit4",
         "platform-test-annotations",
         "truth",
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 8f459c6..73c2e22 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -18,6 +18,9 @@
 
 import static android.os.Process.FIRST_APPLICATION_UID;
 
+import android.aconfig.Aconfig.flag_state;
+import android.aconfig.Aconfig.parsed_flag;
+import android.aconfig.Aconfig.parsed_flags;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -147,6 +150,17 @@
      */
     private static final String CONFIG_STAGED_PREFIX = "staged/";
 
+    private static final List<String> sAconfigTextProtoFilesOnDevice = List.of(
+            "/system/etc/aconfig_flags.pb",
+            "/system_ext/etc/aconfig_flags.pb",
+            "/product/etc/aconfig_flags.pb",
+            "/vendor/etc/aconfig_flags.pb");
+
+    /**
+     * This tag is applied to all aconfig default value-loaded flags.
+     */
+    private static final String BOOT_LOADED_DEFAULT_TAG = "BOOT_LOADED_DEFAULT";
+
     // This was used in version 120 and before.
     private static final String NULL_VALUE_OLD_STYLE = "null";
 
@@ -315,6 +329,59 @@
 
         synchronized (mLock) {
             readStateSyncLocked();
+
+            if (Flags.loadAconfigDefaults()) {
+                // Only load aconfig defaults if this is the first boot, the XML
+                // file doesn't exist yet, or this device is on its first boot after
+                // an OTA.
+                boolean shouldLoadAconfigValues = isConfigSettingsKey(mKey)
+                        && (!file.exists()
+                                || mContext.getPackageManager().isDeviceUpgrading());
+                if (shouldLoadAconfigValues) {
+                    loadAconfigDefaultValuesLocked();
+                }
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void loadAconfigDefaultValuesLocked() {
+        for (String fileName : sAconfigTextProtoFilesOnDevice) {
+            try (FileInputStream inputStream = new FileInputStream(fileName)) {
+                byte[] contents = inputStream.readAllBytes();
+                loadAconfigDefaultValues(contents);
+            } catch (IOException e) {
+                Slog.e(LOG_TAG, "failed to read protobuf", e);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    @GuardedBy("mLock")
+    public void loadAconfigDefaultValues(byte[] fileContents) {
+        try {
+            parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents);
+
+            if (parsedFlags == null) {
+                Slog.e(LOG_TAG, "failed to parse aconfig protobuf");
+                return;
+            }
+
+            for (parsed_flag flag : parsedFlags.getParsedFlagList()) {
+                String flagName = flag.getNamespace() + "/"
+                        + flag.getPackage() + "." + flag.getName();
+                String value = flag.getState() == flag_state.ENABLED ? "true" : "false";
+
+                Setting existingSetting = getSettingLocked(flagName);
+                boolean isDefaultLoaded = existingSetting.getTag() != null
+                        && existingSetting.getTag().equals(BOOT_LOADED_DEFAULT_TAG);
+                if (existingSetting.getValue() == null || isDefaultLoaded) {
+                    insertSettingLocked(flagName, value, BOOT_LOADED_DEFAULT_TAG,
+                            false, flag.getPackage());
+                }
+            }
+        } catch (IOException e) {
+            Slog.e(LOG_TAG, "failed to parse protobuf", e);
         }
     }
 
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
index 27ce0d4..ecac5ee 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
+++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
@@ -6,3 +6,11 @@
     description: "When enabled, allows setting and displaying local overrides via adb."
     bug: "298392357"
 }
+
+flag {
+    name: "load_aconfig_defaults"
+    namespace: "core_experiments_team_internal"
+    description: "When enabled, loads aconfig default values into DeviceConfig on boot."
+    bug: "311155098"
+    is_fixed_read_only: true
+}
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 02a7bc1..24625ea 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -15,6 +15,9 @@
  */
 package com.android.providers.settings;
 
+import android.aconfig.Aconfig;
+import android.aconfig.Aconfig.parsed_flag;
+import android.aconfig.Aconfig.parsed_flags;
 import android.os.Looper;
 import android.test.AndroidTestCase;
 import android.util.Xml;
@@ -84,6 +87,86 @@
         super.tearDown();
     }
 
+    public void testLoadValidAconfigProto() {
+        int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
+        Object lock = new Object();
+        SettingsState settingsState = new SettingsState(
+                getContext(), lock, mSettingsFile, configKey,
+                SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
+
+        parsed_flags flags = parsed_flags
+                .newBuilder()
+                .addParsedFlag(parsed_flag
+                    .newBuilder()
+                        .setPackage("com.android.flags")
+                        .setName("flag1")
+                        .setNamespace("test_namespace")
+                        .setDescription("test flag")
+                        .addBug("12345678")
+                        .setState(Aconfig.flag_state.DISABLED)
+                        .setPermission(Aconfig.flag_permission.READ_WRITE))
+                .addParsedFlag(parsed_flag
+                    .newBuilder()
+                        .setPackage("com.android.flags")
+                        .setName("flag2")
+                        .setNamespace("test_namespace")
+                        .setDescription("another test flag")
+                        .addBug("12345678")
+                        .setState(Aconfig.flag_state.ENABLED)
+                        .setPermission(Aconfig.flag_permission.READ_WRITE))
+                .build();
+
+        synchronized (lock) {
+            settingsState.loadAconfigDefaultValues(flags.toByteArray());
+            settingsState.persistSettingsLocked();
+        }
+        settingsState.waitForHandler();
+
+        synchronized (lock) {
+            assertEquals("false",
+                    settingsState.getSettingLocked(
+                        "test_namespace/com.android.flags.flag1").getValue());
+            assertEquals("true",
+                    settingsState.getSettingLocked(
+                        "test_namespace/com.android.flags.flag2").getValue());
+        }
+    }
+
+    public void testSkipLoadingAconfigFlagWithMissingFields() {
+        int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
+        Object lock = new Object();
+        SettingsState settingsState = new SettingsState(
+                getContext(), lock, mSettingsFile, configKey,
+                SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
+
+        parsed_flags flags = parsed_flags
+                .newBuilder()
+                .addParsedFlag(parsed_flag
+                    .newBuilder()
+                        .setDescription("test flag")
+                        .addBug("12345678")
+                        .setState(Aconfig.flag_state.DISABLED)
+                        .setPermission(Aconfig.flag_permission.READ_WRITE))
+                .build();
+
+        synchronized (lock) {
+            settingsState.loadAconfigDefaultValues(flags.toByteArray());
+            settingsState.persistSettingsLocked();
+        }
+        settingsState.waitForHandler();
+
+        synchronized (lock) {
+            assertEquals(null,
+                    settingsState.getSettingLocked(
+                        "test_namespace/com.android.flags.flag1").getValue());
+        }
+    }
+
+    public void testInvalidAconfigProtoDoesNotCrash() {
+        SettingsState settingsState = getSettingStateObject();
+        settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes());
+    }
+
     public void testIsBinary() {
         assertFalse(SettingsState.isBinary(" abc 日本語"));
 
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 6e65c16..477c42e 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -688,6 +688,9 @@
     <!-- Permission required for CTS test - CtsAmbientContextDetectionServiceDeviceTest -->
     <uses-permission android:name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT" />
 
+    <!-- Permission required for CTS test - CtsWearableSensingServiceTestCases -->
+    <uses-permission android:name="android.permission.MANAGE_WEARABLE_SENSING_SERVICE" />
+
     <!-- Permission required for CTS test - CallAudioInterceptionTest -->
     <uses-permission android:name="android.permission.CALL_AUDIO_INTERCEPTION" />
 
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 1a35f04..a03fa9b 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -256,6 +256,9 @@
     <!-- launcher apps -->
     <uses-permission android:name="android.permission.ACCESS_SHORTCUTS" />
 
+    <!-- Permission to start Launcher's widget picker activity. -->
+    <uses-permission android:name="android.permission.START_WIDGET_PICKER_ACTIVITY" />
+
     <uses-permission android:name="android.permission.MODIFY_THEME_OVERLAY" />
 
     <!-- accessibility -->
@@ -974,15 +977,6 @@
             android:excludeFromRecents="true"
             android:visibleToInstantApps="true"/>
 
-        <activity android:name="com.android.systemui.communal.widgets.WidgetPickerActivity"
-            android:theme="@style/Theme.EditWidgetsActivity"
-            android:excludeFromRecents="true"
-            android:autoRemoveFromRecents="true"
-            android:showOnLockScreen="true"
-            android:launchMode="singleTop"
-            android:exported="false">
-        </activity>
-
         <activity android:name="com.android.systemui.communal.widgets.EditWidgetsActivity"
             android:theme="@style/Theme.EditWidgetsActivity"
             android:excludeFromRecents="true"
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index ab4fe76..8b27960 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -45,6 +45,13 @@
 }
 
 flag {
+    name: "notifications_improved_hun_animation"
+    namespace: "systemui"
+    description: "Adds a translateY animation, and other improvements to match the motion specs of the HUN Intro + Outro animations."
+    bug: "243302608"
+}
+
+flag {
     name: "notification_lifetime_extension_refactor"
     namespace: "systemui"
     description: "Enables moving notification lifetime extension management from SystemUI to "
@@ -241,10 +248,16 @@
 }
 
 flag {
+   name: "switch_user_on_bg"
+   namespace: "systemui"
+   description: "Does user switching on a background thread"
+   bug: "284095720"
+}
+
+flag {
     name: "status_bar_static_inout_indicators"
     namespace: "systemui"
     description: "(Upstream request) Always show the network activity inout indicators and "
         "prefer using alpha to distinguish network activity."
     bug: "310715220"
 }
-
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
index 4d53cca..cbf2496 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.keyguard.KeyguardViewConfigurator
 import com.android.systemui.keyguard.qualifiers.KeyguardRootView
 import com.android.systemui.keyguard.ui.composable.LockscreenScene
+import com.android.systemui.keyguard.ui.composable.LockscreenSceneBlueprintModule
 import com.android.systemui.scene.shared.model.Scene
 import dagger.Binds
 import dagger.Module
@@ -29,7 +30,12 @@
 import javax.inject.Provider
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
-@Module
+@Module(
+    includes =
+        [
+            LockscreenSceneBlueprintModule::class,
+        ],
+)
 interface LockscreenSceneModule {
 
     @Binds @IntoSet fun lockscreenScene(scene: LockscreenScene): Scene
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
new file mode 100644
index 0000000..2cb0034
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.transitions
+import com.android.systemui.keyguard.ui.composable.blueprint.LockscreenSceneBlueprint
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
+import javax.inject.Inject
+
+/**
+ * Renders the content of the lockscreen.
+ *
+ * This is separate from the [LockscreenScene] because it's meant to support usage of this UI from
+ * outside the scene container framework.
+ */
+class LockscreenContent
+@Inject
+constructor(
+    private val viewModel: LockscreenContentViewModel,
+    private val blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>,
+) {
+
+    private val sceneKeyByBlueprint: Map<LockscreenSceneBlueprint, SceneKey> by lazy {
+        blueprints.associateWith { blueprint -> SceneKey(blueprint.id) }
+    }
+    private val sceneKeyByBlueprintId: Map<String, SceneKey> by lazy {
+        sceneKeyByBlueprint.entries.associate { (blueprint, sceneKey) -> blueprint.id to sceneKey }
+    }
+
+    @Composable
+    fun Content(
+        modifier: Modifier = Modifier,
+    ) {
+        val coroutineScope = rememberCoroutineScope()
+        val blueprintId by viewModel.blueprintId(coroutineScope).collectAsState()
+
+        // Switch smoothly between blueprints, any composable tagged with element() will be
+        // transition-animated between any two blueprints, if they both display the same element.
+        SceneTransitionLayout(
+            currentScene = checkNotNull(sceneKeyByBlueprintId[blueprintId]),
+            onChangeScene = {},
+            transitions =
+                transitions { sceneKeyByBlueprintId.values.forEach { sceneKey -> to(sceneKey) } },
+            modifier = modifier,
+        ) {
+            sceneKeyByBlueprint.entries.forEach { (blueprint, sceneKey) ->
+                scene(sceneKey) { with(blueprint) { Content(Modifier.fillMaxSize()) } }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 3053654..93f31ec 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalFoundationApi::class)
+@file:OptIn(ExperimentalFoundationApi::class)
 
 package com.android.systemui.keyguard.ui.composable
 
@@ -50,14 +50,17 @@
 import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.scene.shared.model.UserAction
 import com.android.systemui.scene.ui.composable.ComposableScene
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
+/** Set this to `true` to use the LockscreenContent replacement of KeyguardRootView. */
+private val UseLockscreenContent = false
+
 /** The lock screen scene shows when the device is locked. */
 @SysUISingleton
 class LockscreenScene
@@ -66,6 +69,7 @@
     @Application private val applicationScope: CoroutineScope,
     private val viewModel: LockscreenSceneViewModel,
     @KeyguardRootView private val viewProvider: () -> @JvmSuppressWildcards View,
+    private val lockscreenContent: Lazy<LockscreenContent>,
 ) : ComposableScene {
     override val key = SceneKey.Lockscreen
 
@@ -91,6 +95,7 @@
         LockscreenScene(
             viewProvider = viewProvider,
             viewModel = viewModel,
+            lockscreenContent = lockscreenContent,
             modifier = modifier,
         )
     }
@@ -113,6 +118,7 @@
 private fun SceneScope.LockscreenScene(
     viewProvider: () -> View,
     viewModel: LockscreenSceneViewModel,
+    lockscreenContent: Lazy<LockscreenContent>,
     modifier: Modifier = Modifier,
 ) {
     fun findSettingsMenu(): View {
@@ -133,16 +139,24 @@
             modifier = Modifier.fillMaxSize(),
         )
 
-        AndroidView(
-            factory = { _ ->
-                val keyguardRootView = viewProvider()
-                // Remove the KeyguardRootView from any parent it might already have in legacy code
-                // just in case (a view can't have two parents).
-                (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView)
-                keyguardRootView
-            },
-            modifier = Modifier.fillMaxSize(),
-        )
+        if (UseLockscreenContent) {
+            lockscreenContent
+                .get()
+                .Content(
+                    modifier = Modifier.fillMaxSize(),
+                )
+        } else {
+            AndroidView(
+                factory = { _ ->
+                    val keyguardRootView = viewProvider()
+                    // Remove the KeyguardRootView from any parent it might already have in legacy
+                    // code just in case (a view can't have two parents).
+                    (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView)
+                    keyguardRootView
+                },
+                modifier = Modifier.fillMaxSize(),
+            )
+        }
 
         val notificationStackPosition by viewModel.keyguardRoot.notificationBounds.collectAsState()
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
new file mode 100644
index 0000000..9abb50c
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable
+
+import com.android.systemui.keyguard.ui.composable.blueprint.CommunalBlueprintModule
+import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule
+import com.android.systemui.keyguard.ui.composable.blueprint.ShortcutsBesideUdfpsBlueprintModule
+import com.android.systemui.keyguard.ui.composable.blueprint.SplitShadeBlueprintModule
+import dagger.Module
+
+@Module(
+    includes =
+        [
+            CommunalBlueprintModule::class,
+            DefaultBlueprintModule::class,
+            ShortcutsBesideUdfpsBlueprintModule::class,
+            SplitShadeBlueprintModule::class,
+        ],
+)
+interface LockscreenSceneBlueprintModule
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt
new file mode 100644
index 0000000..7eddaaf
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.blueprint
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+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.Color
+import com.android.compose.animation.scene.SceneScope
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import javax.inject.Inject
+
+/** Renders the lockscreen scene when showing the communal glanceable hub. */
+class CommunalBlueprint @Inject constructor() : LockscreenSceneBlueprint {
+
+    override val id: String = "communal"
+
+    @Composable
+    override fun SceneScope.Content(modifier: Modifier) {
+        Box(modifier.background(Color.Black)) {
+            Text(
+                text = "TODO(b/316211368): communal blueprint",
+                color = Color.White,
+                modifier = Modifier.align(Alignment.Center),
+            )
+        }
+    }
+}
+
+@Module
+interface CommunalBlueprintModule {
+    @Binds @IntoSet fun blueprint(blueprint: CommunalBlueprint): LockscreenSceneBlueprint
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
new file mode 100644
index 0000000..fc1df84
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.blueprint
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.IntOffset
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.height
+import com.android.compose.modifiers.width
+import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
+import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
+import com.android.systemui.keyguard.ui.composable.section.ClockSection
+import com.android.systemui.keyguard.ui.composable.section.LockSection
+import com.android.systemui.keyguard.ui.composable.section.NotificationSection
+import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
+import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import javax.inject.Inject
+
+/**
+ * Renders the lockscreen scene when showing with the default layout (e.g. vertical phone form
+ * factor).
+ */
+class DefaultBlueprint
+@Inject
+constructor(
+    private val viewModel: LockscreenContentViewModel,
+    private val statusBarSection: StatusBarSection,
+    private val clockSection: ClockSection,
+    private val smartSpaceSection: SmartSpaceSection,
+    private val notificationSection: NotificationSection,
+    private val lockSection: LockSection,
+    private val ambientIndicationSection: AmbientIndicationSection,
+    private val bottomAreaSection: BottomAreaSection,
+) : LockscreenSceneBlueprint {
+
+    override val id: String = "default"
+
+    @Composable
+    override fun SceneScope.Content(modifier: Modifier) {
+        val context = LocalContext.current
+        val lockIconBounds = lockSection.lockIconBounds(context)
+        val isUdfpsVisible = viewModel.isUdfpsVisible
+
+        Box(
+            modifier = modifier,
+        ) {
+            Column(
+                modifier = Modifier.fillMaxWidth().height { lockIconBounds.top },
+            ) {
+                with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
+                with(clockSection) { SmallClock(modifier = Modifier.fillMaxWidth()) }
+                with(smartSpaceSection) { SmartSpace(modifier = Modifier.fillMaxWidth()) }
+                with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
+                with(notificationSection) {
+                    Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
+                }
+                if (!isUdfpsVisible) {
+                    with(ambientIndicationSection) {
+                        AmbientIndication(modifier = Modifier.fillMaxWidth())
+                    }
+                }
+            }
+
+            with(lockSection) {
+                LockIcon(
+                    modifier =
+                        Modifier.width { lockIconBounds.width() }
+                            .height { lockIconBounds.height() }
+                            .offset { IntOffset(lockIconBounds.left, lockIconBounds.top) }
+                )
+            }
+
+            Column(modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter)) {
+                if (isUdfpsVisible) {
+                    with(ambientIndicationSection) {
+                        AmbientIndication(modifier = Modifier.fillMaxWidth())
+                    }
+                }
+
+                with(bottomAreaSection) { BottomArea(modifier = Modifier.fillMaxWidth()) }
+            }
+        }
+    }
+}
+
+@Module
+interface DefaultBlueprintModule {
+    @Binds @IntoSet fun blueprint(blueprint: DefaultBlueprint): LockscreenSceneBlueprint
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt
new file mode 100644
index 0000000..6d9cba4
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.blueprint
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.SceneScope
+
+/** Defines interface for classes that can render the content for a specific blueprint/layout. */
+interface LockscreenSceneBlueprint {
+
+    /** The ID that uniquely identifies this blueprint across all other blueprints. */
+    val id: String
+
+    /** Renders the content of this blueprint. */
+    @Composable fun SceneScope.Content(modifier: Modifier)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
new file mode 100644
index 0000000..fa913f1
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.blueprint
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.IntOffset
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.height
+import com.android.compose.modifiers.width
+import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
+import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
+import com.android.systemui.keyguard.ui.composable.section.ClockSection
+import com.android.systemui.keyguard.ui.composable.section.LockSection
+import com.android.systemui.keyguard.ui.composable.section.NotificationSection
+import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
+import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
+import com.android.systemui.res.R
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import javax.inject.Inject
+import kotlin.math.roundToInt
+
+/**
+ * Renders the lockscreen scene when showing with the default layout (e.g. vertical phone form
+ * factor).
+ */
+class ShortcutsBesideUdfpsBlueprint
+@Inject
+constructor(
+    private val viewModel: LockscreenContentViewModel,
+    private val statusBarSection: StatusBarSection,
+    private val clockSection: ClockSection,
+    private val smartSpaceSection: SmartSpaceSection,
+    private val notificationSection: NotificationSection,
+    private val lockSection: LockSection,
+    private val ambientIndicationSection: AmbientIndicationSection,
+    private val bottomAreaSection: BottomAreaSection,
+) : LockscreenSceneBlueprint {
+
+    override val id: String = "shortcuts-besides-udfps"
+
+    @Composable
+    override fun SceneScope.Content(modifier: Modifier) {
+        val context = LocalContext.current
+        val lockIconBounds = lockSection.lockIconBounds(context)
+        val isUdfpsVisible = viewModel.isUdfpsVisible
+
+        Box(
+            modifier = modifier,
+        ) {
+            Column(
+                modifier = Modifier.fillMaxWidth().height { lockIconBounds.top },
+            ) {
+                with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
+                with(clockSection) { SmallClock(modifier = Modifier.fillMaxWidth()) }
+                with(smartSpaceSection) { SmartSpace(modifier = Modifier.fillMaxWidth()) }
+                with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
+                with(notificationSection) {
+                    Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
+                }
+                if (!isUdfpsVisible) {
+                    with(ambientIndicationSection) {
+                        AmbientIndication(modifier = Modifier.fillMaxWidth())
+                    }
+                }
+            }
+
+            val shortcutSizePx =
+                with(LocalDensity.current) { bottomAreaSection.shortcutSizeDp().toSize() }
+
+            Row(
+                verticalAlignment = Alignment.CenterVertically,
+                modifier =
+                    Modifier.fillMaxWidth().offset {
+                        val rowTop =
+                            if (shortcutSizePx.height > lockIconBounds.height()) {
+                                (lockIconBounds.top -
+                                        (shortcutSizePx.height + lockIconBounds.height()) / 2)
+                                    .roundToInt()
+                            } else {
+                                lockIconBounds.top
+                            }
+
+                        IntOffset(0, rowTop)
+                    },
+            ) {
+                Spacer(Modifier.weight(1f))
+
+                with(bottomAreaSection) { Shortcut(isStart = true) }
+
+                Spacer(Modifier.weight(1f))
+
+                with(lockSection) {
+                    LockIcon(
+                        modifier =
+                            Modifier.width { lockIconBounds.width() }
+                                .height { lockIconBounds.height() }
+                    )
+                }
+
+                Spacer(Modifier.weight(1f))
+
+                with(bottomAreaSection) { Shortcut(isStart = false) }
+
+                Spacer(Modifier.weight(1f))
+            }
+
+            Column(modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter)) {
+                if (isUdfpsVisible) {
+                    with(ambientIndicationSection) {
+                        AmbientIndication(modifier = Modifier.fillMaxWidth())
+                    }
+                }
+
+                with(bottomAreaSection) {
+                    IndicationArea(
+                        modifier =
+                            Modifier.fillMaxWidth()
+                                .padding(
+                                    horizontal =
+                                        dimensionResource(
+                                            R.dimen.keyguard_affordance_horizontal_offset
+                                        )
+                                )
+                                .padding(
+                                    bottom =
+                                        dimensionResource(
+                                            R.dimen.keyguard_affordance_vertical_offset
+                                        )
+                                )
+                                .heightIn(min = shortcutSizeDp().height),
+                    )
+                }
+            }
+        }
+    }
+}
+
+@Module
+interface ShortcutsBesideUdfpsBlueprintModule {
+    @Binds
+    @IntoSet
+    fun blueprint(blueprint: ShortcutsBesideUdfpsBlueprint): LockscreenSceneBlueprint
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
new file mode 100644
index 0000000..7545d5f
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.blueprint
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+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.Color
+import com.android.compose.animation.scene.SceneScope
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import javax.inject.Inject
+
+/**
+ * Renders the lockscreen scene when showing with a split shade (e.g. unfolded foldable and/or
+ * tablet form factor).
+ */
+class SplitShadeBlueprint @Inject constructor() : LockscreenSceneBlueprint {
+
+    override val id: String = "split-shade"
+
+    @Composable
+    override fun SceneScope.Content(modifier: Modifier) {
+        Box(modifier.background(Color.Black)) {
+            Text(
+                text = "TODO(b/316211368): split shade blueprint",
+                color = Color.White,
+                modifier = Modifier.align(Alignment.Center),
+            )
+        }
+    }
+}
+
+@Module
+interface SplitShadeBlueprintModule {
+    @Binds @IntoSet fun blueprint(blueprint: SplitShadeBlueprint): LockscreenSceneBlueprint
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt
new file mode 100644
index 0000000..0e7ac5e
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.section
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+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.Color
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import javax.inject.Inject
+
+class AmbientIndicationSection @Inject constructor() {
+    @Composable
+    fun SceneScope.AmbientIndication(modifier: Modifier = Modifier) {
+        MovableElement(
+            key = AmbientIndicationElementKey,
+            modifier = modifier,
+        ) {
+            Box(
+                modifier = Modifier.fillMaxWidth().background(Color.Green),
+            ) {
+                Text(
+                    text = "TODO(b/316211368): Ambient indication",
+                    color = Color.White,
+                    modifier = Modifier.align(Alignment.Center),
+                )
+            }
+        }
+    }
+}
+
+private val AmbientIndicationElementKey = ElementKey("AmbientIndication")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
new file mode 100644
index 0000000..53e4be3
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.section
+
+import android.view.View
+import android.widget.ImageView
+import androidx.annotation.IdRes
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.res.ResourcesCompat
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.animation.view.LaunchableImageView
+import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
+import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
+import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.KeyguardIndicationController
+import com.android.systemui.statusbar.VibratorHelper
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.flow.Flow
+
+class BottomAreaSection
+@Inject
+constructor(
+    private val viewModel: KeyguardQuickAffordancesCombinedViewModel,
+    private val falsingManager: FalsingManager,
+    private val vibratorHelper: VibratorHelper,
+    private val indicationController: KeyguardIndicationController,
+    private val indicationAreaViewModel: KeyguardIndicationAreaViewModel,
+    private val keyguardRootViewModel: KeyguardRootViewModel,
+) {
+    @Composable
+    fun SceneScope.BottomArea(
+        modifier: Modifier = Modifier,
+    ) {
+        Row(
+            modifier =
+                modifier
+                    .padding(
+                        horizontal =
+                            dimensionResource(R.dimen.keyguard_affordance_horizontal_offset)
+                    )
+                    .padding(
+                        bottom = dimensionResource(R.dimen.keyguard_affordance_vertical_offset)
+                    ),
+        ) {
+            Shortcut(
+                isStart = true,
+            )
+
+            IndicationArea(
+                modifier = Modifier.weight(1f),
+            )
+
+            Shortcut(
+                isStart = false,
+            )
+        }
+    }
+
+    @Composable
+    fun SceneScope.Shortcut(
+        isStart: Boolean,
+        modifier: Modifier = Modifier,
+    ) {
+        MovableElement(
+            key = if (isStart) StartButtonElementKey else EndButtonElementKey,
+            modifier = modifier,
+        ) {
+            Shortcut(
+                viewId = if (isStart) R.id.start_button else R.id.end_button,
+                viewModel = if (isStart) viewModel.startButton else viewModel.endButton,
+                transitionAlpha = viewModel.transitionAlpha,
+                falsingManager = falsingManager,
+                vibratorHelper = vibratorHelper,
+                indicationController = indicationController,
+            )
+        }
+    }
+
+    @Composable
+    fun SceneScope.IndicationArea(
+        modifier: Modifier = Modifier,
+    ) {
+        MovableElement(
+            key = IndicationAreaElementKey,
+            modifier = modifier,
+        ) {
+            IndicationArea(
+                indicationAreaViewModel = indicationAreaViewModel,
+                keyguardRootViewModel = keyguardRootViewModel,
+                indicationController = indicationController,
+            )
+        }
+    }
+
+    @Composable
+    fun shortcutSizeDp(): DpSize {
+        return DpSize(
+            width = dimensionResource(R.dimen.keyguard_affordance_fixed_width),
+            height = dimensionResource(R.dimen.keyguard_affordance_fixed_height),
+        )
+    }
+
+    @Composable
+    private fun Shortcut(
+        @IdRes viewId: Int,
+        viewModel: Flow<KeyguardQuickAffordanceViewModel>,
+        transitionAlpha: Flow<Float>,
+        falsingManager: FalsingManager,
+        vibratorHelper: VibratorHelper,
+        indicationController: KeyguardIndicationController,
+        modifier: Modifier = Modifier,
+    ) {
+        val (binding, setBinding) = mutableStateOf<KeyguardQuickAffordanceViewBinder.Binding?>(null)
+
+        AndroidView(
+            factory = { context ->
+                val padding =
+                    context.resources.getDimensionPixelSize(
+                        R.dimen.keyguard_affordance_fixed_padding
+                    )
+                val view =
+                    LaunchableImageView(context, null).apply {
+                        id = viewId
+                        scaleType = ImageView.ScaleType.FIT_CENTER
+                        background =
+                            ResourcesCompat.getDrawable(
+                                context.resources,
+                                R.drawable.keyguard_bottom_affordance_bg,
+                                context.theme
+                            )
+                        foreground =
+                            ResourcesCompat.getDrawable(
+                                context.resources,
+                                R.drawable.keyguard_bottom_affordance_selected_border,
+                                context.theme
+                            )
+                        visibility = View.INVISIBLE
+                        setPadding(padding, padding, padding, padding)
+                    }
+
+                setBinding(
+                    KeyguardQuickAffordanceViewBinder.bind(
+                        view,
+                        viewModel,
+                        transitionAlpha,
+                        falsingManager,
+                        vibratorHelper,
+                    ) {
+                        indicationController.showTransientIndication(it)
+                    }
+                )
+
+                view
+            },
+            onRelease = { binding?.destroy() },
+            modifier =
+                modifier.size(
+                    width = shortcutSizeDp().width,
+                    height = shortcutSizeDp().height,
+                )
+        )
+    }
+
+    @Composable
+    private fun IndicationArea(
+        indicationAreaViewModel: KeyguardIndicationAreaViewModel,
+        keyguardRootViewModel: KeyguardRootViewModel,
+        indicationController: KeyguardIndicationController,
+        modifier: Modifier = Modifier,
+    ) {
+        val (disposable, setDisposable) = mutableStateOf<DisposableHandle?>(null)
+
+        AndroidView(
+            factory = { context ->
+                val view = KeyguardIndicationArea(context, null)
+                setDisposable(
+                    KeyguardIndicationAreaBinder.bind(
+                        view = view,
+                        viewModel = indicationAreaViewModel,
+                        keyguardRootViewModel = keyguardRootViewModel,
+                        indicationController = indicationController,
+                    )
+                )
+                view
+            },
+            onRelease = { disposable?.dispose() },
+            modifier = modifier.fillMaxWidth(),
+        )
+    }
+}
+
+private val StartButtonElementKey = ElementKey("StartButton")
+private val EndButtonElementKey = ElementKey("EndButton")
+private val IndicationAreaElementKey = ElementKey("IndicationArea")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
new file mode 100644
index 0000000..eaf8063
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.section
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+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.Color
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import javax.inject.Inject
+
+class ClockSection
+@Inject
+constructor(
+    private val viewModel: KeyguardClockViewModel,
+) {
+    @Composable
+    fun SceneScope.SmallClock(modifier: Modifier = Modifier) {
+        if (viewModel.useLargeClock) {
+            return
+        }
+
+        MovableElement(
+            key = ClockElementKey,
+            modifier = modifier,
+        ) {
+            Box(
+                modifier = Modifier.fillMaxWidth().background(Color.Magenta),
+            ) {
+                Text(
+                    text = "TODO(b/316211368): Small clock",
+                    color = Color.White,
+                    modifier = Modifier.align(Alignment.Center),
+                )
+            }
+        }
+    }
+
+    @Composable
+    fun SceneScope.LargeClock(modifier: Modifier = Modifier) {
+        if (!viewModel.useLargeClock) {
+            return
+        }
+
+        MovableElement(
+            key = ClockElementKey,
+            modifier = modifier,
+        ) {
+            Box(
+                modifier = Modifier.fillMaxWidth().background(Color.Blue),
+            ) {
+                Text(
+                    text = "TODO(b/316211368): Large clock",
+                    color = Color.White,
+                    modifier = Modifier.align(Alignment.Center),
+                )
+            }
+        }
+    }
+}
+
+private val ClockElementKey = ElementKey("Clock")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
new file mode 100644
index 0000000..8bbe424b
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.section
+
+import android.content.Context
+import android.graphics.Point
+import android.graphics.Rect
+import android.util.DisplayMetrics
+import android.view.WindowManager
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+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.Color
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class LockSection
+@Inject
+constructor(
+    private val windowManager: WindowManager,
+    private val authController: AuthController,
+    private val featureFlags: FeatureFlagsClassic,
+) {
+    @Composable
+    fun SceneScope.LockIcon(modifier: Modifier = Modifier) {
+        MovableElement(
+            key = LockIconElementKey,
+            modifier = modifier,
+        ) {
+            Box(
+                modifier = Modifier.background(Color.Red),
+            ) {
+                Text(
+                    text = "TODO(b/316211368): Lock",
+                    color = Color.White,
+                    modifier = Modifier.align(Alignment.Center),
+                )
+            }
+        }
+    }
+
+    /**
+     * Returns the bounds of the lock icon, in window view coordinates.
+     *
+     * On devices that support UDFPS (under-display fingerprint sensor), the bounds of the icon are
+     * the same as the bounds of the sensor.
+     */
+    fun lockIconBounds(
+        context: Context,
+    ): Rect {
+        val windowViewBounds = windowManager.currentWindowMetrics.bounds
+        var widthPx = windowViewBounds.right.toFloat()
+        if (featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)) {
+            val insets = windowManager.currentWindowMetrics.windowInsets
+            // Assumed to be initially neglected as there are no left or right insets in portrait.
+            // However, on landscape, these insets need to included when calculating the midpoint.
+            @Suppress("DEPRECATION")
+            widthPx -= (insets.systemWindowInsetLeft + insets.systemWindowInsetRight).toFloat()
+        }
+        val defaultDensity =
+            DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
+                DisplayMetrics.DENSITY_DEFAULT.toFloat()
+        val lockIconRadiusPx = (defaultDensity * 36).toInt()
+
+        val udfpsLocation = authController.udfpsLocation
+        return if (authController.isUdfpsSupported && udfpsLocation != null) {
+            centerLockIcon(udfpsLocation, authController.udfpsRadius)
+        } else {
+            val scaleFactor = authController.scaleFactor
+            val bottomPaddingPx =
+                context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom)
+            val heightPx = windowViewBounds.bottom.toFloat()
+
+            centerLockIcon(
+                Point(
+                    (widthPx / 2).toInt(),
+                    (heightPx - ((bottomPaddingPx + lockIconRadiusPx) * scaleFactor)).toInt()
+                ),
+                lockIconRadiusPx * scaleFactor
+            )
+        }
+    }
+
+    private fun centerLockIcon(
+        center: Point,
+        radius: Float,
+    ): Rect {
+        return Rect().apply {
+            set(
+                center.x - radius.toInt(),
+                center.y - radius.toInt(),
+                center.x + radius.toInt(),
+                center.y + radius.toInt(),
+            )
+        }
+    }
+}
+
+private val LockIconElementKey = ElementKey("LockIcon")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
new file mode 100644
index 0000000..f135be2
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.section
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+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.Color
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import javax.inject.Inject
+
+class NotificationSection @Inject constructor() {
+    @Composable
+    fun SceneScope.Notifications(modifier: Modifier = Modifier) {
+        MovableElement(
+            key = NotificationsElementKey,
+            modifier = modifier,
+        ) {
+            Box(
+                modifier = Modifier.fillMaxSize().background(Color.Yellow),
+            ) {
+                Text(
+                    text = "TODO(b/316211368): Notifications",
+                    color = Color.White,
+                    modifier = Modifier.align(Alignment.Center),
+                )
+            }
+        }
+    }
+}
+
+private val NotificationsElementKey = ElementKey("Notifications")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt
new file mode 100644
index 0000000..ca03af5
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.section
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+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.Color
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import javax.inject.Inject
+
+class SmartSpaceSection @Inject constructor() {
+    @Composable
+    fun SceneScope.SmartSpace(modifier: Modifier = Modifier) {
+        MovableElement(key = SmartSpaceElementKey, modifier = modifier) {
+            Box(
+                modifier = Modifier.fillMaxWidth().background(Color.Cyan),
+            ) {
+                Text(
+                    text = "TODO(b/316211368): Smart space",
+                    color = Color.White,
+                    modifier = Modifier.align(Alignment.Center),
+                )
+            }
+        }
+    }
+}
+
+private val SmartSpaceElementKey = ElementKey("SmartSpace")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt
new file mode 100644
index 0000000..6811eb4
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.section
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.height
+import com.android.keyguard.dagger.KeyguardStatusBarViewComponent
+import com.android.systemui.res.R
+import com.android.systemui.shade.NotificationPanelView
+import com.android.systemui.shade.ShadeViewStateProvider
+import com.android.systemui.statusbar.phone.KeyguardStatusBarView
+import com.android.systemui.util.Utils
+import dagger.Lazy
+import javax.inject.Inject
+
+class StatusBarSection
+@Inject
+constructor(
+    private val componentFactory: KeyguardStatusBarViewComponent.Factory,
+    private val notificationPanelView: Lazy<NotificationPanelView>,
+) {
+    @Composable
+    fun SceneScope.StatusBar(modifier: Modifier = Modifier) {
+        val context = LocalContext.current
+
+        MovableElement(
+            key = StatusBarElementKey,
+            modifier = modifier,
+        ) {
+            AndroidView(
+                factory = {
+                    notificationPanelView.get().findViewById<View>(R.id.keyguard_header)?.let {
+                        (it.parent as ViewGroup).removeView(it)
+                    }
+
+                    val provider =
+                        object : ShadeViewStateProvider {
+                            override val lockscreenShadeDragProgress: Float = 0f
+                            override val panelViewExpandedHeight: Float = 0f
+                            override fun shouldHeadsUpBeVisible(): Boolean {
+                                return false
+                            }
+                        }
+
+                    @SuppressLint("InflateParams")
+                    val view =
+                        LayoutInflater.from(context)
+                            .inflate(
+                                R.layout.keyguard_status_bar,
+                                null,
+                                false,
+                            ) as KeyguardStatusBarView
+                    componentFactory.build(view, provider).keyguardStatusBarViewController.init()
+                    view
+                },
+                modifier =
+                    Modifier.fillMaxWidth().height {
+                        Utils.getStatusBarHeaderHeightKeyguard(context)
+                    },
+            )
+        }
+    }
+}
+
+private val StatusBarElementKey = ElementKey("StatusBar")
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 9d9b0a9..a85d9bf 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -16,6 +16,7 @@
 
 package com.android.compose.animation.scene
 
+import android.graphics.Picture
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
@@ -66,12 +67,21 @@
      * The movable content of this element, if this element is composed using
      * [SceneScope.MovableElement].
      */
-    val movableContent by
-        // This is only accessed from the composition (main) thread, so no need to use the default
-        // lock of lazy {} to synchronize.
-        lazy(mode = LazyThreadSafetyMode.NONE) {
-            movableContentOf { content: @Composable () -> Unit -> content() }
-        }
+    private var _movableContent: (@Composable (@Composable () -> Unit) -> Unit)? = null
+    val movableContent: @Composable (@Composable () -> Unit) -> Unit
+        get() =
+            _movableContent
+                ?: movableContentOf { content: @Composable () -> Unit -> content() }
+                    .also { _movableContent = it }
+
+    /**
+     * The [Picture] to which we save the last drawing commands of this element, if it is movable.
+     * This is necessary because the content of this element might not be composed in the scene it
+     * should currently be drawn.
+     */
+    private var _picture: Picture? = null
+    val picture: Picture
+        get() = _picture ?: Picture().also { _picture = it }
 
     override fun toString(): String {
         return "Element(key=$key)"
@@ -521,11 +531,6 @@
             sceneValues.targetOffset = targetOffsetInScene
         }
 
-        // No need to place the element in this scene if we don't want to draw it anyways.
-        if (!shouldDrawElement(layoutImpl, scene, element)) {
-            return
-        }
-
         val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero)
         val lastSharedValues = element.lastSharedValues
         val lastValues = sceneValues.lastValues
@@ -548,6 +553,13 @@
         lastSharedValues.offset = targetOffset
         lastValues.offset = targetOffset
 
+        // No need to place the element in this scene if we don't want to draw it anyways. Note that
+        // it's still important to compute the target offset and update lastValues, otherwise it
+        // will be out of date.
+        if (!shouldDrawElement(layoutImpl, scene, element)) {
+            return
+        }
+
         val offset = (targetOffset - currentOffset).round()
         if (isElementOpaque(layoutImpl, element, scene, sceneValues)) {
             // TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is not
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
index 306f276..49df2f6 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
@@ -16,7 +16,6 @@
 
 package com.android.compose.animation.scene
 
-import android.graphics.Picture
 import android.util.Log
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
@@ -60,7 +59,7 @@
         // The [Picture] to which we save the last drawing commands of this element. This is
         // necessary because the content of this element might not be composed in this scene, in
         // which case we still need to draw it.
-        val picture = remember { Picture() }
+        val picture = element.picture
 
         // Whether we should compose the movable element here. The scene picker logic to know in
         // which scene we should compose/draw a movable element might depend on the current
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 6a7a3a0..30e50a9 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -34,6 +34,7 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.zIndex
+import com.android.compose.animation.scene.modifiers.noResizeDuringTransitions
 
 /** A scene in a [SceneTransitionLayout]. */
 @Stable
@@ -152,4 +153,8 @@
         bounds: ElementKey,
         shape: Shape
     ): Modifier = punchHole(layoutImpl, element, bounds, shape)
+
+    override fun Modifier.noResizeDuringTransitions(): Modifier {
+        return noResizeDuringTransitions(layoutState = layoutImpl.state)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 3608e37..5eb339e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -65,11 +65,11 @@
     SceneTransitionLayoutForTesting(
         currentScene,
         onChangeScene,
+        modifier,
         transitions,
         state,
         edgeDetector,
         transitionInterceptionThreshold,
-        modifier,
         onLayoutImpl = null,
         scenes,
     )
@@ -205,6 +205,12 @@
      * the result.
      */
     fun Modifier.punchHole(element: ElementKey, bounds: ElementKey, shape: Shape): Modifier
+
+    /**
+     * Don't resize during transitions. This can for instance be used to make sure that scrollable
+     * lists keep a constant size during transitions even if its elements are growing/shrinking.
+     */
+    fun Modifier.noResizeDuringTransitions(): Modifier
 }
 
 // TODO(b/291053742): Add animateSharedValueAsState(targetValue) without any ValueKey and ElementKey
@@ -257,12 +263,12 @@
 internal fun SceneTransitionLayoutForTesting(
     currentScene: SceneKey,
     onChangeScene: (SceneKey) -> Unit,
-    transitions: SceneTransitions,
-    state: SceneTransitionLayoutState,
-    edgeDetector: EdgeDetector,
-    transitionInterceptionThreshold: Float,
-    modifier: Modifier,
-    onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)?,
+    modifier: Modifier = Modifier,
+    transitions: SceneTransitions = transitions {},
+    state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
+    edgeDetector: EdgeDetector = DefaultEdgeDetector,
+    transitionInterceptionThreshold: Float = 0f,
+    onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
     scenes: SceneTransitionLayoutScope.() -> Unit,
 ) {
     val density = LocalDensity.current
@@ -280,6 +286,10 @@
             .also { onLayoutImpl?.invoke(it) }
     }
 
+    // TODO(b/317014852): Move this into the SideEffect {} again once STLImpl.scenes is not a
+    // SnapshotStateMap anymore.
+    layoutImpl.updateScenes(scenes)
+
     val targetSceneChannel = remember { Channel<SceneKey>(Channel.CONFLATED) }
     SideEffect {
         if (state != layoutImpl.state) {
@@ -293,7 +303,6 @@
         (state as SceneTransitionLayoutStateImpl).transitions = transitions
         layoutImpl.density = density
         layoutImpl.edgeDetector = edgeDetector
-        layoutImpl.updateScenes(scenes)
 
         state.transitions = transitions
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index c99c325..45e1a0f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -46,7 +46,12 @@
     builder: SceneTransitionLayoutScope.() -> Unit,
     coroutineScope: CoroutineScope,
 ) {
-    internal val scenes = mutableMapOf<SceneKey, Scene>()
+    /**
+     * The map of [Scene]s.
+     *
+     * TODO(b/317014852): Make this a normal MutableMap instead.
+     */
+    internal val scenes = SnapshotStateMap<SceneKey, Scene>()
 
     /**
      * The map of [Element]s.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt
new file mode 100644
index 0000000..bd36cb8
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.modifiers
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.unit.Constraints
+import com.android.compose.animation.scene.SceneTransitionLayoutState
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun Modifier.noResizeDuringTransitions(layoutState: SceneTransitionLayoutState): Modifier {
+    return intermediateLayout { measurable, constraints ->
+        if (layoutState.currentTransition == null) {
+            return@intermediateLayout measurable.measure(constraints).run {
+                layout(width, height) { place(0, 0) }
+            }
+        }
+
+        // Make sure that this layout node has the same size than when we are at rest.
+        val sizeAtRest = lookaheadSize
+        measurable.measure(Constraints.fixed(sizeAtRest.width, sizeAtRest.height)).run {
+            layout(width, height) { place(0, 0) }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index d332910..da5a0a0 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.compose.animation.scene
 
+import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.Box
@@ -30,16 +31,21 @@
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.test.subjects.DpOffsetSubject
+import com.android.compose.test.subjects.assertThat
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
@@ -259,11 +265,6 @@
             SceneTransitionLayoutForTesting(
                 currentScene = currentScene,
                 onChangeScene = { currentScene = it },
-                transitions = remember { transitions {} },
-                state = remember { SceneTransitionLayoutState(currentScene) },
-                edgeDetector = DefaultEdgeDetector,
-                modifier = Modifier,
-                transitionInterceptionThreshold = 0f,
                 onLayoutImpl = { nullableLayoutImpl = it },
             ) {
                 scene(TestScenes.SceneA) { /* Nothing */}
@@ -429,11 +430,6 @@
             SceneTransitionLayoutForTesting(
                 currentScene = TestScenes.SceneA,
                 onChangeScene = {},
-                transitions = remember { transitions {} },
-                state = remember { SceneTransitionLayoutState(TestScenes.SceneA) },
-                edgeDetector = DefaultEdgeDetector,
-                modifier = Modifier,
-                transitionInterceptionThreshold = 0f,
                 onLayoutImpl = { nullableLayoutImpl = it },
             ) {
                 scene(TestScenes.SceneA) { Box(Modifier.element(key)) }
@@ -484,11 +480,6 @@
             SceneTransitionLayoutForTesting(
                 currentScene = TestScenes.SceneA,
                 onChangeScene = {},
-                transitions = remember { transitions {} },
-                state = remember { SceneTransitionLayoutState(TestScenes.SceneA) },
-                edgeDetector = DefaultEdgeDetector,
-                modifier = Modifier,
-                transitionInterceptionThreshold = 0f,
                 onLayoutImpl = { nullableLayoutImpl = it },
             ) {
                 scene(TestScenes.SceneA) {
@@ -574,4 +565,86 @@
             after { assertThat(fooCompositions).isEqualTo(1) }
         }
     }
+
+    @Test
+    fun sharedElementOffsetIsUpdatedEvenWhenNotPlaced() {
+        var nullableLayoutImpl: SceneTransitionLayoutImpl? = null
+        var density: Density? = null
+
+        fun layoutImpl() = nullableLayoutImpl ?: error("nullableLayoutImpl was not set")
+
+        fun density() = density ?: error("density was not set")
+
+        fun Offset.toDpOffset() = with(density()) { DpOffset(x.toDp(), y.toDp()) }
+
+        fun foo() = layoutImpl().elements[TestElements.Foo] ?: error("Foo not in elements map")
+
+        fun Element.lastSharedOffset() = lastSharedValues.offset.toDpOffset()
+
+        fun Element.lastOffsetIn(scene: SceneKey) =
+            (sceneValues[scene] ?: error("$scene not in sceneValues map"))
+                .lastValues
+                .offset
+                .toDpOffset()
+
+        rule.testTransition(
+            from = TestScenes.SceneA,
+            to = TestScenes.SceneB,
+            transitionLayout = { currentScene, onChangeScene ->
+                density = LocalDensity.current
+
+                SceneTransitionLayoutForTesting(
+                    currentScene = currentScene,
+                    onChangeScene = onChangeScene,
+                    onLayoutImpl = { nullableLayoutImpl = it },
+                    transitions =
+                        transitions {
+                            from(TestScenes.SceneA, to = TestScenes.SceneB) {
+                                spec = tween(durationMillis = 4 * 16, easing = LinearEasing)
+                            }
+                        }
+                ) {
+                    scene(TestScenes.SceneA) { Box(Modifier.element(TestElements.Foo)) }
+                    scene(TestScenes.SceneB) {
+                        Box(Modifier.offset(x = 40.dp, y = 80.dp).element(TestElements.Foo))
+                    }
+                }
+            }
+        ) {
+            val tolerance = DpOffsetSubject.DefaultTolerance
+
+            before {
+                val expected = DpOffset(0.dp, 0.dp)
+                assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneA)).isWithin(tolerance).of(expected)
+            }
+
+            at(16) {
+                val expected = DpOffset(10.dp, 20.dp)
+                assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneA)).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneB)).isWithin(tolerance).of(expected)
+            }
+
+            at(32) {
+                val expected = DpOffset(20.dp, 40.dp)
+                assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneA)).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneB)).isWithin(tolerance).of(expected)
+            }
+
+            at(48) {
+                val expected = DpOffset(30.dp, 60.dp)
+                assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneA)).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneB)).isWithin(tolerance).of(expected)
+            }
+
+            after {
+                val expected = DpOffset(40.dp, 80.dp)
+                assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneB)).isWithin(tolerance).of(expected)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/modifiers/SizeTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/modifiers/SizeTest.kt
new file mode 100644
index 0000000..2c159d1
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/modifiers/SizeTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.modifiers
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.element
+import com.android.compose.animation.scene.testTransition
+import com.android.compose.test.assertSizeIsEqualTo
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SizeTest {
+    @get:Rule val rule = createComposeRule()
+
+    @Test
+    fun noResizeDuringTransitions() {
+        // The tag for the parent of the shared Foo element.
+        val parentTag = "parent"
+
+        rule.testTransition(
+            fromSceneContent = { Box(Modifier.element(TestElements.Foo).size(100.dp)) },
+            toSceneContent = {
+                // Don't resize the parent of Foo during transitions so that it's always the same
+                // size as when there is no transition (200dp).
+                Box(Modifier.noResizeDuringTransitions().testTag(parentTag)) {
+                    Box(Modifier.element(TestElements.Foo).size(200.dp))
+                }
+            },
+            transition = { spec = tween(durationMillis = 4 * 16, easing = LinearEasing) },
+        ) {
+            at(16) { rule.onNodeWithTag(parentTag).assertSizeIsEqualTo(200.dp, 200.dp) }
+            at(32) { rule.onNodeWithTag(parentTag).assertSizeIsEqualTo(200.dp, 200.dp) }
+            at(48) { rule.onNodeWithTag(parentTag).assertSizeIsEqualTo(200.dp, 200.dp) }
+            after { rule.onNodeWithTag(parentTag).assertSizeIsEqualTo(200.dp, 200.dp) }
+        }
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
index 0b7c3f9..26da1f0 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
@@ -17,56 +17,39 @@
 package com.android.systemui.shared.notifications.data.repository
 
 import android.provider.Settings
-import com.android.systemui.shared.notifications.shared.model.NotificationSettingsModel
 import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withContext
 
-/** Provides access to state related to notifications. */
+/** Provides access to state related to notification settings. */
 class NotificationSettingsRepository(
     scope: CoroutineScope,
     private val backgroundDispatcher: CoroutineDispatcher,
     private val secureSettingsRepository: SecureSettingsRepository,
 ) {
     /** The current state of the notification setting. */
-    val settings: SharedFlow<NotificationSettingsModel> =
+    val isShowNotificationsOnLockScreenEnabled: StateFlow<Boolean> =
         secureSettingsRepository
             .intSetting(
                 name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
             )
-            .map { lockScreenShowNotificationsInt ->
-                NotificationSettingsModel(
-                    isShowNotificationsOnLockScreenEnabled = lockScreenShowNotificationsInt == 1,
-                )
-            }
-            .shareIn(
+            .map { it == 1 }
+            .stateIn(
                 scope = scope,
                 started = SharingStarted.WhileSubscribed(),
-                replay = 1,
+                initialValue = false,
             )
 
-    suspend fun getSettings(): NotificationSettingsModel {
-        return withContext(backgroundDispatcher) {
-            NotificationSettingsModel(
-                isShowNotificationsOnLockScreenEnabled =
-                    secureSettingsRepository.get(
-                        name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
-                        defaultValue = 0,
-                    ) == 1
-            )
-        }
-    }
-
-    suspend fun setSettings(model: NotificationSettingsModel) {
+    suspend fun setShowNotificationsOnLockscreenEnabled(enabled: Boolean) {
         withContext(backgroundDispatcher) {
             secureSettingsRepository.set(
                 name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
-                value = if (model.isShowNotificationsOnLockScreenEnabled) 1 else 0,
+                value = if (enabled) 1 else 0,
             )
         }
     }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
index 21f3aca..9ec6ec8 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
@@ -17,32 +17,23 @@
 package com.android.systemui.shared.notifications.domain.interactor
 
 import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
-import com.android.systemui.shared.notifications.shared.model.NotificationSettingsModel
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 
 /** Encapsulates business logic for interacting with notification settings. */
 class NotificationSettingsInteractor(
     private val repository: NotificationSettingsRepository,
 ) {
-    /** The current state of the notification setting. */
-    val settings: Flow<NotificationSettingsModel> = repository.settings
+    /** Should notifications be visible on the lockscreen? */
+    val isShowNotificationsOnLockScreenEnabled: StateFlow<Boolean> =
+        repository.isShowNotificationsOnLockScreenEnabled
+
+    suspend fun setShowNotificationsOnLockscreenEnabled(enabled: Boolean) {
+        repository.setShowNotificationsOnLockscreenEnabled(enabled)
+    }
 
     /** Toggles the setting to show or hide notifications on the lock screen. */
-    suspend fun toggleShowNotificationsOnLockScreenEnabled() {
-        val currentModel = repository.getSettings()
-        setSettings(
-            currentModel.copy(
-                isShowNotificationsOnLockScreenEnabled =
-                    !currentModel.isShowNotificationsOnLockScreenEnabled,
-            )
-        )
-    }
-
-    suspend fun setSettings(model: NotificationSettingsModel) {
-        repository.setSettings(model)
-    }
-
-    suspend fun getSettings(): NotificationSettingsModel {
-        return repository.getSettings()
+    suspend fun toggleShowNotificationsOnLockscreenEnabled() {
+        val current = repository.isShowNotificationsOnLockScreenEnabled.value
+        repository.setShowNotificationsOnLockscreenEnabled(!current)
     }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/shared/model/NotificationSettingsModel.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/shared/model/NotificationSettingsModel.kt
deleted file mode 100644
index 7e35360..0000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/shared/model/NotificationSettingsModel.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.shared.notifications.shared.model
-
-/** Models notification settings. */
-data class NotificationSettingsModel(
-    /** Whether notifications are shown on the lock screen. */
-    val isShowNotificationsOnLockScreenEnabled: Boolean = false,
-)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index dddcf18..a4b55e7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -90,7 +90,6 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -124,8 +123,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import javax.inject.Provider;
-
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 @RunWithLooper(setAsMainLooper = true)
@@ -216,8 +213,6 @@
     @Mock
     private UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
     @Mock
-    private Provider<UdfpsKeyguardViewModels> mUdfpsKeyguardViewModels;
-    @Mock
     private SelectedUserInteractor mSelectedUserInteractor;
 
     // Capture listeners so that they can be used to send events
@@ -339,7 +334,6 @@
                 mInputManager,
                 mock(KeyguardFaceAuthInteractor.class),
                 mUdfpsKeyguardAccessibilityDelegate,
-                mUdfpsKeyguardViewModels,
                 mSelectedUserInteractor,
                 mFpsUnlockTracker,
                 mKeyguardTransitionInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
index 0ab596c..1f8854f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
@@ -616,4 +616,28 @@
                 .onDozeAmountChanged(eq(.3f), eq(.3f), eq(ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN))
             job.cancel()
         }
+
+    @Test
+    fun bouncerToAod_dozeAmountChanged() =
+        testScope.runTest {
+            // GIVEN view is attached
+            mController.onViewAttached()
+            Mockito.reset(mView)
+
+            val job = mController.listenForPrimaryBouncerToAodTransitions(this)
+            // WHEN alternate bouncer to aod transition in progress
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.AOD,
+                    value = .3f,
+                    transitionState = TransitionState.RUNNING
+                )
+            )
+            runCurrent()
+
+            // THEN doze amount is updated to
+            verify(mView).onDozeAmountChanged(eq(.3f), eq(.3f), eq(ANIMATE_APPEAR_ON_SCREEN_OFF))
+            job.cancel()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
index cf076c5..a84b9fa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
@@ -24,16 +24,19 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.qs.external.FakeCustomTileStatePersister
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.external.TileServiceKey
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tiles.impl.custom.TileSubject.Companion.assertThat
 import com.android.systemui.qs.tiles.impl.custom.commons.copy
+import com.android.systemui.qs.tiles.impl.custom.customTileStatePersister
 import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
+import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade
+import com.android.systemui.qs.tiles.impl.custom.tileSpec
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -44,194 +47,260 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class CustomTileRepositoryTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-
-    private val persister = FakeCustomTileStatePersister()
-
+    private val kosmos = Kosmos().apply { tileSpec = TileSpec.create(TEST_COMPONENT) }
     private val underTest: CustomTileRepository =
-        CustomTileRepositoryImpl(
-            TileSpec.create(TEST_COMPONENT),
-            persister,
-            testScope.testScheduler,
-        )
+        with(kosmos) {
+            CustomTileRepositoryImpl(
+                tileSpec,
+                customTileStatePersister,
+                packageManagerAdapterFacade.packageManagerAdapter,
+                testScope.testScheduler,
+            )
+        }
 
     @Test
     fun persistableTileIsRestoredForUser() =
-        testScope.runTest {
-            persister.persistState(TEST_TILE_KEY_1, TEST_TILE_1)
-            persister.persistState(TEST_TILE_KEY_2, TEST_TILE_2)
+        with(kosmos) {
+            testScope.runTest {
+                customTileStatePersister.persistState(TEST_TILE_KEY_1, TEST_TILE_1)
+                customTileStatePersister.persistState(TEST_TILE_KEY_2, TEST_TILE_2)
 
-            underTest.restoreForTheUserIfNeeded(TEST_USER_1, true)
-            runCurrent()
+                underTest.restoreForTheUserIfNeeded(TEST_USER_1, true)
+                runCurrent()
 
-            assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
-            assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
+            }
         }
 
     @Test
     fun notPersistableTileIsNotRestored() =
-        testScope.runTest {
-            persister.persistState(TEST_TILE_KEY_1, TEST_TILE_1)
-            val tiles = collectValues(underTest.getTiles(TEST_USER_1))
+        with(kosmos) {
+            testScope.runTest {
+                customTileStatePersister.persistState(TEST_TILE_KEY_1, TEST_TILE_1)
+                val tiles = collectValues(underTest.getTiles(TEST_USER_1))
 
-            underTest.restoreForTheUserIfNeeded(TEST_USER_1, false)
-            runCurrent()
+                underTest.restoreForTheUserIfNeeded(TEST_USER_1, false)
+                runCurrent()
 
-            assertThat(tiles()).isEmpty()
+                assertThat(tiles()).isEmpty()
+            }
         }
 
     @Test
     fun emptyPersistedStateIsHandled() =
-        testScope.runTest {
-            val tiles = collectValues(underTest.getTiles(TEST_USER_1))
+        with(kosmos) {
+            testScope.runTest {
+                val tiles = collectValues(underTest.getTiles(TEST_USER_1))
 
-            underTest.restoreForTheUserIfNeeded(TEST_USER_1, true)
-            runCurrent()
+                underTest.restoreForTheUserIfNeeded(TEST_USER_1, true)
+                runCurrent()
 
-            assertThat(tiles()).isEmpty()
+                assertThat(tiles()).isEmpty()
+            }
         }
 
     @Test
     fun updatingWithPersistableTilePersists() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+                runCurrent()
 
-            assertThat(persister.readState(TEST_TILE_KEY_1)).isEqualTo(TEST_TILE_1)
+                assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1))
+                    .isEqualTo(TEST_TILE_1)
+            }
         }
 
     @Test
     fun updatingWithNotPersistableTileDoesntPersist() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, false)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, false)
+                runCurrent()
 
-            assertThat(persister.readState(TEST_TILE_KEY_1)).isNull()
+                assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1)).isNull()
+            }
         }
 
     @Test
     fun updateWithTileEmits() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+                runCurrent()
 
-            assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
-            assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+            }
         }
 
     @Test
     fun updatingPeristableWithDefaultsPersists() =
-        testScope.runTest {
-            underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
+                runCurrent()
 
-            assertThat(persister.readState(TEST_TILE_KEY_1)).isEqualTo(TEST_TILE_1)
+                assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1))
+                    .isEqualTo(TEST_TILE_1)
+            }
         }
 
     @Test
     fun updatingNotPersistableWithDefaultsDoesntPersist() =
-        testScope.runTest {
-            underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, false)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, false)
+                runCurrent()
 
-            assertThat(persister.readState(TEST_TILE_KEY_1)).isNull()
+                assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1)).isNull()
+            }
         }
 
     @Test
     fun updatingPeristableWithErrorDefaultsDoesntPersist() =
-        testScope.runTest {
-            underTest.updateWithDefaults(TEST_USER_1, CustomTileDefaults.Error, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithDefaults(TEST_USER_1, CustomTileDefaults.Error, true)
+                runCurrent()
 
-            assertThat(persister.readState(TEST_TILE_KEY_1)).isNull()
+                assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1)).isNull()
+            }
         }
 
     @Test
     fun updateWithDefaultsEmits() =
-        testScope.runTest {
-            underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
+                runCurrent()
 
-            assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
-            assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+            }
         }
 
     @Test
     fun getTileForAnotherUserReturnsNull() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+                runCurrent()
 
-            assertThat(underTest.getTile(TEST_USER_2)).isNull()
+                assertThat(underTest.getTile(TEST_USER_2)).isNull()
+            }
         }
 
     @Test
     fun getTilesForAnotherUserEmpty() =
-        testScope.runTest {
-            val tiles = collectValues(underTest.getTiles(TEST_USER_2))
+        with(kosmos) {
+            testScope.runTest {
+                val tiles = collectValues(underTest.getTiles(TEST_USER_2))
 
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
-            runCurrent()
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+                runCurrent()
 
-            assertThat(tiles()).isEmpty()
+                assertThat(tiles()).isEmpty()
+            }
         }
 
     @Test
     fun updatingWithTileForTheSameUserAddsData() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+                runCurrent()
 
-            underTest.updateWithTile(TEST_USER_1, Tile().apply { subtitle = "test_subtitle" }, true)
-            runCurrent()
+                underTest.updateWithTile(
+                    TEST_USER_1,
+                    Tile().apply { subtitle = "test_subtitle" },
+                    true
+                )
+                runCurrent()
 
-            val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" }
-            assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile)
-            assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile)
+                val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" }
+                assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile)
+                assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile)
+            }
         }
 
     @Test
     fun updatingWithTileForAnotherUserOverridesTile() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+                runCurrent()
 
-            val tiles = collectValues(underTest.getTiles(TEST_USER_2))
-            underTest.updateWithTile(TEST_USER_2, TEST_TILE_2, true)
-            runCurrent()
+                val tiles = collectValues(underTest.getTiles(TEST_USER_2))
+                underTest.updateWithTile(TEST_USER_2, TEST_TILE_2, true)
+                runCurrent()
 
-            assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2)
-            assertThat(tiles()).hasSize(1)
-            assertThat(tiles().last()).isEqualTo(TEST_TILE_2)
+                assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2)
+                assertThat(tiles()).hasSize(1)
+                assertThat(tiles().last()).isEqualTo(TEST_TILE_2)
+            }
         }
 
     @Test
     fun updatingWithDefaultsForTheSameUserAddsData() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, Tile().apply { subtitle = "test_subtitle" }, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(
+                    TEST_USER_1,
+                    Tile().apply { subtitle = "test_subtitle" },
+                    true
+                )
+                runCurrent()
 
-            underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
-            runCurrent()
+                underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
+                runCurrent()
 
-            val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" }
-            assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile)
-            assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile)
+                val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" }
+                assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile)
+                assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile)
+            }
         }
 
     @Test
     fun updatingWithDefaultsForAnotherUserOverridesTile() =
-        testScope.runTest {
-            underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
+                runCurrent()
 
-            val tiles = collectValues(underTest.getTiles(TEST_USER_2))
-            underTest.updateWithDefaults(TEST_USER_2, TEST_DEFAULTS_2, true)
-            runCurrent()
+                val tiles = collectValues(underTest.getTiles(TEST_USER_2))
+                underTest.updateWithDefaults(TEST_USER_2, TEST_DEFAULTS_2, true)
+                runCurrent()
 
-            assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2)
-            assertThat(tiles()).hasSize(1)
-            assertThat(tiles().last()).isEqualTo(TEST_TILE_2)
+                assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2)
+                assertThat(tiles()).hasSize(1)
+                assertThat(tiles().last()).isEqualTo(TEST_TILE_2)
+            }
+        }
+
+    @Test
+    fun isActiveFollowsPackageManagerAdapter() =
+        with(kosmos) {
+            testScope.runTest {
+                packageManagerAdapterFacade.setIsActive(false)
+                assertThat(underTest.isTileActive()).isFalse()
+
+                packageManagerAdapterFacade.setIsActive(true)
+                assertThat(underTest.isTileActive()).isTrue()
+            }
+        }
+
+    @Test
+    fun isToggleableFollowsPackageManagerAdapter() =
+        with(kosmos) {
+            testScope.runTest {
+                packageManagerAdapterFacade.setIsToggleable(false)
+                assertThat(underTest.isTileToggleable()).isFalse()
+
+                packageManagerAdapterFacade.setIsToggleable(true)
+                assertThat(underTest.isTileToggleable()).isTrue()
+            }
         }
 
     private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
index eebb145..90779cb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
@@ -25,157 +25,159 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.qs.external.FakeCustomTileStatePersister
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.external.TileServiceKey
-import com.android.systemui.qs.external.TileServiceManager
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tiles.impl.custom.TileSubject.Companion.assertThat
+import com.android.systemui.qs.tiles.impl.custom.customTileDefaultsRepository
+import com.android.systemui.qs.tiles.impl.custom.customTileRepository
+import com.android.systemui.qs.tiles.impl.custom.customTileStatePersister
 import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
-import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileDefaultsRepository
-import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileRepository
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.qs.tiles.impl.custom.tileSpec
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @OptIn(ExperimentalCoroutinesApi::class)
 class CustomTileInteractorTest : SysuiTestCase() {
 
-    @Mock private lateinit var tileServiceManager: TileServiceManager
+    private val kosmos = Kosmos().apply { tileSpec = TileSpec.create(TEST_COMPONENT) }
 
-    private val testScope = TestScope()
-
-    private val defaultsRepository = FakeCustomTileDefaultsRepository()
-    private val customTileStatePersister = FakeCustomTileStatePersister()
-    private val customTileRepository =
-        FakeCustomTileRepository(
-            TEST_TILE_SPEC,
-            customTileStatePersister,
-            testScope.testScheduler,
-        )
-
-    private lateinit var underTest: CustomTileInteractor
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-
-        underTest =
+    private val underTest: CustomTileInteractor =
+        with(kosmos) {
             CustomTileInteractor(
-                TEST_USER,
-                defaultsRepository,
+                customTileDefaultsRepository,
                 customTileRepository,
-                tileServiceManager,
                 testScope.backgroundScope,
                 testScope.testScheduler,
             )
-    }
+        }
 
     @Test
     fun activeTileIsAvailableAfterRestored() =
-        testScope.runTest {
-            whenever(tileServiceManager.isActiveTile).thenReturn(true)
-            customTileStatePersister.persistState(
-                TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
-                TEST_TILE,
-            )
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(true)
+                customTileStatePersister.persistState(
+                    TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
+                    TEST_TILE,
+                )
 
-            underTest.init()
+                underTest.initForUser(TEST_USER)
 
-            assertThat(underTest.tile).isEqualTo(TEST_TILE)
-            assertThat(underTest.tiles.first()).isEqualTo(TEST_TILE)
+                assertThat(underTest.getTile(TEST_USER)).isEqualTo(TEST_TILE)
+                assertThat(underTest.getTiles(TEST_USER).first()).isEqualTo(TEST_TILE)
+            }
         }
 
     @Test
     fun notActiveTileIsAvailableAfterUpdated() =
-        testScope.runTest {
-            whenever(tileServiceManager.isActiveTile).thenReturn(false)
-            customTileStatePersister.persistState(
-                TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
-                TEST_TILE,
-            )
-            val tiles = collectValues(underTest.tiles)
-            val initJob = launch { underTest.init() }
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(false)
+                customTileStatePersister.persistState(
+                    TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
+                    TEST_TILE,
+                )
+                val tiles = collectValues(underTest.getTiles(TEST_USER))
+                val initJob = launch { underTest.initForUser(TEST_USER) }
 
-            underTest.updateTile(TEST_TILE)
-            runCurrent()
-            initJob.join()
+                underTest.updateTile(TEST_TILE)
+                runCurrent()
+                initJob.join()
 
-            assertThat(tiles()).hasSize(1)
-            assertThat(tiles().last()).isEqualTo(TEST_TILE)
+                assertThat(tiles()).hasSize(1)
+                assertThat(tiles().last()).isEqualTo(TEST_TILE)
+            }
         }
 
     @Test
     fun notActiveTileIsAvailableAfterDefaultsUpdated() =
-        testScope.runTest {
-            whenever(tileServiceManager.isActiveTile).thenReturn(false)
-            customTileStatePersister.persistState(
-                TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
-                TEST_TILE,
-            )
-            val tiles = collectValues(underTest.tiles)
-            val initJob = launch { underTest.init() }
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(false)
+                customTileStatePersister.persistState(
+                    TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
+                    TEST_TILE,
+                )
+                val tiles = collectValues(underTest.getTiles(TEST_USER))
+                val initJob = launch { underTest.initForUser(TEST_USER) }
 
-            defaultsRepository.putDefaults(TEST_USER, TEST_COMPONENT, TEST_DEFAULTS)
-            defaultsRepository.requestNewDefaults(TEST_USER, TEST_COMPONENT)
-            runCurrent()
-            initJob.join()
+                customTileDefaultsRepository.putDefaults(TEST_USER, TEST_COMPONENT, TEST_DEFAULTS)
+                customTileDefaultsRepository.requestNewDefaults(TEST_USER, TEST_COMPONENT)
+                runCurrent()
+                initJob.join()
 
-            assertThat(tiles()).hasSize(1)
-            assertThat(tiles().last()).isEqualTo(TEST_TILE)
+                assertThat(tiles()).hasSize(1)
+                assertThat(tiles().last()).isEqualTo(TEST_TILE)
+            }
         }
 
     @Test(expected = IllegalStateException::class)
-    fun getTileBeforeInitThrows() = testScope.runTest { underTest.tile }
+    fun getTileBeforeInitThrows() =
+        with(kosmos) { testScope.runTest { underTest.getTile(TEST_USER) } }
 
     @Test
     fun initSuspendsForActiveTileNotRestoredAndNotUpdated() =
-        testScope.runTest {
-            whenever(tileServiceManager.isActiveTile).thenReturn(true)
-            val tiles = collectValues(underTest.tiles)
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(true)
+                val tiles = collectValues(underTest.getTiles(TEST_USER))
 
-            val initJob = backgroundScope.launch { underTest.init() }
-            advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS)
+                val initJob = backgroundScope.launch { underTest.initForUser(TEST_USER) }
+                advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS)
 
-            // Is still suspended
-            assertThat(initJob.isActive).isTrue()
-            assertThat(tiles()).isEmpty()
+                // Is still suspended
+                assertThat(initJob.isActive).isTrue()
+                assertThat(tiles()).isEmpty()
+            }
         }
 
     @Test
     fun initSuspendedForNotActiveTileWithoutUpdates() =
-        testScope.runTest {
-            whenever(tileServiceManager.isActiveTile).thenReturn(false)
-            customTileStatePersister.persistState(
-                TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
-                TEST_TILE,
-            )
-            val tiles = collectValues(underTest.tiles)
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(false)
+                customTileStatePersister.persistState(
+                    TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
+                    TEST_TILE,
+                )
+                val tiles = collectValues(underTest.getTiles(TEST_USER))
 
-            val initJob = backgroundScope.launch { underTest.init() }
-            advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS)
+                val initJob = backgroundScope.launch { underTest.initForUser(TEST_USER) }
+                advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS)
 
-            // Is still suspended
-            assertThat(initJob.isActive).isTrue()
-            assertThat(tiles()).isEmpty()
+                // Is still suspended
+                assertThat(initJob.isActive).isTrue()
+                assertThat(tiles()).isEmpty()
+            }
         }
 
+    @Test
+    fun toggleableFollowsTheRepository() {
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileToggleable(false)
+                assertThat(underTest.isTileToggleable()).isFalse()
+
+                customTileRepository.setTileToggleable(true)
+                assertThat(underTest.isTileToggleable()).isTrue()
+            }
+        }
+    }
+
     private companion object {
 
         val TEST_COMPONENT = ComponentName("test.pkg", "test.cls")
-        val TEST_TILE_SPEC = TileSpec.create(TEST_COMPONENT)
         val TEST_USER = UserHandle.of(1)!!
         val TEST_TILE =
             Tile().apply {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 2ecf01f..1237347 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -16,7 +16,8 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
 
-import android.platform.test.flag.junit.SetFlagsRule
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION
@@ -60,8 +61,6 @@
 
     private lateinit var underTest: WifiViewModel
 
-    private val setFlagsRule = SetFlagsRule()
-
     @Mock private lateinit var tableLogBuffer: TableLogBuffer
     @Mock private lateinit var connectivityConstants: ConnectivityConstants
     @Mock private lateinit var wifiConstants: WifiConstants
@@ -187,11 +186,9 @@
         }
 
     @Test
+    @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
     fun activity_nullSsid_outputsFalse_staticFlagOff() =
         testScope.runTest {
-            // GIVEN flag is disabled
-            setFlagsRule.disableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
-
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
 
@@ -214,11 +211,9 @@
         }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
     fun activity_nullSsid_outputsFalse_staticFlagOn() =
         testScope.runTest {
-            // GIVEN flag is enabled
-            setFlagsRule.enableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
-
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
 
@@ -371,11 +366,9 @@
         }
 
     @Test
+    @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
     fun activityContainer_inAndOutFalse_outputsTrue_staticFlagOff() =
         testScope.runTest {
-            // GIVEN the flag is off
-            setFlagsRule.disableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
-
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -389,11 +382,9 @@
         }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
     fun activityContainer_inAndOutFalse_outputsTrue_staticFlagOn() =
         testScope.runTest {
-            // GIVEN the flag is on
-            setFlagsRule.enableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
-
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
             wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
diff --git a/packages/SystemUI/res/drawable/ic_memory.xml b/packages/SystemUI/res/drawable/ic_memory.xml
deleted file mode 100644
index ada36c5..0000000
--- a/packages/SystemUI/res/drawable/ic_memory.xml
+++ /dev/null
@@ -1,28 +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.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
-    <path
-        android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z"
-        android:fillAlpha="0.5"
-        android:fillColor="#000000"/>
-    <path
-        android:pathData="M6,9 L6,7 L4,7 L4,5 L6,5 C6,3.9 6.9,3 8,3 L16,3 C17.1,3 18,3.9 18,5 L20,5 L20,7 L18,7 L18,9 L20,9 L20,11 L18,11 L18,13 L20,13 L20,15 L18,15 L18,17 L20,17 L20,19 L18,19 C18,20.1 17.1,21 16,21 L8,21 C6.9,21 6,20.1 6,19 L4,19 L4,17 L6,17 L6,15 L4,15 L4,13 L6,13 L6,11 L4,11 L4,9 L6,9 Z M16,19 L16,5 L8,5 L8,19 L16,19 Z"
-        android:fillColor="#000000"/>
-</vector>
diff --git a/packages/SystemUI/res/layout/widget_picker.xml b/packages/SystemUI/res/layout/widget_picker.xml
deleted file mode 100644
index 21dc224..0000000
--- a/packages/SystemUI/res/layout/widget_picker.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<HorizontalScrollView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <LinearLayout
-        android:id="@+id/widgets_container"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:gravity="center_vertical"
-        android:orientation="horizontal">
-    </LinearLayout>
-
-</HorizontalScrollView>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index deed6c8..e01a2aa 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -480,9 +480,6 @@
      This name is in the ComponentName flattened format (package/class)  -->
     <string name="config_remoteCopyPackage" translatable="false"></string>
 
-    <!-- On debuggable builds, alert the user if SystemUI PSS goes over this number (in kb) -->
-    <integer name="watch_heap_limit">256000</integer>
-
     <!-- SystemUI Plugins that can be loaded on user builds. -->
     <string-array name="config_pluginAllowlist" translatable="false">
         <item>com.android.systemui</item>
@@ -966,14 +963,6 @@
     <bool name="config_edgeToEdgeBottomSheetDialog">true</bool>
 
     <!--
-    Whether the scene container framework is enabled.
-
-    The scene container framework is a newer (2023) way to organize the various "scenes" between the
-    bouncer, lockscreen, shade, and quick settings.
-    -->
-    <bool name="config_sceneContainerFrameworkEnabled">true</bool>
-
-    <!--
     Time in milliseconds the user has to touch the side FPS sensor to successfully authenticate
     TODO(b/302332976) Get this value from the HAL if they can provide an API for it.
     -->
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 0267454..ee89ede 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -274,6 +274,10 @@
     <!-- Side padding on the side of notifications -->
     <dimen name="notification_side_paddings">16dp</dimen>
 
+    <!-- Starting translateY offset of the HUN appear and disappear animations. Indicates
+    the amount by the view is positioned above the screen before the animation starts. -->
+    <dimen name="heads_up_appear_y_above_screen">32dp</dimen>
+
     <!-- padding between the heads up and the statusbar -->
     <dimen name="heads_up_status_bar_padding">8dp</dimen>
 
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 6cb2d45..d511cab 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -262,4 +262,8 @@
 
     <!--Id for the device-entry UDFPS icon that lives in the alternate bouncer. -->
     <item type="id" name="alternate_bouncer_udfps_icon_view" />
+
+    <!-- Id for the udfps accessibility overlay -->
+    <item type="id" name="udfps_accessibility_overlay" />
+    <item type="id" name="udfps_accessibility_overlay_top_guideline" />
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e10925d..f4b25a7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2397,10 +2397,6 @@
     <!-- URl of the webpage that explains battery saver. -->
     <string name="help_uri_battery_saver_learn_more_link_target" translatable="false"></string>
 
-    <!-- Name for a quick settings tile, used only by platform developers, to extract the SystemUI process memory and send it to another
-         app for debugging. Will not be seen by users. [CHAR LIMIT=20] -->
-    <string name="heap_dump_tile_name">Dump SysUI Heap</string>
-
     <!-- Title for the privacy indicators dialog, only appears as part of a11y descriptions [CHAR LIMIT=NONE] -->
     <string name="ongoing_privacy_dialog_a11y_title">In use</string>
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index cdd7b80..74b975c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -39,7 +39,6 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.systemui.Dumpable;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
@@ -48,9 +47,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl;
-import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder;
 import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager;
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.log.dagger.KeyguardClockLog;
@@ -62,17 +59,11 @@
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder;
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
-import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsState;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.settings.SecureSettings;
@@ -102,14 +93,7 @@
     private final DumpManager mDumpManager;
     private final ClockEventController mClockEventController;
     private final LogBuffer mLogBuffer;
-    private final NotificationIconContainerAlwaysOnDisplayViewModel mAodIconsViewModel;
-    private final KeyguardRootViewModel mKeyguardRootViewModel;
-    private final ConfigurationState mConfigurationState;
-    private final SystemBarUtilsState mSystemBarUtilsState;
-    private final DozeParameters mDozeParameters;
-    private final ScreenOffAnimationController mScreenOffAnimationController;
-    private final AlwaysOnDisplayNotificationIconViewStore mAodIconViewStore;
-    private final StatusBarIconViewBindingFailureTracker mIconViewBindingFailureTracker;
+    private final NotificationIconContainerAlwaysOnDisplayViewBinder mNicViewBinder;
     private FrameLayout mSmallClockFrame; // top aligned clock
     private FrameLayout mLargeClockFrame; // centered clock
 
@@ -183,9 +167,7 @@
             KeyguardSliceViewController keyguardSliceViewController,
             NotificationIconAreaController notificationIconAreaController,
             LockscreenSmartspaceController smartspaceController,
-            SystemBarUtilsState systemBarUtilsState,
-            ScreenOffAnimationController screenOffAnimationController,
-            StatusBarIconViewBindingFailureTracker iconViewBindingFailureTracker,
+            NotificationIconContainerAlwaysOnDisplayViewBinder nicViewBinder,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
             SecureSettings secureSettings,
             @Main DelayableExecutor uiExecutor,
@@ -193,11 +175,6 @@
             DumpManager dumpManager,
             ClockEventController clockEventController,
             @KeyguardClockLog LogBuffer logBuffer,
-            NotificationIconContainerAlwaysOnDisplayViewModel aodIconsViewModel,
-            KeyguardRootViewModel keyguardRootViewModel,
-            ConfigurationState configurationState,
-            DozeParameters dozeParameters,
-            AlwaysOnDisplayNotificationIconViewStore aodIconViewStore,
             KeyguardInteractor keyguardInteractor,
             KeyguardClockInteractor keyguardClockInteractor,
             FeatureFlagsClassic featureFlags,
@@ -208,9 +185,7 @@
         mKeyguardSliceViewController = keyguardSliceViewController;
         mNotificationIconAreaController = notificationIconAreaController;
         mSmartspaceController = smartspaceController;
-        mSystemBarUtilsState = systemBarUtilsState;
-        mScreenOffAnimationController = screenOffAnimationController;
-        mIconViewBindingFailureTracker = iconViewBindingFailureTracker;
+        mNicViewBinder = nicViewBinder;
         mSecureSettings = secureSettings;
         mUiExecutor = uiExecutor;
         mBgExecutor = bgExecutor;
@@ -218,11 +193,6 @@
         mDumpManager = dumpManager;
         mClockEventController = clockEventController;
         mLogBuffer = logBuffer;
-        mAodIconsViewModel = aodIconsViewModel;
-        mKeyguardRootViewModel = keyguardRootViewModel;
-        mConfigurationState = configurationState;
-        mDozeParameters = dozeParameters;
-        mAodIconViewStore = aodIconViewStore;
         mView.setLogBuffer(mLogBuffer);
         mFeatureFlags = featureFlags;
         mKeyguardInteractor = keyguardInteractor;
@@ -619,28 +589,7 @@
                     mAodIconsBindHandle.dispose();
                 }
                 if (nic != null) {
-                    final DisposableHandle viewHandle =
-                            NotificationIconContainerViewBinder.bindWhileAttached(
-                                    nic,
-                                    mAodIconsViewModel,
-                                    mConfigurationState,
-                                    mSystemBarUtilsState,
-                                    mIconViewBindingFailureTracker,
-                                    mAodIconViewStore);
-                    final DisposableHandle visHandle = KeyguardRootViewBinder.bindAodIconVisibility(
-                            nic,
-                            mKeyguardRootViewModel.isNotifIconContainerVisible(),
-                            mConfigurationState,
-                            mFeatureFlags,
-                            mScreenOffAnimationController);
-                    if (visHandle == null) {
-                        mAodIconsBindHandle = viewHandle;
-                    } else {
-                        mAodIconsBindHandle = () -> {
-                            viewHandle.dispose();
-                            visHandle.dispose();
-                        };
-                    }
+                    mAodIconsBindHandle = mNicViewBinder.bindWhileAttached(nic);
                     mAodIconContainer = nic;
                 }
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 65668b5..240728a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -86,8 +86,6 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
-import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter;
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -115,7 +113,6 @@
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
-import javax.inject.Provider;
 
 import kotlinx.coroutines.ExperimentalCoroutinesApi;
 
@@ -152,7 +149,6 @@
     @NonNull private final SystemUIDialogManager mDialogManager;
     @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @NonNull private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor;
-    @NonNull private final Provider<UdfpsKeyguardViewModels> mUdfpsKeyguardViewModels;
     @NonNull private final VibratorHelper mVibrator;
     @NonNull private final FalsingManager mFalsingManager;
     @NonNull private final PowerManager mPowerManager;
@@ -623,7 +619,7 @@
         } else {
             onKeyguard = mOverlay != null
                     && mOverlay.getAnimationViewController()
-                        instanceof UdfpsKeyguardViewControllerAdapter;
+                        instanceof UdfpsKeyguardViewControllerLegacy;
         }
         return onKeyguard
                 && mKeyguardStateController.canDismissLockScreen()
@@ -666,7 +662,6 @@
             @NonNull InputManager inputManager,
             @NonNull KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
             @NonNull UdfpsKeyguardAccessibilityDelegate udfpsKeyguardAccessibilityDelegate,
-            @NonNull Provider<UdfpsKeyguardViewModels> udfpsKeyguardViewModelsProvider,
             @NonNull SelectedUserInteractor selectedUserInteractor,
             @NonNull FpsUnlockTracker fpsUnlockTracker,
             @NonNull KeyguardTransitionInteractor keyguardTransitionInteractor,
@@ -737,7 +732,6 @@
                     return Unit.INSTANCE;
                 });
         mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor;
-        mUdfpsKeyguardViewModels = udfpsKeyguardViewModelsProvider;
 
         final UdfpsOverlayController mUdfpsOverlayController = new UdfpsOverlayController();
         mFingerprintManager.setUdfpsOverlayController(mUdfpsOverlayController);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index dae6d08..aabee93 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -55,7 +55,6 @@
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -438,7 +437,7 @@
             if (DeviceEntryUdfpsRefactor.isEnabled) {
                 !keyguardStateController.isShowing
             } else {
-                animation !is UdfpsKeyguardViewControllerAdapter
+                animation !is UdfpsKeyguardViewControllerLegacy
             }
 
         if (keyguardNotShowing) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
index 63fe26a..64148f6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.res.R
@@ -81,8 +80,7 @@
         primaryBouncerInteractor,
         systemUIDialogManager,
         dumpManager,
-    ),
-    UdfpsKeyguardViewControllerAdapter {
+    ) {
     private val uniqueIdentifier = this.toString()
     private var showingUdfpsBouncer = false
     private var udfpsRequested = false
@@ -199,11 +197,27 @@
                 listenForAodToOccludedTransitions(this)
                 listenForAlternateBouncerToAodTransitions(this)
                 listenForDreamingToAodTransitions(this)
+                listenForPrimaryBouncerToAodTransitions(this)
             }
         }
     }
 
     @VisibleForTesting
+    suspend fun listenForPrimaryBouncerToAodTransitions(scope: CoroutineScope): Job {
+        return scope.launch {
+            transitionInteractor
+                .transition(KeyguardState.PRIMARY_BOUNCER, KeyguardState.AOD)
+                .collect { transitionStep ->
+                    view.onDozeAmountChanged(
+                        transitionStep.value,
+                        transitionStep.value,
+                        ANIMATE_APPEAR_ON_SCREEN_OFF,
+                    )
+                }
+        }
+    }
+
+    @VisibleForTesting
     suspend fun listenForDreamingToAodTransitions(scope: CoroutineScope): Job {
         return scope.launch {
             transitionInteractor.transition(KeyguardState.DREAMING, KeyguardState.AOD).collect {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt
deleted file mode 100644
index 6f4e1a3..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics.ui.controller
-
-import com.android.systemui.biometrics.UdfpsAnimationViewController
-import com.android.systemui.biometrics.UdfpsKeyguardView
-import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-/** Class that coordinates non-HBM animations during keyguard authentication. */
-@ExperimentalCoroutinesApi
-open class UdfpsKeyguardViewController(
-    val view: UdfpsKeyguardView,
-    statusBarStateController: StatusBarStateController,
-    primaryBouncerInteractor: PrimaryBouncerInteractor,
-    systemUIDialogManager: SystemUIDialogManager,
-    dumpManager: DumpManager,
-    private val alternateBouncerInteractor: AlternateBouncerInteractor,
-    udfpsKeyguardViewModels: UdfpsKeyguardViewModels,
-) :
-    UdfpsAnimationViewController<UdfpsKeyguardView>(
-        view,
-        statusBarStateController,
-        primaryBouncerInteractor,
-        systemUIDialogManager,
-        dumpManager,
-    ),
-    UdfpsKeyguardViewControllerAdapter {
-    private val uniqueIdentifier = this.toString()
-    override val tag: String
-        get() = TAG
-
-    init {
-        udfpsKeyguardViewModels.bindViews(view)
-    }
-
-    public override fun onViewAttached() {
-        super.onViewAttached()
-        alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, uniqueIdentifier)
-    }
-
-    public override fun onViewDetached() {
-        super.onViewDetached()
-        alternateBouncerInteractor.setAlternateBouncerUIAvailable(false, uniqueIdentifier)
-    }
-
-    override fun shouldPauseAuth(): Boolean {
-        return !view.isVisible()
-    }
-
-    override fun listenForTouchesOutsideView(): Boolean {
-        return true
-    }
-
-    companion object {
-        private const val TAG = "UdfpsKeyguardViewController"
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index 887b18c..0a13e48 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -16,8 +16,9 @@
 
 package com.android.systemui.communal.widgets
 
-import android.appwidget.AppWidgetProviderInfo
+import android.content.ComponentName
 import android.content.Intent
+import android.content.pm.PackageManager
 import android.os.Bundle
 import android.os.RemoteException
 import android.util.Log
@@ -39,10 +40,8 @@
     private var windowManagerService: IWindowManager? = null,
 ) : ComponentActivity() {
     companion object {
-        /**
-         * Intent extra name for the {@link AppWidgetProviderInfo} of a widget to add to hub mode.
-         */
-        const val ADD_WIDGET_INFO = "add_widget_info"
+        private const val EXTRA_FILTER_STRATEGY = "filter_strategy"
+        private const val FILTER_STRATEGY_GLANCEABLE_HUB = 1
         private const val TAG = "EditWidgetsActivity"
     }
 
@@ -51,13 +50,8 @@
             when (result.resultCode) {
                 RESULT_OK -> {
                     result.data
-                        ?.let {
-                            it.getParcelableExtra(
-                                ADD_WIDGET_INFO,
-                                AppWidgetProviderInfo::class.java
-                            )
-                        }
-                        ?.let { communalInteractor.addWidget(it.provider, 0) }
+                        ?.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME, ComponentName::class.java)
+                        ?.let { communalInteractor.addWidget(it, 0) }
                         ?: run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
                 }
                 else ->
@@ -71,13 +65,35 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
+        setShowWhenLocked(true)
+
         setCommunalEditWidgetActivityContent(
             activity = this,
             viewModel = communalViewModel,
             onOpenWidgetPicker = {
-                addWidgetActivityLauncher.launch(
-                    Intent(applicationContext, WidgetPickerActivity::class.java)
-                )
+                val localPackageManager: PackageManager = getPackageManager()
+                val intent =
+                    Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }
+                localPackageManager
+                    .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
+                    ?.activityInfo
+                    ?.packageName
+                    ?.let { packageName ->
+                        try {
+                            addWidgetActivityLauncher.launch(
+                                Intent(Intent.ACTION_PICK).also {
+                                    it.setPackage(packageName)
+                                    it.putExtra(
+                                        EXTRA_FILTER_STRATEGY,
+                                        FILTER_STRATEGY_GLANCEABLE_HUB
+                                    )
+                                }
+                            )
+                        } catch (e: Exception) {
+                            Log.e(TAG, "Failed to launch widget picker activity", e)
+                        }
+                    }
+                    ?: run { Log.e(TAG, "Couldn't resolve launcher package name") }
             },
             onEditDone = {
                 try {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt
deleted file mode 100644
index a26afc8..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.communal.widgets
-
-import android.appwidget.AppWidgetManager
-import android.appwidget.AppWidgetProviderInfo
-import android.content.Intent
-import android.graphics.Color
-import android.os.Bundle
-import android.util.Log
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.LinearLayout
-import androidx.activity.ComponentActivity
-import androidx.core.view.setMargins
-import androidx.core.view.setPadding
-import com.android.systemui.res.R
-import javax.inject.Inject
-
-/**
- * An Activity responsible for displaying a list of widgets to add to the hub mode grid. This is
- * essentially a placeholder until Launcher's widget picker can be used.
- */
-class WidgetPickerActivity
-@Inject
-constructor(
-    private val appWidgetManager: AppWidgetManager,
-) : ComponentActivity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        setContentView(R.layout.widget_picker)
-        loadWidgets()
-    }
-
-    private fun loadWidgets() {
-        val containerView: ViewGroup? = findViewById(R.id.widgets_container)
-        containerView?.apply {
-            try {
-                appWidgetManager
-                    .getInstalledProviders(AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD)
-                    ?.stream()
-                    ?.forEach { widgetInfo ->
-                        val activity = this@WidgetPickerActivity
-                        (widgetInfo.loadPreviewImage(activity, 0)
-                                ?: widgetInfo.loadIcon(activity, 0))
-                            ?.let {
-                                addView(
-                                    ImageView(activity).also { v ->
-                                        v.setImageDrawable(it)
-                                        v.setBackgroundColor(WIDGET_PREVIEW_BACKGROUND_COLOR)
-                                        v.setPadding(WIDGET_PREVIEW_PADDING)
-                                        v.layoutParams =
-                                            LinearLayout.LayoutParams(
-                                                    WIDGET_PREVIEW_SIZE,
-                                                    WIDGET_PREVIEW_SIZE
-                                                )
-                                                .also { lp ->
-                                                    lp.setMargins(WIDGET_PREVIEW_MARGINS)
-                                                }
-                                        v.setOnClickListener {
-                                            setResult(
-                                                RESULT_OK,
-                                                Intent()
-                                                    .putExtra(
-                                                        EditWidgetsActivity.ADD_WIDGET_INFO,
-                                                        widgetInfo
-                                                    )
-                                            )
-                                            finish()
-                                        }
-                                    }
-                                )
-                            }
-                    }
-            } catch (e: RuntimeException) {
-                Log.e(TAG, "Exception fetching widget providers", e)
-            }
-        }
-    }
-
-    companion object {
-        private const val WIDGET_PREVIEW_SIZE = 600
-        private const val WIDGET_PREVIEW_MARGINS = 32
-        private const val WIDGET_PREVIEW_PADDING = 32
-        private val WIDGET_PREVIEW_BACKGROUND_COLOR = Color.rgb(216, 225, 220)
-        private const val TAG = "WidgetPickerActivity"
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
index 4b27af1..9afd5ed 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
@@ -20,7 +20,6 @@
 
 import com.android.systemui.ForegroundServicesDialog;
 import com.android.systemui.communal.widgets.EditWidgetsActivity;
-import com.android.systemui.communal.widgets.WidgetPickerActivity;
 import com.android.systemui.contrast.ContrastDialogActivity;
 import com.android.systemui.keyguard.WorkLockActivity;
 import com.android.systemui.people.PeopleSpaceActivity;
@@ -158,12 +157,6 @@
     @ClassKey(EditWidgetsActivity.class)
     public abstract Activity bindEditWidgetsActivity(EditWidgetsActivity activity);
 
-    /** Inject into WidgetPickerActivity. */
-    @Binds
-    @IntoMap
-    @ClassKey(WidgetPickerActivity.class)
-    public abstract Activity bindWidgetPickerActivity(WidgetPickerActivity activity);
-
     /** Inject into SwitchToManagedProfileForCallActivity. */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt
new file mode 100644
index 0000000..ef2537c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.deviceentry.ui.binder
+
+import android.annotation.SuppressLint
+import androidx.core.view.isInvisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
+import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+object UdfpsAccessibilityOverlayBinder {
+
+    /** Forwards hover events to the view model to make guided announcements for accessibility. */
+    @SuppressLint("ClickableViewAccessibility")
+    @JvmStatic
+    fun bind(
+        view: UdfpsAccessibilityOverlay,
+        viewModel: UdfpsAccessibilityOverlayViewModel,
+    ) {
+        view.setOnHoverListener { v, event -> viewModel.onHoverEvent(v, event) }
+        view.repeatWhenAttached {
+            // Repeat on CREATED because we update the visibility of the view
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                viewModel.visible.collect { visible -> view.isInvisible = !visible }
+            }
+        }
+    }
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt
similarity index 71%
copy from core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
copy to packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt
index 6c1f0fc..7be3230 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package android.companion.virtual.camera;
+package com.android.systemui.deviceentry.ui.view
 
-/**
- * Data structure used to store {@link android.hardware.camera2.CameraMetadata} compatible with
- * VirtualCamera.
- * @hide
- */
-parcelable VirtualCameraMetadata;
+import android.content.Context
+import android.view.View
+
+/** Overlay to handle under-fingerprint sensor accessibility events. */
+class UdfpsAccessibilityOverlay(context: Context?) : View(context)
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt
new file mode 100644
index 0000000..80684b4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.deviceentry.ui.viewmodel
+
+import android.graphics.Point
+import android.view.MotionEvent
+import android.view.View
+import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor
+import com.android.systemui.biometrics.UdfpsUtils
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
+import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+
+/** Models the UI state for the UDFPS accessibility overlay */
+@ExperimentalCoroutinesApi
+class UdfpsAccessibilityOverlayViewModel
+@Inject
+constructor(
+    udfpsOverlayInteractor: UdfpsOverlayInteractor,
+    accessibilityInteractor: AccessibilityInteractor,
+    deviceEntryIconViewModel: DeviceEntryIconViewModel,
+    deviceEntryFgIconViewModel: DeviceEntryForegroundViewModel,
+) {
+    private val udfpsUtils = UdfpsUtils()
+    private val udfpsOverlayParams: StateFlow<UdfpsOverlayParams> =
+        udfpsOverlayInteractor.udfpsOverlayParams
+
+    /** Overlay is only visible if touch exploration is enabled and UDFPS can be used. */
+    val visible: Flow<Boolean> =
+        accessibilityInteractor.isTouchExplorationEnabled.flatMapLatest { touchExplorationEnabled ->
+            if (touchExplorationEnabled) {
+                combine(
+                    deviceEntryFgIconViewModel.viewModel,
+                    deviceEntryIconViewModel.deviceEntryViewAlpha,
+                ) { iconViewModel, alpha ->
+                    iconViewModel.type == DeviceEntryIconView.IconType.FINGERPRINT &&
+                        !iconViewModel.useAodVariant &&
+                        alpha == 1f
+                }
+            } else {
+                flowOf(false)
+            }
+        }
+
+    /** Give directional feedback to help the user authenticate with UDFPS. */
+    fun onHoverEvent(v: View, event: MotionEvent): Boolean {
+        val overlayParams = udfpsOverlayParams.value
+        val scaledTouch: Point =
+            udfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0), event, overlayParams)
+
+        if (!udfpsUtils.isWithinSensorArea(event.getPointerId(0), event, overlayParams)) {
+            // view only receives motionEvents when [visible] which requires touchExplorationEnabled
+            val announceStr =
+                udfpsUtils.onTouchOutsideOfSensorArea(
+                    /* touchExplorationEnabled */ true,
+                    v.context,
+                    scaledTouch.x,
+                    scaledTouch.y,
+                    overlayParams,
+                )
+            if (announceStr != null) {
+                v.announceForAccessibility(announceStr)
+            }
+        }
+        // always let the motion events go through to underlying views
+        return false
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 5ec51f4..1540423 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -25,7 +25,9 @@
 import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
 import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
@@ -36,20 +38,28 @@
 class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, handler: Handler) :
     FlagDependenciesBase(featureFlags, handler) {
     override fun defineDependencies() {
+        // Internal notification backend dependencies
+        crossAppPoliteNotifications dependsOn politeNotifications
+        vibrateWhileUnlockedToken dependsOn politeNotifications
+
+        // Internal notification frontend dependencies
         NotificationsLiveDataStoreRefactor.token dependsOn NotificationIconContainerRefactor.token
         FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
 
-        val keyguardBottomAreaRefactor = FlagToken(
-                FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor())
+        // Internal keyguard dependencies
         KeyguardShadeMigrationNssl.token dependsOn keyguardBottomAreaRefactor
 
-        val crossAppPoliteNotifToken =
-                FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications())
-        val politeNotifToken = FlagToken(FLAG_POLITE_NOTIFICATIONS, politeNotifications())
-        crossAppPoliteNotifToken dependsOn politeNotifToken
-
-        val vibrateWhileUnlockedToken =
-                FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked())
-        vibrateWhileUnlockedToken dependsOn politeNotifToken
+        // SceneContainer dependencies
+        SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
+        SceneContainerFlag.getMainStaticFlag() dependsOn MIGRATE_KEYGUARD_STATUS_BAR_VIEW
     }
+
+    private inline val politeNotifications
+        get() = FlagToken(FLAG_POLITE_NOTIFICATIONS, politeNotifications())
+    private inline val crossAppPoliteNotifications
+        get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications())
+    private inline val vibrateWhileUnlockedToken: FlagToken
+        get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked())
+    private inline val keyguardBottomAreaRefactor
+        get() = FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index b43f54d..f6db978 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -17,11 +17,11 @@
 
 import android.provider.DeviceConfig
 import com.android.internal.annotations.Keep
-import com.android.systemui.res.R
 import com.android.systemui.flags.FlagsFactory.releasedFlag
 import com.android.systemui.flags.FlagsFactory.resourceBooleanFlag
 import com.android.systemui.flags.FlagsFactory.sysPropBooleanFlag
 import com.android.systemui.flags.FlagsFactory.unreleasedFlag
+import com.android.systemui.res.R
 
 /**
  * List of [Flag] objects for use in SystemUI.
@@ -483,9 +483,6 @@
     // TODO(b/264916608): Tracking Bug
     @JvmField val SCREENSHOT_METADATA = unreleasedFlag("screenshot_metadata")
 
-    // TODO(b/266955521): Tracking bug
-    @JvmField val SCREENSHOT_DETECTION = releasedFlag("screenshot_detection")
-
     // TODO(b/251205791): Tracking Bug
     @JvmField val SCREENSHOT_APP_CLIPS = releasedFlag("screenshot_app_clips")
 
@@ -607,9 +604,6 @@
     @JvmField
     val LOCKSCREEN_WALLPAPER_DREAM_ENABLED = unreleasedFlag("enable_lockscreen_wallpaper_dream")
 
-    // TODO(b/283084712): Tracking Bug
-    @JvmField val IMPROVED_HUN_ANIMATIONS = unreleasedFlag("improved_hun_animations")
-
     // TODO(b/283447257): Tracking bug
     @JvmField
     val BIGPICTURE_NOTIFICATION_LAZY_LOADING =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 20da00e..af5d48d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -124,7 +124,7 @@
 
         indicationAreaHandle =
             KeyguardIndicationAreaBinder.bind(
-                notificationShadeWindowView,
+                notificationShadeWindowView.requireViewById(R.id.keyguard_indication_area),
                 keyguardIndicationAreaViewModel,
                 keyguardRootViewModel,
                 indicationController,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
index de15fd6..c98f637 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.res.R
 import javax.inject.Inject
@@ -48,7 +49,9 @@
 ) {
 
     init {
-        alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, TAG)
+        if (!DeviceEntryUdfpsRefactor.isEnabled) {
+            alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, TAG)
+        }
     }
 
     private val showIndicatorForPrimaryBouncer: Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt
deleted file mode 100644
index ebf1beb..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.keyguard.ui.adapter
-
-/**
- * Temporary adapter class while
- * [com.android.systemui.biometrics.ui.controller.UdfpsKeyguardViewController] is being refactored
- * before [com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy] is removed.
- *
- * TODO (b/278719514): Delete once udfps keyguard view is fully refactored.
- */
-interface UdfpsKeyguardViewControllerAdapter
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt
index d12d193..c749818 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
 
 @ExperimentalCoroutinesApi
 object AlternateBouncerUdfpsViewBinder {
@@ -71,10 +72,12 @@
         bgView.visibility = View.VISIBLE
         bgView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
-                viewModel.bgViewModel.collect { bgViewModel ->
-                    bgView.alpha = bgViewModel.alpha
-                    bgView.imageTintList = ColorStateList.valueOf(bgViewModel.tint)
+                launch {
+                    viewModel.bgColor.collect { color ->
+                        bgView.imageTintList = ColorStateList.valueOf(color)
+                    }
                 }
+                launch { viewModel.bgAlpha.collect { alpha -> bgView.alpha = alpha } }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index dcf4284..a02e8ac 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -131,10 +131,10 @@
 
         bgView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
+                launch { bgViewModel.alpha.collect { alpha -> bgView.alpha = alpha } }
                 launch {
-                    bgViewModel.viewModel.collect { bgViewModel ->
-                        bgView.alpha = bgViewModel.alpha
-                        bgView.imageTintList = ColorStateList.valueOf(bgViewModel.tint)
+                    bgViewModel.color.collect { color ->
+                        bgView.imageTintList = ColorStateList.valueOf(color)
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
index 4efd9ef..4c33d90 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
@@ -54,12 +54,11 @@
         keyguardRootViewModel: KeyguardRootViewModel,
         indicationController: KeyguardIndicationController,
     ): DisposableHandle {
-        val indicationArea: ViewGroup = view.requireViewById(R.id.keyguard_indication_area)
-        indicationController.setIndicationArea(indicationArea)
+        indicationController.setIndicationArea(view)
 
-        val indicationText: TextView = indicationArea.requireViewById(R.id.keyguard_indication_text)
+        val indicationText: TextView = view.requireViewById(R.id.keyguard_indication_text)
         val indicationTextBottom: TextView =
-            indicationArea.requireViewById(R.id.keyguard_indication_text_bottom)
+            view.requireViewById(R.id.keyguard_indication_text_bottom)
 
         view.clipChildren = false
         view.clipToPadding = false
@@ -71,7 +70,7 @@
                     launch {
                         if (keyguardBottomAreaRefactor()) {
                             keyguardRootViewModel.alpha.collect { alpha ->
-                                indicationArea.apply {
+                                view.apply {
                                     this.importantForAccessibility =
                                         if (alpha == 0f) {
                                             View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
@@ -83,7 +82,7 @@
                             }
                         } else {
                             viewModel.alpha.collect { alpha ->
-                                indicationArea.apply {
+                                view.apply {
                                     this.importantForAccessibility =
                                         if (alpha == 0f) {
                                             View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
@@ -98,7 +97,7 @@
 
                     launch {
                         viewModel.indicationAreaTranslationX.collect { translationX ->
-                            indicationArea.translationX = translationX
+                            view.translationX = translationX
                         }
                     }
 
@@ -113,9 +112,7 @@
                                     0
                                 }
                             }
-                            .collect { paddingPx ->
-                                indicationArea.setPadding(paddingPx, 0, paddingPx, 0)
-                            }
+                            .collect { paddingPx -> view.setPadding(paddingPx, 0, paddingPx, 0) }
                     }
 
                     launch {
@@ -124,7 +121,7 @@
                             .flatMapLatest { defaultBurnInOffsetY ->
                                 viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
                             }
-                            .collect { translationY -> indicationArea.translationY = translationY }
+                            .collect { translationY -> view.translationY = translationY }
                     }
 
                     launch {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 01a1ca3..362e7e6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -29,7 +29,6 @@
 import android.view.ViewPropertyAnimator
 import android.view.WindowInsets
 import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.animation.Interpolators
 import com.android.internal.jank.InteractionJankMonitor
@@ -67,6 +66,7 @@
 import javax.inject.Provider
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
@@ -205,7 +205,6 @@
                                     childViews[aodNotificationIconContainerId]
                                         ?.setAodNotifIconContainerIsVisible(
                                             isVisible,
-                                            featureFlags,
                                             iconsAppearTranslationPx.value,
                                             screenOffAnimationController,
                                         )
@@ -359,41 +358,32 @@
         }
     }
 
-    @JvmStatic
-    fun bindAodIconVisibility(
+    suspend fun bindAodNotifIconVisibility(
         view: View,
         isVisible: Flow<AnimatedValue<Boolean>>,
         configuration: ConfigurationState,
-        featureFlags: FeatureFlagsClassic,
         screenOffAnimationController: ScreenOffAnimationController,
-    ): DisposableHandle? {
+    ) {
         KeyguardShadeMigrationNssl.assertInLegacyMode()
-        if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return null
-        return view.repeatWhenAttached {
-            lifecycleScope.launch {
-                val iconAppearTranslationPx =
-                    configuration
-                        .getDimensionPixelSize(R.dimen.shelf_appear_translation)
-                        .stateIn(this)
-                isVisible.collect { isVisible ->
-                    view.setAodNotifIconContainerIsVisible(
-                        isVisible,
-                        featureFlags,
-                        iconAppearTranslationPx.value,
-                        screenOffAnimationController,
-                    )
-                }
+        if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return
+        coroutineScope {
+            val iconAppearTranslationPx =
+                configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this)
+            isVisible.collect { isVisible ->
+                view.setAodNotifIconContainerIsVisible(
+                    isVisible = isVisible,
+                    iconsAppearTranslationPx = iconAppearTranslationPx.value,
+                    screenOffAnimationController = screenOffAnimationController,
+                )
             }
         }
     }
 
     private fun View.setAodNotifIconContainerIsVisible(
         isVisible: AnimatedValue<Boolean>,
-        featureFlags: FeatureFlagsClassic,
         iconsAppearTranslationPx: Int,
         screenOffAnimationController: ScreenOffAnimationController,
     ) {
-        val statusViewMigrated = KeyguardShadeMigrationNssl.isEnabled
         animate().cancel()
         val animatorListener =
             object : AnimatorListenerAdapter() {
@@ -404,13 +394,13 @@
         when {
             !isVisible.isAnimating -> {
                 alpha = 1f
-                if (!statusViewMigrated) {
+                if (!KeyguardShadeMigrationNssl.isEnabled) {
                     translationY = 0f
                 }
                 visibility = if (isVisible.value) View.VISIBLE else View.INVISIBLE
             }
             newAodTransition() -> {
-                animateInIconTranslation(statusViewMigrated)
+                animateInIconTranslation()
                 if (isVisible.value) {
                     CrossFadeHelper.fadeIn(this, animatorListener)
                 } else {
@@ -419,7 +409,7 @@
             }
             !isVisible.value -> {
                 // Let's make sure the icon are translated to 0, since we cancelled it above
-                animateInIconTranslation(statusViewMigrated)
+                animateInIconTranslation()
                 CrossFadeHelper.fadeOut(this, animatorListener)
             }
             visibility != View.VISIBLE -> {
@@ -429,13 +419,12 @@
                 appearIcons(
                     animate = screenOffAnimationController.shouldAnimateAodIcons(),
                     iconsAppearTranslationPx,
-                    statusViewMigrated,
                     animatorListener,
                 )
             }
             else -> {
                 // Let's make sure the icons are translated to 0, since we cancelled it above
-                animateInIconTranslation(statusViewMigrated)
+                animateInIconTranslation()
                 // We were fading out, let's fade in instead
                 CrossFadeHelper.fadeIn(this, animatorListener)
             }
@@ -445,11 +434,10 @@
     private fun View.appearIcons(
         animate: Boolean,
         iconAppearTranslation: Int,
-        statusViewMigrated: Boolean,
         animatorListener: Animator.AnimatorListener,
     ) {
         if (animate) {
-            if (!statusViewMigrated) {
+            if (!KeyguardShadeMigrationNssl.isEnabled) {
                 translationY = -iconAppearTranslation.toFloat()
             }
             alpha = 0f
@@ -457,19 +445,19 @@
                 .alpha(1f)
                 .setInterpolator(Interpolators.LINEAR)
                 .setDuration(AOD_ICONS_APPEAR_DURATION)
-                .apply { if (statusViewMigrated) animateInIconTranslation() }
+                .apply { if (KeyguardShadeMigrationNssl.isEnabled) animateInIconTranslation() }
                 .setListener(animatorListener)
                 .start()
         } else {
             alpha = 1.0f
-            if (!statusViewMigrated) {
+            if (!KeyguardShadeMigrationNssl.isEnabled) {
                 translationY = 0f
             }
         }
     }
 
-    private fun View.animateInIconTranslation(statusViewMigrated: Boolean) {
-        if (!statusViewMigrated) {
+    private fun View.animateInIconTranslation() {
+        if (!KeyguardShadeMigrationNssl.isEnabled) {
             animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
deleted file mode 100644
index 52d87d3..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.ui.binder
-
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.airbnb.lottie.LottieAnimationView
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-
-@ExperimentalCoroutinesApi
-object UdfpsAodFingerprintViewBinder {
-
-    /**
-     * Drives UI for the UDFPS aod fingerprint view. See [UdfpsFingerprintViewBinder] and
-     * [UdfpsBackgroundViewBinder].
-     */
-    @JvmStatic
-    fun bind(
-        view: LottieAnimationView,
-        viewModel: UdfpsAodViewModel,
-    ) {
-        view.alpha = 0f
-        view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                launch {
-                    viewModel.burnInOffsets.collect { burnInOffsets ->
-                        view.progress = burnInOffsets.progress
-                        view.translationX = burnInOffsets.x.toFloat()
-                        view.translationY = burnInOffsets.y.toFloat()
-                    }
-                }
-
-                launch { viewModel.alpha.collect { alpha -> view.alpha = alpha } }
-
-                launch {
-                    viewModel.padding.collect { padding ->
-                        view.setPadding(padding, padding, padding, padding)
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt
deleted file mode 100644
index 0113628..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.ui.binder
-
-import android.content.res.ColorStateList
-import android.widget.ImageView
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-
-@ExperimentalCoroutinesApi
-object UdfpsBackgroundViewBinder {
-
-    /**
-     * Drives UI for the udfps background view. See [UdfpsAodFingerprintViewBinder] and
-     * [UdfpsFingerprintViewBinder].
-     */
-    @JvmStatic
-    fun bind(
-        view: ImageView,
-        viewModel: BackgroundViewModel,
-    ) {
-        view.alpha = 0f
-        view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                launch {
-                    viewModel.transition.collect {
-                        view.alpha = it.alpha
-                        view.scaleX = it.scale
-                        view.scaleY = it.scale
-                        view.imageTintList = ColorStateList.valueOf(it.color)
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
deleted file mode 100644
index d4621e6..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.ui.binder
-
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffColorFilter
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.airbnb.lottie.LottieAnimationView
-import com.airbnb.lottie.LottieProperty
-import com.airbnb.lottie.model.KeyPath
-import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-
-@ExperimentalCoroutinesApi
-object UdfpsFingerprintViewBinder {
-    private var udfpsIconColor = 0
-
-    /**
-     * Drives UI for the UDFPS fingerprint view when it's NOT on aod. See
-     * [UdfpsAodFingerprintViewBinder] and [UdfpsBackgroundViewBinder].
-     */
-    @JvmStatic
-    fun bind(
-        view: LottieAnimationView,
-        viewModel: FingerprintViewModel,
-    ) {
-        view.alpha = 0f
-        view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                launch {
-                    viewModel.transition.collect {
-                        view.alpha = it.alpha
-                        view.scaleX = it.scale
-                        view.scaleY = it.scale
-                        if (udfpsIconColor != (it.color)) {
-                            udfpsIconColor = it.color
-                            view.invalidate()
-                        }
-                    }
-                }
-
-                launch {
-                    viewModel.burnInOffsets.collect { burnInOffsets ->
-                        view.translationX = burnInOffsets.x.toFloat()
-                        view.translationY = burnInOffsets.y.toFloat()
-                    }
-                }
-
-                launch {
-                    viewModel.dozeAmount.collect { dozeAmount ->
-                        // Lottie progress represents: aod=0 to lockscreen=1
-                        view.progress = 1f - dozeAmount
-                    }
-                }
-
-                launch {
-                    viewModel.padding.collect { padding ->
-                        view.setPadding(padding, padding, padding, padding)
-                    }
-                }
-            }
-        }
-
-        // Add a callback that updates the color to `udfpsIconColor` whenever invalidate is called
-        view.addValueCallback(KeyPath("**"), LottieProperty.COLOR_FILTER) {
-            PorterDuffColorFilter(udfpsIconColor, PorterDuff.Mode.SRC_ATOP)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt
deleted file mode 100644
index aabb3f4..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.biometrics.ui.binder
-
-import android.view.View
-import com.android.systemui.res.R
-import com.android.systemui.keyguard.ui.binder.UdfpsAodFingerprintViewBinder
-import com.android.systemui.keyguard.ui.binder.UdfpsBackgroundViewBinder
-import com.android.systemui.keyguard.ui.binder.UdfpsFingerprintViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel
-import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardInternalViewModel
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-@ExperimentalCoroutinesApi
-object UdfpsKeyguardInternalViewBinder {
-
-    @JvmStatic
-    fun bind(
-        view: View,
-        viewModel: UdfpsKeyguardInternalViewModel,
-        aodViewModel: UdfpsAodViewModel,
-        fingerprintViewModel: FingerprintViewModel,
-        backgroundViewModel: BackgroundViewModel,
-    ) {
-        view.accessibilityDelegate = viewModel.accessibilityDelegate
-
-        // bind child views
-        UdfpsAodFingerprintViewBinder.bind(view.requireViewById(R.id.udfps_aod_fp), aodViewModel)
-        UdfpsFingerprintViewBinder.bind(
-            view.requireViewById(R.id.udfps_lockscreen_fp),
-            fingerprintViewModel
-        )
-        UdfpsBackgroundViewBinder.bind(
-            view.requireViewById(R.id.udfps_keyguard_fp_bg),
-            backgroundViewModel
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt
deleted file mode 100644
index 475d26f..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.ui.binder
-
-import android.graphics.RectF
-import android.view.View
-import android.widget.FrameLayout
-import androidx.asynclayoutinflater.view.AsyncLayoutInflater
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.biometrics.UdfpsKeyguardView
-import com.android.systemui.biometrics.ui.binder.UdfpsKeyguardInternalViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel
-import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardInternalViewModel
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModel
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.res.R
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.launch
-
-@ExperimentalCoroutinesApi
-object UdfpsKeyguardViewBinder {
-    /**
-     * Drives UI for the keyguard UDFPS view. Inflates child views on a background thread. For view
-     * binders for its child views, see [UdfpsFingerprintViewBinder], [UdfpsBackgroundViewBinder] &
-     * [UdfpsAodFingerprintViewBinder].
-     */
-    @JvmStatic
-    fun bind(
-        view: UdfpsKeyguardView,
-        viewModel: UdfpsKeyguardViewModel,
-        udfpsKeyguardInternalViewModel: UdfpsKeyguardInternalViewModel,
-        aodViewModel: UdfpsAodViewModel,
-        fingerprintViewModel: FingerprintViewModel,
-        backgroundViewModel: BackgroundViewModel,
-    ) {
-        val layoutInflaterFinishListener =
-            AsyncLayoutInflater.OnInflateFinishedListener { inflatedInternalView, _, parent ->
-                UdfpsKeyguardInternalViewBinder.bind(
-                    inflatedInternalView,
-                    udfpsKeyguardInternalViewModel,
-                    aodViewModel,
-                    fingerprintViewModel,
-                    backgroundViewModel,
-                )
-                val lp = inflatedInternalView.layoutParams as FrameLayout.LayoutParams
-                lp.width = viewModel.sensorBounds.width()
-                lp.height = viewModel.sensorBounds.height()
-                val relativeToView =
-                    getBoundsRelativeToView(
-                        inflatedInternalView,
-                        RectF(viewModel.sensorBounds),
-                    )
-                lp.setMarginsRelative(
-                    relativeToView.left.toInt(),
-                    relativeToView.top.toInt(),
-                    relativeToView.right.toInt(),
-                    relativeToView.bottom.toInt(),
-                )
-                parent!!.addView(inflatedInternalView, lp)
-            }
-        val inflater = AsyncLayoutInflater(view.context)
-        inflater.inflate(R.layout.udfps_keyguard_view_internal, view, layoutInflaterFinishListener)
-
-        view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.CREATED) {
-                launch {
-                    combine(aodViewModel.isVisible, fingerprintViewModel.visible) {
-                            isAodVisible,
-                            isFingerprintVisible ->
-                            isAodVisible || isFingerprintVisible
-                        }
-                        .collect { view.setVisible(it) }
-                }
-            }
-        }
-    }
-
-    /**
-     * Converts coordinates of RectF relative to the screen to coordinates relative to this view.
-     *
-     * @param bounds RectF based off screen coordinates in current orientation
-     */
-    private fun getBoundsRelativeToView(view: View, bounds: RectF): RectF {
-        val pos: IntArray = view.locationOnScreen
-        return RectF(
-            bounds.left - pos[0],
-            bounds.top - pos[1],
-            bounds.right - pos[0],
-            bounds.bottom - pos[1]
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt
index 1c6a2ab..bc9671e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt
@@ -31,18 +31,21 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
+import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection
 import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule.Companion.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION
 import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
 import java.util.Optional
 import javax.inject.Inject
 import javax.inject.Named
 import kotlin.jvm.optionals.getOrNull
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 /**
  * Positions elements of the lockscreen to the default position.
  *
  * This will be the most common use case for phones in portrait mode.
  */
+@ExperimentalCoroutinesApi
 @SysUISingleton
 @JvmSuppressWildcards
 class DefaultKeyguardBlueprint
@@ -62,6 +65,7 @@
     communalTutorialIndicatorSection: CommunalTutorialIndicatorSection,
     clockSection: ClockSection,
     smartspaceSection: SmartspaceSection,
+    udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection,
 ) : KeyguardBlueprint {
     override val id: String = DEFAULT
 
@@ -79,7 +83,8 @@
             aodBurnInSection,
             communalTutorialIndicatorSection,
             clockSection,
-            defaultDeviceEntrySection, // Add LAST: Intentionally has z-order above other views.
+            defaultDeviceEntrySection,
+            udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others
         )
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
index bf70682..9b40433 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
@@ -29,14 +29,17 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
+import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection
 import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines
 import com.android.systemui.util.kotlin.getOrNull
 import java.util.Optional
 import javax.inject.Inject
 import javax.inject.Named
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 /** Vertically aligns the shortcuts with the udfps. */
+@ExperimentalCoroutinesApi
 @SysUISingleton
 class ShortcutsBesideUdfpsKeyguardBlueprint
 @Inject
@@ -53,6 +56,7 @@
     defaultNotificationStackScrollLayoutSection: DefaultNotificationStackScrollLayoutSection,
     aodNotificationIconsSection: AodNotificationIconsSection,
     aodBurnInSection: AodBurnInSection,
+    udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection,
 ) : KeyguardBlueprint {
     override val id: String = SHORTCUTS_BESIDE_UDFPS
 
@@ -68,7 +72,8 @@
             splitShadeGuidelines,
             aodNotificationIconsSection,
             aodBurnInSection,
-            defaultDeviceEntrySection, // Add LAST: Intentionally has z-order above other views.
+            defaultDeviceEntrySection,
+            udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others
         )
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
index 56f717d..66c137f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
@@ -54,7 +54,7 @@
         if (keyguardBottomAreaRefactor()) {
             indicationAreaHandle =
                 KeyguardIndicationAreaBinder.bind(
-                    constraintLayout,
+                    constraintLayout.requireViewById(R.id.keyguard_indication_area),
                     keyguardIndicationAreaViewModel,
                     keyguardRootViewModel,
                     indicationController,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
new file mode 100644
index 0000000..e1a33de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.ui.view.layout.sections
+
+import android.content.Context
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.Flags
+import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder
+import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
+import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel
+import com.android.systemui.keyguard.shared.model.KeyguardSection
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** Positions the UDFPS accessibility overlay on the bottom half of the keyguard. */
+@ExperimentalCoroutinesApi
+class DefaultUdfpsAccessibilityOverlaySection
+@Inject
+constructor(
+    private val context: Context,
+    private val viewModel: UdfpsAccessibilityOverlayViewModel,
+) : KeyguardSection() {
+    private val viewId = R.id.udfps_accessibility_overlay
+    private var cachedConstraintLayout: ConstraintLayout? = null
+
+    override fun addViews(constraintLayout: ConstraintLayout) {
+        cachedConstraintLayout = constraintLayout
+        constraintLayout.addView(UdfpsAccessibilityOverlay(context).apply { id = viewId })
+    }
+
+    override fun bindData(constraintLayout: ConstraintLayout) {
+        UdfpsAccessibilityOverlayBinder.bind(
+            constraintLayout.findViewById(viewId)!!,
+            viewModel,
+        )
+    }
+
+    override fun applyConstraints(constraintSet: ConstraintSet) {
+        constraintSet.apply {
+            connect(viewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
+            connect(viewId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
+
+            create(R.id.udfps_accessibility_overlay_top_guideline, ConstraintSet.HORIZONTAL)
+            setGuidelinePercent(R.id.udfps_accessibility_overlay_top_guideline, .5f)
+            connect(
+                viewId,
+                ConstraintSet.TOP,
+                R.id.udfps_accessibility_overlay_top_guideline,
+                ConstraintSet.BOTTOM,
+            )
+
+            if (Flags.keyguardBottomAreaRefactor()) {
+                connect(
+                    viewId,
+                    ConstraintSet.BOTTOM,
+                    R.id.keyguard_indication_area,
+                    ConstraintSet.TOP,
+                )
+            } else {
+                connect(viewId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
+            }
+        }
+    }
+
+    override fun removeViews(constraintLayout: ConstraintLayout) {
+        constraintLayout.removeView(viewId)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
index e18893a..f4ae365 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
@@ -43,6 +43,7 @@
     val context: Context,
     configurationInteractor: ConfigurationInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+    deviceEntryBackgroundViewModel: DeviceEntryBackgroundViewModel,
     fingerprintPropertyRepository: FingerprintPropertyRepository,
 ) {
     private val isSupported: Flow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported
@@ -90,26 +91,8 @@
             )
         }
 
-    private val bgColor: Flow<Int> =
-        configurationInteractor.onAnyConfigurationChange
-            .map {
-                Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorSurface)
-            }
-            .onStart {
-                emit(
-                    Utils.getColorAttrDefaultColor(
-                        context,
-                        com.android.internal.R.attr.colorSurface
-                    )
-                )
-            }
-    val bgViewModel: Flow<DeviceEntryBackgroundViewModel.BackgroundViewModel> =
-        bgColor.map { color ->
-            DeviceEntryBackgroundViewModel.BackgroundViewModel(
-                alpha = 1f,
-                tint = color,
-            )
-        }
+    val bgColor: Flow<Int> = deviceEntryBackgroundViewModel.color
+    val bgAlpha: Flow<Float> = flowOf(1f)
 
     data class IconLocation(
         val left: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
index c45caf0..be9ae1d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
@@ -19,11 +19,10 @@
 
 import android.content.Context
 import com.android.settingslib.Utils
-import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
@@ -34,7 +33,7 @@
 @Inject
 constructor(
     val context: Context,
-    configurationRepository: ConfigurationRepository, // TODO (b/309655554): create & use interactor
+    configurationInteractor: ConfigurationInteractor,
     lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel,
     aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
     goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
@@ -44,8 +43,8 @@
     dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
     alternateBouncerToAodTransitionViewModel: AlternateBouncerToAodTransitionViewModel,
 ) {
-    private val color: Flow<Int> =
-        configurationRepository.onAnyConfigurationChange
+    val color: Flow<Int> =
+        configurationInteractor.onAnyConfigurationChange
             .map {
                 Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorSurface)
             }
@@ -57,7 +56,7 @@
                     )
                 )
             }
-    private val alpha: Flow<Float> =
+    val alpha: Flow<Float> =
         setOf(
                 lockscreenToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
                 aodToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
@@ -69,17 +68,4 @@
                 alternateBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
             )
             .merge()
-
-    val viewModel: Flow<BackgroundViewModel> =
-        combine(color, alpha) { color, alpha ->
-            BackgroundViewModel(
-                alpha = alpha,
-                tint = color,
-            )
-        }
-
-    data class BackgroundViewModel(
-        val alpha: Float,
-        val tint: Int,
-    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
new file mode 100644
index 0000000..d57e569
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class LockscreenContentViewModel
+@Inject
+constructor(
+    private val interactor: KeyguardBlueprintInteractor,
+    private val authController: AuthController,
+) {
+    val isUdfpsVisible: Boolean
+        get() = authController.isUdfpsSupported
+
+    fun blueprintId(scope: CoroutineScope): StateFlow<String> {
+        return interactor.blueprint
+            .map { it.id }
+            .distinctUntilChanged()
+            .stateIn(
+                scope = scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = interactor.getCurrentBlueprint().id,
+            )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt
deleted file mode 100644
index 6e77e13e..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.viewmodel
-
-import android.content.Context
-import com.android.systemui.keyguard.domain.interactor.Offsets
-import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
-import com.android.systemui.res.R
-import javax.inject.Inject
-import kotlin.math.roundToInt
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-
-/** View-model for UDFPS AOD view. */
-@ExperimentalCoroutinesApi
-class UdfpsAodViewModel
-@Inject
-constructor(
-    val interactor: UdfpsKeyguardInteractor,
-    val context: Context,
-) {
-    val alpha: Flow<Float> = interactor.dozeAmount
-    val burnInOffsets: Flow<Offsets> = interactor.burnInOffsets
-    val isVisible: Flow<Boolean> = alpha.map { it != 0f }
-
-    // Padding between the fingerprint icon and its bounding box in pixels.
-    val padding: Flow<Int> =
-        interactor.scaleForResolution.map { scale ->
-            (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale)
-                .roundToInt()
-        }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt
deleted file mode 100644
index d894a11..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.viewmodel
-
-import com.android.systemui.biometrics.UdfpsKeyguardAccessibilityDelegate
-import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-@ExperimentalCoroutinesApi
-class UdfpsKeyguardInternalViewModel
-@Inject
-constructor(val accessibilityDelegate: UdfpsKeyguardAccessibilityDelegate)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt
deleted file mode 100644
index dca151d..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.viewmodel
-
-import android.graphics.Rect
-import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-@ExperimentalCoroutinesApi
-class UdfpsKeyguardViewModel @Inject constructor() {
-    var sensorBounds: Rect = Rect()
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt
deleted file mode 100644
index 098b481..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.android.systemui.keyguard.ui.viewmodel
-
-import android.graphics.Rect
-import com.android.systemui.biometrics.UdfpsKeyguardView
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.ui.binder.UdfpsKeyguardViewBinder
-import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-@ExperimentalCoroutinesApi
-@SysUISingleton
-class UdfpsKeyguardViewModels
-@Inject
-constructor(
-    private val viewModel: UdfpsKeyguardViewModel,
-    private val internalViewModel: UdfpsKeyguardInternalViewModel,
-    private val aodViewModel: UdfpsAodViewModel,
-    private val lockscreenFingerprintViewModel: FingerprintViewModel,
-    private val lockscreenBackgroundViewModel: BackgroundViewModel,
-) {
-
-    fun setSensorBounds(sensorBounds: Rect) {
-        viewModel.sensorBounds = sensorBounds
-    }
-
-    fun bindViews(view: UdfpsKeyguardView) {
-        UdfpsKeyguardViewBinder.bind(
-            view,
-            viewModel,
-            internalViewModel,
-            aodViewModel,
-            lockscreenFingerprintViewModel,
-            lockscreenBackgroundViewModel
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt
deleted file mode 100644
index 642904d..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.viewmodel
-
-import android.content.Context
-import androidx.annotation.ColorInt
-import com.android.settingslib.Utils.getColorAttrDefaultColor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.Offsets
-import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.StatusBarState
-import com.android.systemui.res.R
-import com.android.wm.shell.animation.Interpolators
-import javax.inject.Inject
-import kotlin.math.roundToInt
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-
-/** View-model for UDFPS lockscreen views. */
-@ExperimentalCoroutinesApi
-open class UdfpsLockscreenViewModel(
-    context: Context,
-    lockscreenColorResId: Int,
-    alternateBouncerColorResId: Int,
-    transitionInteractor: KeyguardTransitionInteractor,
-    udfpsKeyguardInteractor: UdfpsKeyguardInteractor,
-    keyguardInteractor: KeyguardInteractor,
-) {
-    private val toLockscreen: Flow<TransitionViewModel> =
-        transitionInteractor.anyStateToLockscreenTransition.map {
-            TransitionViewModel(
-                alpha =
-                    if (it.from == KeyguardState.AOD) {
-                        it.value // animate
-                    } else {
-                        1f
-                    },
-                scale = 1f,
-                color = getColorAttrDefaultColor(context, lockscreenColorResId),
-            )
-        }
-
-    private val toAlternateBouncer: Flow<TransitionViewModel> =
-        keyguardInteractor.statusBarState.flatMapLatest { statusBarState ->
-            transitionInteractor.transitionStepsToState(KeyguardState.ALTERNATE_BOUNCER).map {
-                TransitionViewModel(
-                    alpha = 1f,
-                    scale =
-                        if (visibleInKeyguardState(it.from, statusBarState)) {
-                            1f
-                        } else {
-                            Interpolators.FAST_OUT_SLOW_IN.getInterpolation(it.value)
-                        },
-                    color = getColorAttrDefaultColor(context, alternateBouncerColorResId),
-                )
-            }
-        }
-
-    private val fadeOut: Flow<TransitionViewModel> =
-        keyguardInteractor.statusBarState.flatMapLatest { statusBarState ->
-            merge(
-                    transitionInteractor.anyStateToGoneTransition,
-                    transitionInteractor.anyStateToAodTransition,
-                    transitionInteractor.anyStateToOccludedTransition,
-                    transitionInteractor.anyStateToPrimaryBouncerTransition,
-                    transitionInteractor.anyStateToDreamingTransition,
-                )
-                .map {
-                    TransitionViewModel(
-                        alpha =
-                            if (visibleInKeyguardState(it.from, statusBarState)) {
-                                1f - it.value
-                            } else {
-                                0f
-                            },
-                        scale = 1f,
-                        color =
-                            if (it.from == KeyguardState.ALTERNATE_BOUNCER) {
-                                getColorAttrDefaultColor(context, alternateBouncerColorResId)
-                            } else {
-                                getColorAttrDefaultColor(context, lockscreenColorResId)
-                            },
-                    )
-                }
-        }
-
-    private fun visibleInKeyguardState(
-        state: KeyguardState,
-        statusBarState: StatusBarState
-    ): Boolean {
-        return when (state) {
-            KeyguardState.OFF,
-            KeyguardState.DOZING,
-            KeyguardState.DREAMING,
-            KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
-            KeyguardState.AOD,
-            KeyguardState.PRIMARY_BOUNCER,
-            KeyguardState.GONE,
-            KeyguardState.OCCLUDED -> false
-            KeyguardState.LOCKSCREEN -> statusBarState == StatusBarState.KEYGUARD
-            KeyguardState.ALTERNATE_BOUNCER -> true
-        }
-    }
-
-    private val keyguardStateTransition =
-        merge(
-            toAlternateBouncer,
-            toLockscreen,
-            fadeOut,
-        )
-
-    private val dialogHideAffordancesAlphaMultiplier: Flow<Float> =
-        udfpsKeyguardInteractor.dialogHideAffordancesRequest.map { hideAffordances ->
-            if (hideAffordances) {
-                0f
-            } else {
-                1f
-            }
-        }
-
-    private val alphaMultiplier: Flow<Float> =
-        combine(
-            transitionInteractor.startedKeyguardState,
-            dialogHideAffordancesAlphaMultiplier,
-            udfpsKeyguardInteractor.shadeExpansion,
-            udfpsKeyguardInteractor.qsProgress,
-        ) { startedKeyguardState, dialogHideAffordancesAlphaMultiplier, shadeExpansion, qsProgress
-            ->
-            if (startedKeyguardState == KeyguardState.ALTERNATE_BOUNCER) {
-                1f
-            } else {
-                dialogHideAffordancesAlphaMultiplier * (1f - shadeExpansion) * (1f - qsProgress)
-            }
-        }
-
-    val transition: Flow<TransitionViewModel> =
-        combine(
-            alphaMultiplier,
-            keyguardStateTransition,
-        ) { alphaMultiplier, keyguardStateTransition ->
-            TransitionViewModel(
-                alpha = keyguardStateTransition.alpha * alphaMultiplier,
-                scale = keyguardStateTransition.scale,
-                color = keyguardStateTransition.color,
-            )
-        }
-    val visible: Flow<Boolean> = transition.map { it.alpha != 0f }
-}
-
-@ExperimentalCoroutinesApi
-class FingerprintViewModel
-@Inject
-constructor(
-    val context: Context,
-    transitionInteractor: KeyguardTransitionInteractor,
-    interactor: UdfpsKeyguardInteractor,
-    keyguardInteractor: KeyguardInteractor,
-) :
-    UdfpsLockscreenViewModel(
-        context,
-        android.R.attr.textColorPrimary,
-        com.android.internal.R.attr.materialColorOnPrimaryFixed,
-        transitionInteractor,
-        interactor,
-        keyguardInteractor,
-    ) {
-    val dozeAmount: Flow<Float> = interactor.dozeAmount
-    val burnInOffsets: Flow<Offsets> = interactor.burnInOffsets
-
-    // Padding between the fingerprint icon and its bounding box in pixels.
-    val padding: Flow<Int> =
-        interactor.scaleForResolution.map { scale ->
-            (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale)
-                .roundToInt()
-        }
-}
-
-@ExperimentalCoroutinesApi
-class BackgroundViewModel
-@Inject
-constructor(
-    val context: Context,
-    transitionInteractor: KeyguardTransitionInteractor,
-    interactor: UdfpsKeyguardInteractor,
-    keyguardInteractor: KeyguardInteractor,
-) :
-    UdfpsLockscreenViewModel(
-        context,
-        com.android.internal.R.attr.colorSurface,
-        com.android.internal.R.attr.materialColorPrimaryFixed,
-        transitionInteractor,
-        interactor,
-        keyguardInteractor,
-    )
-
-data class TransitionViewModel(
-    val alpha: Float,
-    val scale: Float,
-    @ColorInt val color: Int,
-)
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
index a99c51c2..be93936 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -405,12 +405,15 @@
         }
         widgetGroupIds.forEach { id ->
             squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha =
-                    calculateAlpha(
-                        squishFraction,
-                        startPosition / nonsquishedHeight,
-                        endPosition / nonsquishedHeight
-                    )
+                // Don't modify alpha for elements that should be invisible (e.g. disabled seekbar)
+                if (state.alpha != 0f) {
+                    state.alpha =
+                        calculateAlpha(
+                            squishFraction,
+                            startPosition / nonsquishedHeight,
+                            endPosition / nonsquishedHeight
+                        )
+                }
             }
         }
         return groupTop // used for the widget group above this group
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaInSceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaInSceneContainerFlag.kt
index 898298c..77279f2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaInSceneContainerFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaInSceneContainerFlag.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media.controls.util
 
 import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the media_in_scene_container flag state. */
@@ -25,6 +26,10 @@
     /** The aconfig flag name */
     const val FLAG_NAME = Flags.FLAG_MEDIA_IN_SCENE_CONTAINER
 
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
     /** Is the flag enabled? */
     @JvmStatic
     inline val isEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 0e7e69b..270bfbe 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -263,11 +263,7 @@
      * If the shortcut entry `android:enabled` is set to `true`, the shortcut will be visible in the
      * Widget Picker to all users.
      */
-    // TODO(b/316332684)
-    @Suppress("UNREACHABLE_CODE")
     fun setNoteTaskShortcutEnabled(value: Boolean, user: UserHandle) {
-        return // shortcut should not be enabled until additional features are implemented.
-
         if (!userManager.isUserUnlocked(user)) {
             debugLog { "setNoteTaskShortcutEnabled call but user locked: user=$user" }
             return
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
index 1ab64b7..ba3357c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
@@ -17,12 +17,10 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
-import android.os.Build;
 import android.provider.Settings;
 
-import com.android.systemui.res.R;
 import com.android.systemui.plugins.qs.QSTile;
-import com.android.systemui.util.leak.GarbageMonitor;
+import com.android.systemui.res.R;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -44,10 +42,6 @@
         final String defaultTileList = res.getString(R.string.quick_settings_tiles_default);
 
         tiles.addAll(Arrays.asList(defaultTileList.split(",")));
-        if (Build.IS_DEBUGGABLE
-                && GarbageMonitor.ADD_MEMORY_TILE_TO_DEFAULT_ON_DEBUGGABLE_BUILDS) {
-            tiles.add(GarbageMonitor.MemoryTile.TILE_SPEC);
-        }
         return tiles;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index 2af7ae0..47b0624 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -23,7 +23,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.provider.Settings;
 import android.service.quicksettings.Tile;
 import android.service.quicksettings.TileService;
@@ -33,7 +32,6 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.systemui.res.R;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.qs.QSTile;
@@ -42,8 +40,8 @@
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.external.CustomTile;
 import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.util.leak.GarbageMonitor;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -114,9 +112,6 @@
                 possibleTiles.add(spec);
             }
         }
-        if (Build.IS_DEBUGGABLE && !current.contains(GarbageMonitor.MemoryTile.TILE_SPEC)) {
-            possibleTiles.add(GarbageMonitor.MemoryTile.TILE_SPEC);
-        }
 
         final ArrayList<QSTile> tilesToAdd = new ArrayList<>();
         possibleTiles.remove("cell");
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index 17e6375..bdcbac0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -14,7 +14,6 @@
 
 package com.android.systemui.qs.tileimpl;
 
-import android.os.Build;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
@@ -25,15 +24,14 @@
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.external.CustomTile;
-import com.android.systemui.util.leak.GarbageMonitor;
-
-import dagger.Lazy;
 
 import java.util.Map;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
 
+import dagger.Lazy;
+
 /**
  * A factory that creates Quick Settings tiles based on a tileSpec
  *
@@ -79,9 +77,7 @@
     @Nullable
     protected QSTileImpl createTileInternal(String tileSpec) {
         // Stock tiles.
-        if (mTileMap.containsKey(tileSpec)
-                // We should not return a Garbage Monitory Tile if the build is not Debuggable
-                && (!tileSpec.equals(GarbageMonitor.MemoryTile.TILE_SPEC) || Build.IS_DEBUGGABLE)) {
+        if (mTileMap.containsKey(tileSpec)) {
             return mTileMap.get(tileSpec).get();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt
index ca5302e..c932cee 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt
@@ -16,11 +16,15 @@
 
 package com.android.systemui.qs.tiles.impl.custom.data.repository
 
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
 import android.graphics.drawable.Icon
 import android.os.UserHandle
 import android.service.quicksettings.Tile
+import android.service.quicksettings.TileService
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.qs.external.CustomTileStatePersister
+import com.android.systemui.qs.external.PackageManagerAdapter
 import com.android.systemui.qs.external.TileServiceKey
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tiles.impl.custom.commons.copy
@@ -63,6 +67,12 @@
      */
     fun getTile(user: UserHandle): Tile?
 
+    /** @see [com.android.systemui.qs.external.TileLifecycleManager.isActiveTile] */
+    suspend fun isTileActive(): Boolean
+
+    /** @see [com.android.systemui.qs.external.TileLifecycleManager.isToggleableTile] */
+    suspend fun isTileToggleable(): Boolean
+
     /**
      * Updates tile with the non-null values from [newTile]. Overwrites the current cache when
      * [user] differs from the cached one. [isPersistable] tile will be persisted to be possibly
@@ -92,6 +102,7 @@
 constructor(
     private val tileSpec: TileSpec.CustomTileSpec,
     private val customTileStatePersister: CustomTileStatePersister,
+    private val packageManagerAdapter: PackageManagerAdapter,
     @Background private val backgroundContext: CoroutineContext,
 ) : CustomTileRepository {
 
@@ -149,6 +160,34 @@
         }
     }
 
+    override suspend fun isTileActive(): Boolean =
+        withContext(backgroundContext) {
+            try {
+                val info: ServiceInfo =
+                    packageManagerAdapter.getServiceInfo(
+                        tileSpec.componentName,
+                        META_DATA_QUERY_FLAGS
+                    )
+                info.metaData?.getBoolean(TileService.META_DATA_ACTIVE_TILE, false) == true
+            } catch (e: PackageManager.NameNotFoundException) {
+                false
+            }
+        }
+
+    override suspend fun isTileToggleable(): Boolean =
+        withContext(backgroundContext) {
+            try {
+                val info: ServiceInfo =
+                    packageManagerAdapter.getServiceInfo(
+                        tileSpec.componentName,
+                        META_DATA_QUERY_FLAGS
+                    )
+                info.metaData?.getBoolean(TileService.META_DATA_TOGGLEABLE_TILE, false) == true
+            } catch (e: PackageManager.NameNotFoundException) {
+                false
+            }
+        }
+
     private suspend fun updateTile(
         user: UserHandle,
         isPersistable: Boolean,
@@ -193,4 +232,12 @@
     private fun UserHandle.getKey() = TileServiceKey(tileSpec.componentName, this.identifier)
 
     private data class TileWithUser(val user: UserHandle, val tile: Tile)
+
+    private companion object {
+        const val META_DATA_QUERY_FLAGS =
+            (PackageManager.GET_META_DATA or
+                PackageManager.MATCH_UNINSTALLED_PACKAGES or
+                PackageManager.MATCH_DIRECT_BOOT_UNAWARE or
+                PackageManager.MATCH_DIRECT_BOOT_AWARE)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
index 351bba5..10b012d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
@@ -19,10 +19,9 @@
 import android.os.UserHandle
 import android.service.quicksettings.Tile
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.qs.external.TileServiceManager
 import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository
 import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileRepository
-import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundScope
+import com.android.systemui.qs.tiles.impl.di.QSTileScope
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
@@ -35,15 +34,13 @@
 import kotlinx.coroutines.flow.onEach
 
 /** Manages updates of the [Tile] assigned for the current custom tile. */
-@CustomTileBoundScope
+@QSTileScope
 class CustomTileInteractor
 @Inject
 constructor(
-    private val user: UserHandle,
     private val defaultsRepository: CustomTileDefaultsRepository,
     private val customTileRepository: CustomTileRepository,
-    private val tileServiceManager: TileServiceManager,
-    @CustomTileBoundScope private val boundScope: CoroutineScope,
+    @QSTileScope private val tileScope: CoroutineScope,
     @Background private val backgroundContext: CoroutineContext,
 ) {
 
@@ -51,8 +48,7 @@
         MutableSharedFlow<Tile>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
 
     /** [Tile] updates. [updateTile] to emit a new one. */
-    val tiles: Flow<Tile>
-        get() = customTileRepository.getTiles(user)
+    fun getTiles(user: UserHandle): Flow<Tile> = customTileRepository.getTiles(user)
 
     /**
      * Current [Tile]
@@ -61,10 +57,14 @@
      *   the tile hasn't been updated for the current user. Can happen when this is accessed before
      *   [init] returns.
      */
-    val tile: Tile
-        get() =
-            customTileRepository.getTile(user)
-                ?: throw IllegalStateException("Attempt to get a tile for a wrong user")
+    fun getTile(user: UserHandle): Tile =
+        customTileRepository.getTile(user)
+            ?: throw IllegalStateException("Attempt to get a tile for a wrong user")
+
+    /**
+     * True if the tile is toggleable like a switch and false if it operates as a clickable button.
+     */
+    suspend fun isTileToggleable(): Boolean = customTileRepository.isTileToggleable()
 
     /**
      * Initializes the repository for the current user. Suspends until it's safe to call [tile]
@@ -73,36 +73,36 @@
      * - receive tile update in [updateTile];
      * - restoration happened for a persisted tile.
      */
-    suspend fun init() {
-        launchUpdates()
-        customTileRepository.restoreForTheUserIfNeeded(user, tileServiceManager.isActiveTile)
+    suspend fun initForUser(user: UserHandle) {
+        launchUpdates(user)
+        customTileRepository.restoreForTheUserIfNeeded(user, customTileRepository.isTileActive())
         // Suspend to make sure it gets the tile from one of the sources: restoration, defaults, or
         // tile update.
         customTileRepository.getTiles(user).firstOrNull()
     }
 
-    private fun launchUpdates() {
+    private fun launchUpdates(user: UserHandle) {
         tileUpdates
             .onEach {
                 customTileRepository.updateWithTile(
                     user,
                     it,
-                    tileServiceManager.isActiveTile,
+                    customTileRepository.isTileActive(),
                 )
             }
             .flowOn(backgroundContext)
-            .launchIn(boundScope)
+            .launchIn(tileScope)
         defaultsRepository
             .defaults(user)
             .onEach {
                 customTileRepository.updateWithDefaults(
                     user,
                     it,
-                    tileServiceManager.isActiveTile,
+                    customTileRepository.isTileActive(),
                 )
             }
             .flowOn(backgroundContext)
-            .launchIn(boundScope)
+            .launchIn(tileScope)
     }
 
     /** Updates current [Tile]. Emits a new event in [tiles]. */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
index 580c421..ef3df48 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
@@ -22,8 +22,8 @@
 
 /**
  * Represents tiles behaviour logic. This ViewModel is a connection between tile view and data
- * layers. All direct inheritors must be added to the [QSTileViewModelInterfaceComplianceTest] class
- * to pass compliance tests.
+ * layers. All direct inheritors must be added to the
+ * [com.android.systemui.qs.tiles.viewmodel.QSTileViewModelTest] class to pass interface tests.
  *
  * All methods of this view model should be considered running on the main thread. This means no
  * synchronous long running operations are permitted in any method.
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
index 1156250..8c3e4a5 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
@@ -14,30 +14,94 @@
  * limitations under the License.
  */
 
+@file:Suppress("NOTHING_TO_INLINE")
+
 package com.android.systemui.scene.shared.flag
 
-import android.content.Context
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.Flags as AConfigFlags
+import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
 import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.Flags.sceneContainer
 import com.android.systemui.compose.ComposeFacade
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flag
-import com.android.systemui.flags.Flags
-import com.android.systemui.flags.ReleasedFlag
-import com.android.systemui.flags.ResourceBooleanFlag
-import com.android.systemui.flags.UnreleasedFlag
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.Flags.SCENE_CONTAINER_ENABLED
+import com.android.systemui.flags.RefactorFlagUtils
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.media.controls.util.MediaInSceneContainerFlag
-import com.android.systemui.res.R
 import dagger.Module
 import dagger.Provides
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
+
+/** Helper for reading or using the scene container flag state. */
+object SceneContainerFlag {
+    /** The flag description -- not an aconfig flag name */
+    const val DESCRIPTION = "SceneContainerFlag"
+
+    inline val isEnabled
+        get() =
+            SCENE_CONTAINER_ENABLED && // mainStaticFlag
+            sceneContainer() && // mainAconfigFlag
+                keyguardBottomAreaRefactor() &&
+                KeyguardShadeMigrationNssl.isEnabled &&
+                MediaInSceneContainerFlag.isEnabled &&
+                ComposeFacade.isComposeAvailable()
+
+    /**
+     * The main static flag, SCENE_CONTAINER_ENABLED. This is an explicit static flag check that
+     * helps with downstream optimizations (like unused code stripping) in builds where aconfig
+     * flags are still writable. Do not remove!
+     */
+    inline fun getMainStaticFlag() =
+        FlagToken("Flags.SCENE_CONTAINER_ENABLED", SCENE_CONTAINER_ENABLED)
+
+    /** The main aconfig flag. */
+    inline fun getMainAconfigFlag() = FlagToken(FLAG_SCENE_CONTAINER, sceneContainer())
+
+    /** The set of secondary flags which must be enabled for scene container to work properly */
+    inline fun getSecondaryFlags(): Sequence<FlagToken> =
+        sequenceOf(
+            FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()),
+            KeyguardShadeMigrationNssl.token,
+            MediaInSceneContainerFlag.token,
+        )
+
+    /** The full set of requirements for SceneContainer */
+    inline fun getAllRequirements(): Sequence<FlagToken> {
+        val composeRequirement =
+            FlagToken("ComposeFacade.isComposeAvailable()", ComposeFacade.isComposeAvailable())
+        return sequenceOf(getMainStaticFlag(), getMainAconfigFlag()) +
+            getSecondaryFlags() +
+            composeRequirement
+    }
+
+    /** Return all dependencies of this flag in pairs where [Pair.first] depends on [Pair.second] */
+    inline fun getFlagDependencies(): Sequence<Pair<FlagToken, FlagToken>> {
+        val mainStaticFlag = getMainStaticFlag()
+        val mainAconfigFlag = getMainAconfigFlag()
+        return sequence {
+            // The static and aconfig flags should be equal; make them co-dependent
+            yield(mainAconfigFlag to mainStaticFlag)
+            yield(mainStaticFlag to mainAconfigFlag)
+            // all other flags depend on the static flag for brevity
+        } + getSecondaryFlags().map { mainStaticFlag to it }
+    }
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, DESCRIPTION)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, DESCRIPTION)
+}
 
 /**
  * Defines interface for classes that can check whether the scene container framework feature is
@@ -52,133 +116,25 @@
     fun requirementDescription(): String
 }
 
-class SceneContainerFlagsImpl
-@AssistedInject
-constructor(
-    @Application private val context: Context,
-    private val featureFlagsClassic: FeatureFlagsClassic,
-    @Assisted private val isComposeAvailable: Boolean,
-) : SceneContainerFlags {
-
-    companion object {
-        @VisibleForTesting
-        val classicFlagTokens: List<Flag<Boolean>> =
-            listOf(
-                Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW,
-            )
-    }
-
-    /** The list of requirements, all must be met for the feature to be enabled. */
-    private val requirements =
-        listOf(
-            AconfigFlagMustBeEnabled(
-                flagName = AConfigFlags.FLAG_SCENE_CONTAINER,
-                flagValue = sceneContainer(),
-            ),
-            AconfigFlagMustBeEnabled(
-                flagName = AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
-                flagValue = keyguardBottomAreaRefactor(),
-            ),
-            AconfigFlagMustBeEnabled(
-                flagName = KeyguardShadeMigrationNssl.FLAG_NAME,
-                flagValue = KeyguardShadeMigrationNssl.isEnabled,
-            ),
-            AconfigFlagMustBeEnabled(
-                flagName = MediaInSceneContainerFlag.FLAG_NAME,
-                flagValue = MediaInSceneContainerFlag.isEnabled,
-            ),
-        ) +
-            classicFlagTokens.map { flagToken -> FlagMustBeEnabled(flagToken) } +
-            listOf(
-                ComposeMustBeAvailable(),
-                CompileTimeFlagMustBeEnabled(),
-                ResourceConfigMustBeEnabled()
-            )
+class SceneContainerFlagsImpl : SceneContainerFlags {
 
     override fun isEnabled(): Boolean {
-        // SCENE_CONTAINER_ENABLED is an explicit static flag check that helps with downstream
-        // optimizations, e.g., unused code stripping. Do not remove!
-        return Flags.SCENE_CONTAINER_ENABLED && requirements.all { it.isMet() }
+        return SceneContainerFlag.isEnabled
     }
 
     override fun requirementDescription(): String {
         return buildString {
-            requirements.forEach { requirement ->
+            SceneContainerFlag.getAllRequirements().forEach { requirement ->
                 append('\n')
-                append(if (requirement.isMet()) "    [MET]" else "[NOT MET]")
+                append(if (requirement.isEnabled) "    [MET]" else "[NOT MET]")
                 append(" ${requirement.name}")
             }
         }
     }
-
-    private interface Requirement {
-        val name: String
-
-        fun isMet(): Boolean
-    }
-
-    private inner class ComposeMustBeAvailable : Requirement {
-        override val name = "Jetpack Compose must be available"
-
-        override fun isMet(): Boolean {
-            return isComposeAvailable
-        }
-    }
-
-    private inner class CompileTimeFlagMustBeEnabled : Requirement {
-        override val name = "Flags.SCENE_CONTAINER_ENABLED must be enabled in code"
-
-        override fun isMet(): Boolean {
-            return Flags.SCENE_CONTAINER_ENABLED
-        }
-    }
-
-    private inner class FlagMustBeEnabled<FlagType : Flag<*>>(
-        private val flag: FlagType,
-    ) : Requirement {
-        override val name = "Flag ${flag.name} must be enabled"
-
-        override fun isMet(): Boolean {
-            return when (flag) {
-                is ResourceBooleanFlag -> featureFlagsClassic.isEnabled(flag)
-                is ReleasedFlag -> featureFlagsClassic.isEnabled(flag)
-                is UnreleasedFlag -> featureFlagsClassic.isEnabled(flag)
-                else -> error("Unsupported flag type ${flag.javaClass}")
-            }
-        }
-    }
-
-    private inner class AconfigFlagMustBeEnabled(
-        flagName: String,
-        private val flagValue: Boolean,
-    ) : Requirement {
-        override val name: String = "Aconfig flag $flagName must be enabled"
-
-        override fun isMet(): Boolean {
-            return flagValue
-        }
-    }
-
-    private inner class ResourceConfigMustBeEnabled : Requirement {
-        override val name: String = "R.bool.config_sceneContainerFrameworkEnabled must be true"
-
-        override fun isMet(): Boolean {
-            return context.resources.getBoolean(R.bool.config_sceneContainerFrameworkEnabled)
-        }
-    }
-
-    @AssistedFactory
-    interface Factory {
-        fun create(isComposeAvailable: Boolean): SceneContainerFlagsImpl
-    }
 }
 
 @Module
 object SceneContainerFlagsModule {
 
-    @Provides
-    @SysUISingleton
-    fun impl(factory: SceneContainerFlagsImpl.Factory): SceneContainerFlags {
-        return factory.create(ComposeFacade.isComposeAvailable())
-    }
+    @Provides @SysUISingleton fun impl(): SceneContainerFlags = SceneContainerFlagsImpl()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
index 5cbea90..7130fa1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
@@ -11,8 +11,6 @@
 import android.view.animation.AccelerateDecelerateInterpolator
 import androidx.constraintlayout.widget.Guideline
 import com.android.systemui.res.R
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import javax.inject.Inject
 
 /**
@@ -23,7 +21,6 @@
 constructor(
     private val workProfileMessageController: WorkProfileMessageController,
     private val screenshotDetectionController: ScreenshotDetectionController,
-    private val featureFlags: FeatureFlags,
 ) {
     private lateinit var container: ViewGroup
     private lateinit var guideline: Guideline
@@ -63,10 +60,8 @@
 
     fun onScreenshotTaken(screenshot: ScreenshotData) {
         val workProfileData = workProfileMessageController.onScreenshotTaken(screenshot.userHandle)
-        var notifiedApps: List<CharSequence> = listOf()
-        if (featureFlags.isEnabled(Flags.SCREENSHOT_DETECTION)) {
-            notifiedApps = screenshotDetectionController.maybeNotifyOfScreenshot(screenshot)
-        }
+        var notifiedApps: List<CharSequence> =
+            screenshotDetectionController.maybeNotifyOfScreenshot(screenshot)
 
         // If work profile first run needs to show, bias towards that, otherwise show screenshot
         // detection notification if needed.
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 95f7c94a..878e6fa 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -1096,7 +1096,7 @@
             }
 
             @Override
-            public void onPulseExpansionChanged(boolean expandingChanged) {
+            public void onPulseExpansionAmountChanged(boolean expandingChanged) {
                 if (mKeyguardBypassController.getBypassEnabled()) {
                     // Position the notifications while dragging down while pulsing
                     requestScrollerTopPaddingUpdate(false /* animate */);
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 8d1e8d0..0c67279 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -20,9 +20,9 @@
 import android.view.animation.Interpolator
 import androidx.annotation.VisibleForTesting
 import androidx.core.animation.ObjectAnimator
-import com.android.systemui.Dumpable
 import com.android.app.animation.Interpolators
 import com.android.app.animation.InterpolatorsAndroidX
+import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -31,6 +31,7 @@
 import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
 import com.android.systemui.statusbar.phone.DozeParameters
@@ -206,8 +207,15 @@
             val nowExpanding = isPulseExpanding()
             val changed = nowExpanding != pulseExpanding
             pulseExpanding = nowExpanding
-            for (listener in wakeUpListeners) {
-                listener.onPulseExpansionChanged(changed)
+            if (!NotificationIconContainerRefactor.isEnabled) {
+                for (listener in wakeUpListeners) {
+                    listener.onPulseExpansionAmountChanged(changed)
+                }
+            }
+            if (changed) {
+                for (listener in wakeUpListeners) {
+                    listener.onPulseExpandingChanged(pulseExpanding)
+                }
             }
         }
     }
@@ -620,13 +628,20 @@
          *
          * @param expandingChanged if the user has started or stopped expanding
          */
-        fun onPulseExpansionChanged(expandingChanged: Boolean) {}
+        @Deprecated(
+            message = "Use onPulseExpandedChanged instead.",
+            replaceWith = ReplaceWith("onPulseExpandedChanged"),
+        )
+        fun onPulseExpansionAmountChanged(expandingChanged: Boolean) {}
 
         /**
          * Called when the animator started by [scheduleDelayedDozeAmountAnimation] begins running
          * after the start delay, or after it ends/is cancelled.
          */
         fun onDelayedDozeAmountAnimationRunning(running: Boolean) {}
+
+        /** Called whenever a pulse has started or stopped expanding. */
+        fun onPulseExpandingChanged(isPulseExpanding: Boolean) {}
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
index 3e9c6fb..3b48b39 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -3,10 +3,8 @@
 import android.util.FloatProperty
 import android.view.View
 import androidx.annotation.FloatRange
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.flags.RefactorFlag
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation
 import com.android.systemui.statusbar.notification.stack.AnimationProperties
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
 import kotlin.math.abs
@@ -42,13 +40,13 @@
     /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */
     val topCornerRadius: Float
         get() =
-            if (roundableState.newHeadsUpAnim.isEnabled) roundableState.topCornerRadius
+            if (NotificationsImprovedHunAnimation.isEnabled) roundableState.topCornerRadius
             else topRoundness * maxRadius
 
     /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
     val bottomCornerRadius: Float
         get() =
-            if (roundableState.newHeadsUpAnim.isEnabled) roundableState.bottomCornerRadius
+            if (NotificationsImprovedHunAnimation.isEnabled) roundableState.bottomCornerRadius
             else bottomRoundness * maxRadius
 
     /** Get and update the current radii */
@@ -318,13 +316,10 @@
     internal val targetView: View,
     private val roundable: Roundable,
     maxRadius: Float,
-    featureFlags: FeatureFlags? = null
 ) {
     internal var maxRadius = maxRadius
         private set
 
-    internal val newHeadsUpAnim = RefactorFlag.forView(Flags.IMPROVED_HUN_ANIMATIONS, featureFlags)
-
     /** Animatable for top roundness */
     private val topAnimatable = topAnimatable(roundable)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt
index cf03d1c..2cc1403 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt
@@ -62,8 +62,8 @@
     override val isPulseExpanding: Flow<Boolean> = conflatedCallbackFlow {
         val listener =
             object : NotificationWakeUpCoordinator.WakeUpListener {
-                override fun onPulseExpansionChanged(expandingChanged: Boolean) {
-                    trySend(expandingChanged)
+                override fun onPulseExpandingChanged(isPulseExpanding: Boolean) {
+                    trySend(isPulseExpanding)
                 }
             }
         trySend(wakeUpCoordinator.isPulseExpanding())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
index 61f9be5..76e5fd3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
@@ -112,15 +112,6 @@
         aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
         aodIcon.setIncreasedSize(true)
 
-        // Construct the centered icon view.
-        val centeredIcon = if (entry.sbn.notification.isMediaNotification) {
-            iconBuilder.createIconView(entry).apply {
-                scaleType = ImageView.ScaleType.CENTER_INSIDE
-            }
-        } else {
-            null
-        }
-
         // Set the icon views' icons
         val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
 
@@ -128,10 +119,7 @@
             setIcon(entry, normalIconDescriptor, sbIcon)
             setIcon(entry, sensitiveIconDescriptor, shelfIcon)
             setIcon(entry, sensitiveIconDescriptor, aodIcon)
-            if (centeredIcon != null) {
-                setIcon(entry, normalIconDescriptor, centeredIcon)
-            }
-            entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, centeredIcon, entry.icons)
+            entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, entry.icons)
         } catch (e: InflationException) {
             entry.icons = IconPack.buildEmptyPack(entry.icons)
             throw e
@@ -171,11 +159,6 @@
             it.setNotification(entry.sbn, notificationContentDescription)
             setIcon(entry, sensitiveIconDescriptor, it)
         }
-
-        entry.icons.centeredIcon?.let {
-            it.setNotification(entry.sbn, notificationContentDescription)
-            setIcon(entry, sensitiveIconDescriptor, it)
-        }
     }
 
     private fun updateIconsSafe(entry: NotificationEntry) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
index 054e381..442c097 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
@@ -31,7 +31,6 @@
     @Nullable private final StatusBarIconView mStatusBarIcon;
     @Nullable private final StatusBarIconView mShelfIcon;
     @Nullable private final StatusBarIconView mAodIcon;
-    @Nullable private final StatusBarIconView mCenteredIcon;
 
     @Nullable private StatusBarIcon mSmallIconDescriptor;
     @Nullable private StatusBarIcon mPeopleAvatarDescriptor;
@@ -43,7 +42,7 @@
      * haven't been inflated yet or there was an error while inflating them).
      */
     public static IconPack buildEmptyPack(@Nullable IconPack fromSource) {
-        return new IconPack(false, null, null, null, null, fromSource);
+        return new IconPack(false, null, null, null, fromSource);
     }
 
     /**
@@ -53,9 +52,8 @@
             @NonNull StatusBarIconView statusBarIcon,
             @NonNull StatusBarIconView shelfIcon,
             @NonNull StatusBarIconView aodIcon,
-            @Nullable StatusBarIconView centeredIcon,
             @Nullable IconPack source) {
-        return new IconPack(true, statusBarIcon, shelfIcon, aodIcon, centeredIcon, source);
+        return new IconPack(true, statusBarIcon, shelfIcon, aodIcon, source);
     }
 
     private IconPack(
@@ -63,12 +61,10 @@
             @Nullable StatusBarIconView statusBarIcon,
             @Nullable StatusBarIconView shelfIcon,
             @Nullable StatusBarIconView aodIcon,
-            @Nullable StatusBarIconView centeredIcon,
             @Nullable IconPack source) {
         mAreIconsAvailable = areIconsAvailable;
         mStatusBarIcon = statusBarIcon;
         mShelfIcon = shelfIcon;
-        mCenteredIcon = centeredIcon;
         mAodIcon = aodIcon;
         if (source != null) {
             mIsImportantConversation = source.mIsImportantConversation;
@@ -91,11 +87,6 @@
         return mShelfIcon;
     }
 
-    @Nullable
-    public StatusBarIconView getCenteredIcon() {
-        return mCenteredIcon;
-    }
-
     /** The version of the icon that's shown when pulsing (in AOD). */
     @Nullable
     public StatusBarIconView getAodIcon() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt
new file mode 100644
index 0000000..d7c29f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.icon.ui.viewbinder
+
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.collection.NotifCollection
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.launch
+
+/** Binds a [NotificationIconContainer] to a [NotificationIconContainerAlwaysOnDisplayViewModel]. */
+class NotificationIconContainerAlwaysOnDisplayViewBinder
+@Inject
+constructor(
+    private val viewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
+    private val keyguardRootViewModel: KeyguardRootViewModel,
+    private val configuration: ConfigurationState,
+    private val failureTracker: StatusBarIconViewBindingFailureTracker,
+    private val screenOffAnimationController: ScreenOffAnimationController,
+    private val systemBarUtilsState: SystemBarUtilsState,
+    private val viewStore: AlwaysOnDisplayNotificationIconViewStore,
+) {
+    fun bindWhileAttached(view: NotificationIconContainer): DisposableHandle {
+        return view.repeatWhenAttached {
+            lifecycleScope.launch {
+                launch {
+                    NotificationIconContainerViewBinder.bind(
+                        view = view,
+                        viewModel = viewModel,
+                        configuration = configuration,
+                        systemBarUtilsState = systemBarUtilsState,
+                        failureTracker = failureTracker,
+                        viewStore = viewStore,
+                    )
+                }
+                launch {
+                    KeyguardRootViewBinder.bindAodNotifIconVisibility(
+                        view = view,
+                        isVisible = keyguardRootViewModel.isNotifIconContainerVisible,
+                        configuration = configuration,
+                        screenOffAnimationController = screenOffAnimationController,
+                    )
+                }
+            }
+        }
+    }
+}
+
+/** [IconViewStore] for the always-on display. */
+class AlwaysOnDisplayNotificationIconViewStore
+@Inject
+constructor(notifCollection: NotifCollection) :
+    IconViewStore by (notifCollection.iconViewStoreBy { it.aodIcon })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt
new file mode 100644
index 0000000..783488af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.icon.ui.viewbinder
+
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.statusbar.notification.collection.NotifCollection
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.bindIcons
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import javax.inject.Inject
+
+/** Binds a [NotificationIconContainer] to a [NotificationIconContainerShelfViewModel]. */
+class NotificationIconContainerShelfViewBinder
+@Inject
+constructor(
+    private val viewModel: NotificationIconContainerShelfViewModel,
+    private val configuration: ConfigurationState,
+    private val systemBarUtilsState: SystemBarUtilsState,
+    private val failureTracker: StatusBarIconViewBindingFailureTracker,
+    private val viewStore: ShelfNotificationIconViewStore,
+) {
+    suspend fun bind(view: NotificationIconContainer) {
+        viewModel.icons.bindIcons(
+            view,
+            configuration,
+            systemBarUtilsState,
+            notifyBindingFailures = { failureTracker.shelfFailures = it },
+            viewStore,
+        )
+    }
+}
+
+/** [IconViewStore] for the [com.android.systemui.statusbar.NotificationShelf] */
+class ShelfNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
+    IconViewStore by (notifCollection.iconViewStoreBy { it.shelfIcon })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt
new file mode 100644
index 0000000..8e089b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.icon.ui.viewbinder
+
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.collection.NotifCollection
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.launch
+
+/** Binds a [NotificationIconContainer] to a [NotificationIconContainerStatusBarViewModel]. */
+class NotificationIconContainerStatusBarViewBinder
+@Inject
+constructor(
+    private val viewModel: NotificationIconContainerStatusBarViewModel,
+    private val configuration: ConfigurationState,
+    private val systemBarUtilsState: SystemBarUtilsState,
+    private val failureTracker: StatusBarIconViewBindingFailureTracker,
+    private val viewStore: StatusBarNotificationIconViewStore,
+) {
+    fun bindWhileAttached(view: NotificationIconContainer): DisposableHandle {
+        return view.repeatWhenAttached {
+            lifecycleScope.launch {
+                NotificationIconContainerViewBinder.bind(
+                    view = view,
+                    viewModel = viewModel,
+                    configuration = configuration,
+                    systemBarUtilsState = systemBarUtilsState,
+                    failureTracker = failureTracker,
+                    viewStore = viewStore,
+                )
+            }
+        }
+    }
+}
+
+/** [IconViewStore] for the status bar. */
+class StatusBarNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
+    IconViewStore by (notifCollection.iconViewStoreBy { it.statusBarIcon })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index e1e30e1..8fe0022 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -35,7 +35,6 @@
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconColors
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData.LimitType
@@ -45,7 +44,6 @@
 import com.android.systemui.util.ui.isAnimating
 import com.android.systemui.util.ui.stopAnimating
 import com.android.systemui.util.ui.value
-import javax.inject.Inject
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.coroutineScope
@@ -56,42 +54,6 @@
 
 /** Binds a view-model to a [NotificationIconContainer]. */
 object NotificationIconContainerViewBinder {
-    @JvmStatic
-    fun bindWhileAttached(
-        view: NotificationIconContainer,
-        viewModel: NotificationIconContainerShelfViewModel,
-        configuration: ConfigurationState,
-        systemBarUtilsState: SystemBarUtilsState,
-        failureTracker: StatusBarIconViewBindingFailureTracker,
-        viewStore: IconViewStore,
-    ): DisposableHandle {
-        return view.repeatWhenAttached {
-            lifecycleScope.launch {
-                viewModel.icons.bindIcons(
-                    view,
-                    configuration,
-                    systemBarUtilsState,
-                    notifyBindingFailures = { failureTracker.shelfFailures = it },
-                    viewStore,
-                )
-            }
-        }
-    }
-
-    @JvmStatic
-    fun bindWhileAttached(
-        view: NotificationIconContainer,
-        viewModel: NotificationIconContainerStatusBarViewModel,
-        configuration: ConfigurationState,
-        systemBarUtilsState: SystemBarUtilsState,
-        failureTracker: StatusBarIconViewBindingFailureTracker,
-        viewStore: IconViewStore,
-    ): DisposableHandle =
-        view.repeatWhenAttached {
-            lifecycleScope.launch {
-                bind(view, viewModel, configuration, systemBarUtilsState, failureTracker, viewStore)
-            }
-        }
 
     suspend fun bind(
         view: NotificationIconContainer,
@@ -215,7 +177,7 @@
      * given `iconKey`. The parent [Job] of this coroutine will be cancelled automatically when the
      * view is to be unbound.
      */
-    private suspend fun Flow<NotificationIconsViewData>.bindIcons(
+    suspend fun Flow<NotificationIconsViewData>.bindIcons(
         view: NotificationIconContainer,
         configuration: ConfigurationState,
         systemBarUtilsState: SystemBarUtilsState,
@@ -377,24 +339,14 @@
     }
 
     @ColorInt private const val DEFAULT_AOD_ICON_COLOR = Color.WHITE
-    private const val TAG =  "NotifIconContainerViewBinder"
+    private const val TAG = "NotifIconContainerViewBinder"
 }
 
-/** [IconViewStore] for the [com.android.systemui.statusbar.NotificationShelf] */
-class ShelfNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
-    IconViewStore by (notifCollection.iconViewStoreBy { it.shelfIcon })
-
-/** [IconViewStore] for the always-on display. */
-class AlwaysOnDisplayNotificationIconViewStore
-@Inject
-constructor(notifCollection: NotifCollection) :
-    IconViewStore by (notifCollection.iconViewStoreBy { it.aodIcon })
-
-/** [IconViewStore] for the status bar. */
-class StatusBarNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
-    IconViewStore by (notifCollection.iconViewStoreBy { it.statusBarIcon })
-
-private fun NotifCollection.iconViewStoreBy(block: (IconPack) -> StatusBarIconView?) =
+/**
+ * Convenience builder for [IconViewStore] that uses [block] to extract the relevant
+ * [StatusBarIconView] from an [IconPack] stored inside of the [NotifCollection].
+ */
+fun NotifCollection.iconViewStoreBy(block: (IconPack) -> StatusBarIconView?) =
     IconViewStore { key ->
         getEntry(key)?.icons?.let(block)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionRefactor.kt
index 2624363..db33f92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionRefactor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionRefactor.kt
@@ -17,12 +17,17 @@
 package com.android.systemui.statusbar.notification.interruption
 
 import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the visual interruptions refactor flag state. */
 object VisualInterruptionRefactor {
     const val FLAG_NAME = Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR
 
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
     /** Whether the refactor is enabled */
     @JvmStatic
     inline val isEnabled
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 4fe05ec..fca527f 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
@@ -31,7 +31,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.animation.Interpolator;
-import android.view.animation.PathInterpolator;
 
 import com.android.app.animation.Interpolators;
 import com.android.internal.jank.InteractionJankMonitor;
@@ -44,6 +43,7 @@
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.util.DumpUtilsKt;
@@ -67,7 +67,8 @@
      * The content of the view should start showing at animation progress value of
      * #ALPHA_APPEAR_START_FRACTION.
      */
-    private static final float ALPHA_APPEAR_START_FRACTION = .4f;
+
+    private static final float ALPHA_APPEAR_START_FRACTION = .7f;
     /**
      * The content should show fully with progress at #ALPHA_APPEAR_END_FRACTION
      * The start of the animation is at #ALPHA_APPEAR_START_FRACTION
@@ -86,9 +87,7 @@
      */
     private boolean mActivated;
 
-    private final Interpolator mSlowOutFastInInterpolator;
     private Interpolator mCurrentAppearInterpolator;
-
     NotificationBackgroundView mBackgroundNormal;
     private float mAnimationTranslationY;
     private boolean mDrawingAppearAnimation;
@@ -116,7 +115,6 @@
 
     public ActivatableNotificationView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f);
         setClipChildren(false);
         setClipToPadding(false);
         updateColors();
@@ -400,12 +398,16 @@
             mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN;
             targetValue = 1.0f;
         } else {
-            mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
+            mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE;
             targetValue = 0.0f;
         }
         mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
                 targetValue);
-        mAppearAnimator.setInterpolator(Interpolators.LINEAR);
+        if (NotificationsImprovedHunAnimation.isEnabled()) {
+            mAppearAnimator.setInterpolator(mCurrentAppearInterpolator);
+        } else {
+            mAppearAnimator.setInterpolator(Interpolators.LINEAR);
+        }
         mAppearAnimator.setDuration(
                 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue)));
         mAppearAnimator.addUpdateListener(animation -> {
@@ -502,8 +504,9 @@
     }
 
     private void updateAppearRect() {
-        float interpolatedFraction = mCurrentAppearInterpolator.getInterpolation(
-                mAppearAnimationFraction);
+        float interpolatedFraction =
+                NotificationsImprovedHunAnimation.isEnabled() ? mAppearAnimationFraction
+                        : mCurrentAppearInterpolator.getInterpolation(mAppearAnimationFraction);
         mAppearAnimationTranslation = (1.0f - interpolatedFraction) * mAnimationTranslationY;
         final int actualHeight = getActualHeight();
         float bottom = actualHeight * interpolatedFraction;
@@ -524,6 +527,7 @@
     }
 
     private float getInterpolatedAppearAnimationFraction() {
+
         if (mAppearAnimationFraction >= 0) {
             return mCurrentAppearInterpolator.getInterpolation(mAppearAnimationFraction);
         }
@@ -569,7 +573,7 @@
 
     @Override
     public float getTopCornerRadius() {
-        if (mImprovedHunAnimation.isEnabled()) {
+        if (NotificationsImprovedHunAnimation.isEnabled()) {
             return super.getTopCornerRadius();
         }
 
@@ -579,7 +583,7 @@
 
     @Override
     public float getBottomCornerRadius() {
-        if (mImprovedHunAnimation.isEnabled()) {
+        if (NotificationsImprovedHunAnimation.isEnabled()) {
             return super.getBottomCornerRadius();
         }
 
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 2a3e69b..aefd348 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,10 +28,9 @@
 import android.view.View;
 import android.view.ViewOutlineProvider;
 
-import com.android.systemui.flags.Flags;
-import com.android.systemui.flags.RefactorFlag;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.notification.RoundableState;
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
 import com.android.systemui.util.DumpUtilsKt;
 
@@ -49,8 +48,6 @@
     private float mOutlineAlpha = -1f;
     private boolean mAlwaysRoundBothCorners;
     private Path mTmpPath = new Path();
-    protected final RefactorFlag mImprovedHunAnimation =
-            RefactorFlag.forView(Flags.IMPROVED_HUN_ANIMATIONS);
 
     /**
      * {@code false} if the children views of the {@link ExpandableOutlineView} are translated when
@@ -126,7 +123,7 @@
             return EMPTY_PATH;
         }
         float bottomRadius = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius();
-        if (!mImprovedHunAnimation.isEnabled() && (topRadius + bottomRadius > height)) {
+        if (!NotificationsImprovedHunAnimation.isEnabled() && (topRadius + bottomRadius > height)) {
             float overShoot = topRadius + bottomRadius - height;
             float currentTopRoundness = getTopRoundness();
             float currentBottomRoundness = getBottomRoundness();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncHybridViewInflation.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncHybridViewInflation.kt
index 24e7f05..9556c2e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncHybridViewInflation.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncHybridViewInflation.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.row.shared
 
 import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the async hybrid view inflation flag state. */
@@ -24,6 +25,10 @@
 object AsyncHybridViewInflation {
     const val FLAG_NAME = Flags.FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION
 
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
     /** Is async hybrid (single-line) view inflation enabled */
     @JvmStatic
     inline val isEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsImprovedHunAnimation.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsImprovedHunAnimation.kt
new file mode 100644
index 0000000..16d35fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsImprovedHunAnimation.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the notifications improved hun animation flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationsImprovedHunAnimation {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_IMPROVED_HUN_ANIMATION
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationsImprovedHunAnimation()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
index 699e140..5ab4d4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
@@ -16,60 +16,38 @@
 
 package com.android.systemui.statusbar.notification.shelf.ui.viewbinder
 
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.common.ui.ConfigurationState
-import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.NotificationShelf
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
 import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
-import com.android.systemui.statusbar.ui.SystemBarUtilsState
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 
 /** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */
 object NotificationShelfViewBinder {
-    fun bind(
+    suspend fun bind(
         shelf: NotificationShelf,
         viewModel: NotificationShelfViewModel,
-        configuration: ConfigurationState,
-        systemBarUtilsState: SystemBarUtilsState,
         falsingManager: FalsingManager,
-        iconViewBindingFailureTracker: StatusBarIconViewBindingFailureTracker,
+        nicBinder: NotificationIconContainerShelfViewBinder,
         notificationIconAreaController: NotificationIconAreaController,
-        shelfIconViewStore: ShelfNotificationIconViewStore,
-    ) {
+    ): Unit = coroutineScope {
         ActivatableNotificationViewBinder.bind(viewModel, shelf, falsingManager)
         shelf.apply {
             if (NotificationIconContainerRefactor.isEnabled) {
-                NotificationIconContainerViewBinder.bindWhileAttached(
-                    shelfIcons,
-                    viewModel.icons,
-                    configuration,
-                    systemBarUtilsState,
-                    iconViewBindingFailureTracker,
-                    shelfIconViewStore,
-                )
+                launch { nicBinder.bind(shelfIcons) }
             } else {
                 notificationIconAreaController.setShelfIcons(shelfIcons)
             }
-            repeatWhenAttached {
-                repeatOnLifecycle(Lifecycle.State.STARTED) {
-                    launch {
-                        viewModel.canModifyColorOfNotifications.collect(
-                            ::setCanModifyColorOfNotifications
-                        )
-                    }
-                    launch { viewModel.isClickable.collect(::setCanInteract) }
-                    registerViewListenersWhileAttached(shelf, viewModel)
-                }
+            launch {
+                viewModel.canModifyColorOfNotifications.collect(::setCanModifyColorOfNotifications)
             }
+            launch { viewModel.isClickable.collect(::setCanInteract) }
+            registerViewListenersWhileAttached(shelf, viewModel)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
index 64b5b62c..5ca8b53 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.NotificationShelf
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel
 import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor
 import javax.inject.Inject
@@ -32,7 +31,6 @@
 constructor(
     private val interactor: NotificationShelfInteractor,
     activatableViewModel: ActivatableNotificationViewModel,
-    val icons: NotificationIconContainerShelfViewModel,
 ) : ActivatableNotificationViewModel by activatableViewModel {
     /** Is the shelf allowed to be clickable when it has content? */
     val isClickable: Flow<Boolean>
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 3bbdfd1..62fdc29 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
@@ -94,9 +94,7 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.res.R;
-import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.TouchLogger;
-import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.StatusBarState;
@@ -109,12 +107,12 @@
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
-import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -146,8 +144,6 @@
     private static final String TAG = "StackScroller";
     private static final boolean SPEW = Log.isLoggable(TAG, Log.VERBOSE);
 
-    // Delay in milli-seconds before shade closes for clear all.
-    private static final int DELAY_BEFORE_SHADE_CLOSE = 200;
     private boolean mShadeNeedsToClose = false;
 
     @VisibleForTesting
@@ -319,7 +315,7 @@
         }
     };
     private NotificationStackScrollLogger mLogger;
-    private NotificationsController mNotificationsController;
+    private Runnable mResetUserExpandedStatesRunnable;
     private ActivityStarter mActivityStarter;
     private final int[] mTempInt2 = new int[2];
     private final HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>();
@@ -482,7 +478,7 @@
     private final Rect mTmpRect = new Rect();
     private ClearAllListener mClearAllListener;
     private ClearAllAnimationListener mClearAllAnimationListener;
-    private ShadeController mShadeController;
+    private Runnable mClearAllFinishedWhilePanelExpandedRunnable;
     private Consumer<Boolean> mOnStackYChanged;
 
     private Interpolator mHideXInterpolator = Interpolators.FAST_OUT_SLOW_IN;
@@ -3328,8 +3324,10 @@
                     logHunAnimationSkipped(row, "row has no viewState");
                     continue;
                 }
+                boolean shouldHunAppearFromTheBottom =
+                        mStackScrollAlgorithm.shouldHunAppearFromBottom(mAmbientState, viewState);
                 if (isHeadsUp && (mAddedHeadsUpChildren.contains(row) || pinnedAndClosed)) {
-                    if (pinnedAndClosed || shouldHunAppearFromBottom(viewState)) {
+                    if (pinnedAndClosed || shouldHunAppearFromTheBottom) {
                         // Our custom add animation
                         type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR;
                     } else {
@@ -3341,6 +3339,11 @@
             }
             AnimationEvent event = new AnimationEvent(row, type);
             event.headsUpFromBottom = onBottom;
+            if (NotificationsImprovedHunAnimation.isEnabled()) {
+                // TODO(b/283084712) remove this with the flag and update the HUN filters at
+                //  creation
+                event.filter.animateHeight = false;
+            }
             mAnimationEvents.add(event);
             if (SPEW) {
                 Log.v(TAG, "Generating HUN animation event: "
@@ -3355,11 +3358,6 @@
         mAddedHeadsUpChildren.clear();
     }
 
-    private boolean shouldHunAppearFromBottom(ExpandableViewState viewState) {
-        return viewState.getYTranslation() + viewState.height
-                >= mAmbientState.getMaxHeadsUpTranslation();
-    }
-
     private void generateGroupExpansionEvent() {
         // Generate a group expansion/collapsing event if there is such a group at all
         if (mExpandedGroupView != null) {
@@ -4114,7 +4112,7 @@
         mAmbientState.setExpansionChanging(false);
         if (!mIsExpanded) {
             resetScrollPosition();
-            mNotificationsController.resetUserExpandedStates();
+            mResetUserExpandedStatesRunnable.run();
             clearTemporaryViews();
             clearUserLockedViews();
             resetAllSwipeState();
@@ -4316,21 +4314,12 @@
             if (mShadeNeedsToClose) {
                 mShadeNeedsToClose = false;
                 if (mIsExpanded) {
-                    collapseShadeDelayed();
+                    mClearAllFinishedWhilePanelExpandedRunnable.run();
                 }
             }
         }
     }
 
-    private void collapseShadeDelayed() {
-        postDelayed(
-                () -> {
-                    mShadeController.animateCollapseShade(
-                            CommandQueue.FLAG_EXCLUDE_NONE);
-                },
-                DELAY_BEFORE_SHADE_CLOSE /* delayMillis */);
-    }
-
     private void clearHeadsUpDisappearRunning() {
         for (int i = 0; i < getChildCount(); i++) {
             View view = getChildAt(i);
@@ -4747,8 +4736,8 @@
         return max + getStackTranslation();
     }
 
-    public void setNotificationsController(NotificationsController notificationsController) {
-        this.mNotificationsController = notificationsController;
+    public void setResetUserExpandedStatesRunnable(Runnable runnable) {
+        this.mResetUserExpandedStatesRunnable = runnable;
     }
 
     public void setActivityStarter(ActivityStarter activityStarter) {
@@ -4932,7 +4921,9 @@
      */
     public void setHeadsUpBoundaries(int height, int bottomBarHeight) {
         mAmbientState.setMaxHeadsUpTranslation(height - bottomBarHeight);
+        mStackScrollAlgorithm.setHeadsUpAppearHeightBottom(height);
         mStateAnimator.setHeadsUpAppearHeightBottom(height);
+        mStateAnimator.setStackTopMargin(mAmbientState.getStackTopMargin());
         requestChildrenUpdate();
     }
 
@@ -5681,8 +5672,8 @@
         mFooterClearAllListener = listener;
     }
 
-    void setShadeController(ShadeController shadeController) {
-        mShadeController = shadeController;
+    void setClearAllFinishedWhilePanelExpandedRunnable(Runnable runnable) {
+        mClearAllFinishedWhilePanelExpandedRunnable = runnable;
     }
 
     /**
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 e6315fd..7c7d943 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
@@ -81,6 +81,7 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeViewController;
+import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener;
@@ -152,6 +153,8 @@
     private static final String TAG = "StackScrollerController";
     private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
     private static final String HIGH_PRIORITY = "high_priority";
+    /** Delay in milli-seconds before shade closes for clear all. */
+    private static final int DELAY_BEFORE_SHADE_CLOSE = 200;
 
     private final boolean mAllowLongPress;
     private final NotificationGutsManager mNotificationGutsManager;
@@ -754,7 +757,7 @@
         mView.setController(this);
         mView.setLogger(mLogger);
         mView.setTouchHandler(new TouchHandler());
-        mView.setNotificationsController(mNotificationsController);
+        mView.setResetUserExpandedStatesRunnable(mNotificationsController::resetUserExpandedStates);
         mView.setActivityStarter(mActivityStarter);
         mView.setClearAllAnimationListener(this::onAnimationEnd);
         mView.setClearAllListener((selection) -> mUiEventLogger.log(
@@ -770,7 +773,11 @@
                 mView.setIsRemoteInputActive(active);
             }
         });
-        mView.setShadeController(mShadeController);
+        mView.setClearAllFinishedWhilePanelExpandedRunnable(()-> {
+            final Runnable doCollapseRunnable = () ->
+                    mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE);
+            mView.postDelayed(doCollapseRunnable, /* delayMillis = */ DELAY_BEFORE_SHADE_CLOSE);
+        });
         mDumpManager.registerDumpable(mView);
 
         mKeyguardBypassController.registerOnBypassStateChangedListener(
@@ -852,7 +859,7 @@
         mGroupExpansionManager.registerGroupExpansionChangeListener(
                 (changedRow, expanded) -> mView.onGroupExpandChanged(changedRow, expanded));
 
-        mViewBinder.bind(mView, this);
+        mViewBinder.bindWhileAttached(mView, this);
 
         if (!FooterViewRefactor.isEnabled()) {
             collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
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 06ca9a5..c4e6b909 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
@@ -37,6 +37,7 @@
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -66,12 +67,15 @@
     private boolean mClipNotificationScrollToTop;
     @VisibleForTesting
     float mHeadsUpInset;
+    @VisibleForTesting
+    float mHeadsUpAppearStartAboveScreen;
     private int mPinnedZTranslationExtra;
     private float mNotificationScrimPadding;
     private int mMarginBottom;
     private float mQuickQsOffsetHeight;
     private float mSmallCornerRadius;
     private float mLargeCornerRadius;
+    private int mHeadsUpAppearHeightBottom;
 
     public StackScrollAlgorithm(
             Context context,
@@ -94,6 +98,8 @@
         int statusBarHeight = SystemBarUtils.getStatusBarHeight(context);
         mHeadsUpInset = statusBarHeight + res.getDimensionPixelSize(
                 R.dimen.heads_up_status_bar_padding);
+        mHeadsUpAppearStartAboveScreen = res.getDimensionPixelSize(
+                R.dimen.heads_up_appear_y_above_screen);
         mPinnedZTranslationExtra = res.getDimensionPixelSize(
                 R.dimen.heads_up_pinned_elevation);
         mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
@@ -221,6 +227,25 @@
         return getExpansionFractionWithoutShelf(mTempAlgorithmState, ambientState);
     }
 
+    public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
+        mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
+    }
+
+    /**
+     * If the QuickSettings is showing full screen, we want to animate the HeadsUp Notifications
+     * from the bottom of the screen.
+     *
+     * @param ambientState Current ambient state.
+     * @param viewState The state of the HUN that is being queried to appear from the bottom.
+     *
+     * @return true if the HeadsUp Notifications should appear from the bottom
+     */
+    public boolean shouldHunAppearFromBottom(AmbientState ambientState,
+            ExpandableViewState viewState) {
+        return viewState.getYTranslation() + viewState.height
+                >= ambientState.getMaxHeadsUpTranslation();
+    }
+
     public static void log(String s) {
         if (DEBUG) {
             android.util.Log.i(TAG, s);
@@ -793,10 +818,16 @@
                 }
             }
             if (row.isPinned()) {
-                // Make sure row yTranslation is at maximum the HUN yTranslation,
-                // which accounts for AmbientState.stackTopMargin in split-shade.
-                childState.setYTranslation(
-                        Math.max(childState.getYTranslation(), headsUpTranslation));
+                if (NotificationsImprovedHunAnimation.isEnabled()) {
+                    // Make sure row yTranslation is at the HUN yTranslation,
+                    // which accounts for AmbientState.stackTopMargin in split-shade.
+                    childState.setYTranslation(headsUpTranslation);
+                } else {
+                    // Make sure row yTranslation is at maximum the HUN yTranslation,
+                    // which accounts for AmbientState.stackTopMargin in split-shade.
+                    childState.setYTranslation(
+                            Math.max(childState.getYTranslation(), headsUpTranslation));
+                }
                 childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
                 childState.hidden = false;
                 ExpandableViewState topState =
@@ -819,10 +850,22 @@
                 }
             }
             if (row.isHeadsUpAnimatingAway()) {
-                // Make sure row yTranslation is at maximum the HUN yTranslation,
-                // which accounts for AmbientState.stackTopMargin in split-shade.
-                childState.setYTranslation(
-                        Math.max(childState.getYTranslation(), headsUpTranslation));
+                if (NotificationsImprovedHunAnimation.isEnabled()) {
+                    if (shouldHunAppearFromBottom(ambientState, childState)) {
+                        // move to the bottom of the screen
+                        childState.setYTranslation(
+                                mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen);
+                    } else {
+                        // move to the top of the screen
+                        childState.setYTranslation(-ambientState.getStackTopMargin()
+                                - mHeadsUpAppearStartAboveScreen);
+                    }
+                } else {
+                    // Make sure row yTranslation is at maximum the HUN yTranslation,
+                    // which accounts for AmbientState.stackTopMargin in split-shade.
+                    childState.setYTranslation(
+                            Math.max(childState.getYTranslation(), headsUpTranslation));
+                }
                 // keep it visible for the animation
                 childState.hidden = false;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
index e94258f..a3e0941 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack;
 
+import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK;
 
@@ -26,6 +27,7 @@
 import android.view.View;
 
 import com.android.app.animation.Interpolators;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.KeyguardSliceView;
 import com.android.systemui.res.R;
 import com.android.systemui.shared.clocks.AnimatableClockView;
@@ -33,6 +35,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -68,6 +71,8 @@
 
     private final int mGoToFullShadeAppearingTranslation;
     private final int mPulsingAppearingTranslation;
+    @VisibleForTesting
+    float mHeadsUpAppearStartAboveScreen;
     private final ExpandableViewState mTmpState = new ExpandableViewState();
     private final AnimationProperties mAnimationProperties;
     public NotificationStackScrollLayout mHostLayout;
@@ -85,21 +90,23 @@
     private ValueAnimator mTopOverScrollAnimator;
     private ValueAnimator mBottomOverScrollAnimator;
     private int mHeadsUpAppearHeightBottom;
+    private int mStackTopMargin;
     private boolean mShadeExpanded;
     private ArrayList<ExpandableView> mTransientViewsToRemove = new ArrayList<>();
     private NotificationShelf mShelf;
-    private float mStatusBarIconLocation;
-    private int[] mTmpLocation = new int[2];
     private StackStateLogger mLogger;
 
     public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
         mHostLayout = hostLayout;
+        // TODO(b/317061579) reload on configuration changes
         mGoToFullShadeAppearingTranslation =
                 hostLayout.getContext().getResources().getDimensionPixelSize(
                         R.dimen.go_to_full_shade_appearing_translation);
         mPulsingAppearingTranslation =
                 hostLayout.getContext().getResources().getDimensionPixelSize(
                         R.dimen.pulsing_notification_appear_translation);
+        mHeadsUpAppearStartAboveScreen = hostLayout.getContext().getResources()
+                .getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen);
         mAnimationProperties = new AnimationProperties() {
             @Override
             public AnimationFilter getAnimationFilter() {
@@ -455,8 +462,37 @@
                     .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
                 ExpandableNotificationRow row = (ExpandableNotificationRow) event.mChangingView;
                 row.prepareExpansionChanged();
-            } else if (event.animationType == NotificationStackScrollLayout
-                    .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
+            } else if (NotificationsImprovedHunAnimation.isEnabled()
+                    && (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR)) {
+                mHeadsUpAppearChildren.add(changingView);
+
+                mTmpState.copyFrom(changingView.getViewState());
+                if (event.headsUpFromBottom) {
+                    // start from the bottom of the screen
+                    mTmpState.setYTranslation(
+                            mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen);
+                } else {
+                    // start from the top of the screen
+                    mTmpState.setYTranslation(
+                            -mStackTopMargin - mHeadsUpAppearStartAboveScreen);
+                }
+                // set the height and the initial position
+                mTmpState.applyToView(changingView);
+                mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y,
+                        Interpolators.FAST_OUT_SLOW_IN);
+
+                Runnable onAnimationEnd = null;
+                if (loggable) {
+                    // This only captures HEADS_UP_APPEAR animations, but HUNs can appear with
+                    // normal ADD animations, which would not be logged here.
+                    String finalKey = key;
+                    mLogger.logHUNViewAppearing(key);
+                    onAnimationEnd = () -> mLogger.appearAnimationEnded(finalKey);
+                }
+                changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR,
+                        /* isHeadsUpAppear= */ true, onAnimationEnd);
+            } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR) {
+                NotificationsImprovedHunAnimation.assertInLegacyMode();
                 // This item is added, initialize its properties.
                 ExpandableViewState viewState = changingView.getViewState();
                 mTmpState.copyFrom(viewState);
@@ -536,6 +572,10 @@
                             changingView.setInRemovalAnimation(true);
                         };
                     }
+                    if (NotificationsImprovedHunAnimation.isEnabled()) {
+                        mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y,
+                                Interpolators.FAST_OUT_SLOW_IN_REVERSE);
+                    }
                     long removeAnimationDelay = changingView.performRemoveAnimation(
                             ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
                             0, 0.0f, true /* isHeadsUpAppear */,
@@ -601,6 +641,10 @@
         mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
     }
 
+    public void setStackTopMargin(int stackTopMargin) {
+        mStackTopMargin = stackTopMargin;
+    }
+
     public void setShadeExpanded(boolean shadeExpanded) {
         mShadeExpanded = shadeExpanded;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt
index 98c1734..3cd9a8f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.stack.shared
 
 import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the DisplaySwitchNotificationsHider flag state. */
@@ -24,6 +25,10 @@
 object DisplaySwitchNotificationsHiderFlag {
     const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH
 
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
     /** Is the hiding enabled? */
     @JvmStatic
     inline val isEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
index 274bf94..910b40f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
@@ -16,29 +16,22 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewbinder
 
 import androidx.core.view.doOnDetach
-import androidx.lifecycle.lifecycleScope
-import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
-import kotlinx.coroutines.launch
 
 /**
  * Binds a [NotificationStackScrollLayoutController] to its [view model][NotificationListViewModel].
  */
 object HideNotificationsBinder {
-    fun bindHideList(
+    suspend fun bindHideList(
         viewController: NotificationStackScrollLayoutController,
         viewModel: NotificationListViewModel
     ) {
-        viewController.view.repeatWhenAttached {
-            lifecycleScope.launch {
-                viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide ->
-                    viewController.bindHideState(shouldHide)
-                }
-            }
-        }
-
         viewController.view.doOnDetach { viewController.bindHideState(shouldHide = false) }
+
+        viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide ->
+            viewController.bindHideState(shouldHide)
+        }
     }
 
     private fun NotificationStackScrollLayoutController.bindHideState(shouldHide: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 9373d49..1b36660 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -32,15 +32,14 @@
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
-import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import com.android.systemui.util.kotlin.getOrNull
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.combine
@@ -55,25 +54,27 @@
     private val configuration: ConfigurationState,
     private val falsingManager: FalsingManager,
     private val iconAreaController: NotificationIconAreaController,
-    private val iconViewBindingFailureTracker: StatusBarIconViewBindingFailureTracker,
     private val metricsLogger: MetricsLogger,
-    private val shelfIconViewStore: ShelfNotificationIconViewStore,
-    private val systemBarUtilsState: SystemBarUtilsState,
+    private val nicBinder: NotificationIconContainerShelfViewBinder,
 ) {
 
-    fun bind(
+    fun bindWhileAttached(
         view: NotificationStackScrollLayout,
         viewController: NotificationStackScrollLayoutController
     ) {
-        bindShelf(view)
-        bindHideList(viewController, viewModel)
+        val shelf =
+            LayoutInflater.from(view.context)
+                .inflate(R.layout.status_bar_notification_shelf, view, false) as NotificationShelf
+        view.setShelf(shelf)
 
-        if (FooterViewRefactor.isEnabled) {
-            bindFooter(view)
-            bindEmptyShade(view)
+        view.repeatWhenAttached {
+            lifecycleScope.launch {
+                launch { bindShelf(shelf) }
+                launch { bindHideList(viewController, viewModel) }
 
-            view.repeatWhenAttached {
-                lifecycleScope.launch {
+                if (FooterViewRefactor.isEnabled) {
+                    launch { bindFooter(view) }
+                    launch { bindEmptyShade(view) }
                     viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
                         view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
                     }
@@ -82,73 +83,57 @@
         }
     }
 
-    private fun bindShelf(parentView: NotificationStackScrollLayout) {
-        val shelf =
-            LayoutInflater.from(parentView.context)
-                .inflate(R.layout.status_bar_notification_shelf, parentView, false)
-                as NotificationShelf
+    private suspend fun bindShelf(shelf: NotificationShelf) {
         NotificationShelfViewBinder.bind(
             shelf,
             viewModel.shelf,
-            configuration,
-            systemBarUtilsState,
             falsingManager,
-            iconViewBindingFailureTracker,
+            nicBinder,
             iconAreaController,
-            shelfIconViewStore,
         )
-        parentView.setShelf(shelf)
     }
 
-    private fun bindFooter(parentView: NotificationStackScrollLayout) {
-        viewModel.footer.ifPresent { footerViewModel ->
+    private suspend fun bindFooter(parentView: NotificationStackScrollLayout) {
+        viewModel.footer.getOrNull()?.let { footerViewModel ->
             // The footer needs to be re-inflated every time the theme or the font size changes.
-            parentView.repeatWhenAttached {
-                configuration.reinflateAndBindLatest(
-                    R.layout.status_bar_notification_footer,
-                    parentView,
-                    attachToRoot = false,
-                    backgroundDispatcher,
-                ) { footerView: FooterView ->
-                    traceSection("bind FooterView") {
-                        val disposableHandle =
-                            FooterViewBinder.bind(
-                                footerView,
-                                footerViewModel,
-                                clearAllNotifications = {
-                                    metricsLogger.action(
-                                        MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES
-                                    )
-                                    parentView.clearAllNotifications()
-                                },
-                            )
-                        parentView.setFooterView(footerView)
-                        return@reinflateAndBindLatest disposableHandle
-                    }
+            configuration.reinflateAndBindLatest(
+                R.layout.status_bar_notification_footer,
+                parentView,
+                attachToRoot = false,
+                backgroundDispatcher,
+            ) { footerView: FooterView ->
+                traceSection("bind FooterView") {
+                    val disposableHandle =
+                        FooterViewBinder.bind(
+                            footerView,
+                            footerViewModel,
+                            clearAllNotifications = {
+                                metricsLogger.action(
+                                    MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES
+                                )
+                                parentView.clearAllNotifications()
+                            },
+                        )
+                    parentView.setFooterView(footerView)
+                    return@reinflateAndBindLatest disposableHandle
                 }
             }
         }
     }
 
-    private fun bindEmptyShade(
-        parentView: NotificationStackScrollLayout,
-    ) {
-        parentView.repeatWhenAttached {
-            lifecycleScope.launch {
-                combine(
-                        viewModel.shouldShowEmptyShadeView,
-                        viewModel.areNotificationsHiddenInShade,
-                        viewModel.hasFilteredOutSeenNotifications,
-                        ::Triple
-                    )
-                    .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
-                        parentView.updateEmptyShadeView(
-                            shouldShow,
-                            areNotifsHidden,
-                            hasFilteredNotifs,
-                        )
-                    }
+    private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) {
+        combine(
+                viewModel.shouldShowEmptyShadeView,
+                viewModel.areNotificationsHiddenInShade,
+                viewModel.hasFilteredOutSeenNotifications,
+                ::Triple
+            )
+            .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
+                parentView.updateEmptyShadeView(
+                    shouldShow,
+                    areNotifsHidden,
+                    hasFilteredNotifs,
+                )
             }
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index 145dbff..7cc0888 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -196,7 +196,7 @@
     }
 
     private void updateKeyguardStatusBarHeight() {
-        MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
+        ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) getLayoutParams();
         lp.height = getStatusBarHeaderHeightKeyguard(mContext);
         setLayoutParams(lp);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
index a62a1ed..e79f3ff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
@@ -608,7 +608,7 @@
     }
 
     @Override
-    public void onPulseExpansionChanged(boolean expandingChanged) {
+    public void onPulseExpansionAmountChanged(boolean expandingChanged) {
         if (expandingChanged) {
             updateAodIconsVisibility(true /* animate */, false /* force */);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 0dabafb..f34a44a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -284,11 +284,22 @@
 
     @Override
     public String toString() {
-        return "NotificationIconContainer("
-                + "dozing=" + mDozing + " onLockScreen=" + mOnLockScreen
-                + " overrideIconColor=" + mOverrideIconColor
-                + " speedBumpIndex=" + mSpeedBumpIndex
-                + " themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary) + ')';
+        if (NotificationIconContainerRefactor.isEnabled()) {
+            return super.toString()
+                    + " {"
+                    + " overrideIconColor=" + mOverrideIconColor
+                    + ", maxIcons=" + mMaxIcons
+                    + ", isStaticLayout=" + mIsStaticLayout
+                    + ", themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary)
+                    + " }";
+        } else {
+            return "NotificationIconContainer("
+                    + "dozing=" + mDozing + " onLockScreen=" + mOnLockScreen
+                    + " overrideIconColor=" + mOverrideIconColor
+                    + " speedBumpIndex=" + mSpeedBumpIndex
+                    + " themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary)
+                    + ')';
+        }
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index cd99934..2740cc6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -38,7 +38,6 @@
 import com.android.app.animation.InterpolatorsAndroidX;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dumpable;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
@@ -54,10 +53,7 @@
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState;
 import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarNotificationIconViewStore;
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder;
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
@@ -75,7 +71,6 @@
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener;
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsState;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
 import com.android.systemui.util.CarrierConfigTracker;
@@ -95,6 +90,8 @@
 
 import kotlin.Unit;
 
+import kotlinx.coroutines.DisposableHandle;
+
 /**
  * Contains the collapsed status bar and handles hiding/showing based on disable flags
  * and keyguard state. Also manages lifecycle to make sure the views it contains are being
@@ -151,10 +148,7 @@
     private final DumpManager mDumpManager;
     private final StatusBarWindowStateController mStatusBarWindowStateController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private final NotificationIconContainerStatusBarViewModel mStatusBarIconsViewModel;
-    private final ConfigurationState mConfigurationState;
-    private final SystemBarUtilsState mSystemBarUtilsState;
-    private final StatusBarNotificationIconViewStore mStatusBarIconViewStore;
+    private final NotificationIconContainerStatusBarViewBinder mNicViewBinder;
     private final DemoModeController mDemoModeController;
 
     private List<String> mBlockedIcons = new ArrayList<>();
@@ -216,7 +210,7 @@
         mWaitingForWindowStateChangeAfterCameraLaunch = false;
         mTransitionFromLockscreenToDreamStarted = false;
     };
-    private final StatusBarIconViewBindingFailureTracker mIconViewBindingFailureTracker;
+    private DisposableHandle mNicBindingDisposable;
 
     @Inject
     public CollapsedStatusBarFragment(
@@ -234,7 +228,7 @@
             KeyguardStateController keyguardStateController,
             ShadeViewController shadeViewController,
             StatusBarStateController statusBarStateController,
-            StatusBarIconViewBindingFailureTracker iconViewBindingFailureTracker,
+            NotificationIconContainerStatusBarViewBinder nicViewBinder,
             CommandQueue commandQueue,
             CarrierConfigTracker carrierConfigTracker,
             CollapsedStatusBarFragmentLogger collapsedStatusBarFragmentLogger,
@@ -244,10 +238,6 @@
             DumpManager dumpManager,
             StatusBarWindowStateController statusBarWindowStateController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
-            NotificationIconContainerStatusBarViewModel statusBarIconsViewModel,
-            ConfigurationState configurationState,
-            SystemBarUtilsState systemBarUtilsState,
-            StatusBarNotificationIconViewStore statusBarIconViewStore,
             DemoModeController demoModeController) {
         mStatusBarFragmentComponentFactory = statusBarFragmentComponentFactory;
         mOngoingCallController = ongoingCallController;
@@ -263,7 +253,7 @@
         mKeyguardStateController = keyguardStateController;
         mShadeViewController = shadeViewController;
         mStatusBarStateController = statusBarStateController;
-        mIconViewBindingFailureTracker = iconViewBindingFailureTracker;
+        mNicViewBinder = nicViewBinder;
         mCommandQueue = commandQueue;
         mCarrierConfigTracker = carrierConfigTracker;
         mCollapsedStatusBarFragmentLogger = collapsedStatusBarFragmentLogger;
@@ -273,10 +263,6 @@
         mDumpManager = dumpManager;
         mStatusBarWindowStateController = statusBarWindowStateController;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-        mStatusBarIconsViewModel = statusBarIconsViewModel;
-        mConfigurationState = configurationState;
-        mSystemBarUtilsState = systemBarUtilsState;
-        mStatusBarIconViewStore = statusBarIconViewStore;
         mDemoModeController = demoModeController;
     }
 
@@ -455,6 +441,12 @@
             mStartableStates.put(startable, Startable.State.STOPPED);
         }
         mDumpManager.unregisterDumpable(getClass().getSimpleName());
+        if (NotificationIconContainerRefactor.isEnabled()) {
+            if (mNicBindingDisposable != null) {
+                mNicBindingDisposable.dispose();
+                mNicBindingDisposable = null;
+            }
+        }
     }
 
     /** Initializes views related to the notification icon area. */
@@ -466,13 +458,7 @@
                         .inflate(R.layout.notification_icon_area, notificationIconArea, true);
             NotificationIconContainer notificationIcons =
                     notificationIconArea.requireViewById(R.id.notificationIcons);
-            NotificationIconContainerViewBinder.bindWhileAttached(
-                    notificationIcons,
-                    mStatusBarIconsViewModel,
-                    mConfigurationState,
-                    mSystemBarUtilsState,
-                    mIconViewBindingFailureTracker,
-                    mStatusBarIconViewStore);
+            mNicBindingDisposable = mNicViewBinder.bindWhileAttached(notificationIcons);
         } else {
             mNotificationIconAreaInner =
                     mNotificationIconAreaController.getNotificationInnerAreaView();
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 4864fb8..5bced93 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -303,8 +303,7 @@
                     mEntry.mRemoteEditImeVisible = editTextRootWindowInsets != null
                             && editTextRootWindowInsets.isVisible(WindowInsets.Type.ime());
                     if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) {
-                        // Pass null to ensure all inputs are cleared for this entry b/227115380
-                            mController.removeRemoteInput(mEntry, null,
+                            mController.removeRemoteInput(mEntry, mToken,
                                     /* reason= */"RemoteInputView$WindowInsetAnimation#onEnd");
                     }
                 }
@@ -536,6 +535,11 @@
         if (mEntry.getRow().isChangingPosition() || isTemporarilyDetached()) {
             return;
         }
+        // RemoteInputView can be detached from window before IME close event in some cases like
+        // remote input view removal with notification update. As a result of this, RemoteInputView
+        // will stop ime animation updates, which results in never removing remote input. That's why
+        // we have to set mRemoteEditImeAnimatingAway false on detach to remove remote input.
+        mEntry.mRemoteEditImeAnimatingAway = false;
         mController.removeRemoteInput(mEntry, mToken,
                 /* reason= */"RemoteInputView#onDetachedFromWindow");
         mController.removeSpinning(mEntry.getKey(), mToken);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
index fa0cb5c..66bf527 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.policy;
 
 import android.app.AlarmManager;
+import android.app.Flags;
 import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -191,7 +192,11 @@
 
     @Override
     public void setZen(int zen, Uri conditionId, String reason) {
-        mNoMan.setZenMode(zen, conditionId, reason);
+        if (Flags.modesApi()) {
+            mNoMan.setZenMode(zen, conditionId, reason, /* fromUser= */ true);
+        } else {
+            mNoMan.setZenMode(zen, conditionId, reason);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
index e0d205f..c170eb5 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
@@ -37,6 +37,7 @@
 import com.android.internal.util.UserIcons
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.Flags.switchUserOnBg
 import com.android.systemui.SystemUISecondaryUserService
 import com.android.systemui.animation.Expandable
 import com.android.systemui.broadcast.BroadcastDispatcher
@@ -44,6 +45,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -100,6 +102,7 @@
     broadcastDispatcher: BroadcastDispatcher,
     keyguardUpdateMonitor: KeyguardUpdateMonitor,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @Main private val mainDispatcher: CoroutineDispatcher,
     private val activityManager: ActivityManager,
     private val refreshUsersScheduler: RefreshUsersScheduler,
     private val guestUserInteractor: GuestUserInteractor,
@@ -339,7 +342,11 @@
             }
             .launchIn(applicationScope)
         restartSecondaryService(repository.getSelectedUserInfo().id)
-        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+        applicationScope.launch {
+            withContext(mainDispatcher) {
+                keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+            }
+        }
     }
 
     fun addCallback(callback: UserCallback) {
@@ -593,10 +600,18 @@
     private fun switchUser(userId: Int) {
         // TODO(b/246631653): track jank and latency like in the old impl.
         refreshUsersScheduler.pause()
-        try {
-            activityManager.switchUser(userId)
-        } catch (e: RemoteException) {
-            Log.e(TAG, "Couldn't switch user.", e)
+        val runnable = Runnable {
+            try {
+                activityManager.switchUser(userId)
+            } catch (e: RemoteException) {
+                Log.e(TAG, "Couldn't switch user.", e)
+            }
+        }
+
+        if (switchUserOnBg()) {
+            applicationScope.launch { withContext(backgroundDispatcher) { runnable.run() } }
+        } else {
+            runnable.run()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java b/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java
deleted file mode 100644
index 8215360..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java
+++ /dev/null
@@ -1,186 +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.systemui.util.leak;
-
-import android.content.ClipData;
-import android.content.ClipDescription;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.util.Log;
-
-import androidx.core.content.FileProvider;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-
-/**
- * Utility class for dumping, compressing, sending, and serving heap dump files.
- *
- * <p>Unlike the Internet, this IS a big truck you can dump something on.
- */
-public class DumpTruck {
-    private static final String FILEPROVIDER_AUTHORITY = "com.android.systemui.fileprovider";
-    private static final String FILEPROVIDER_PATH = "leak";
-
-    private static final String TAG = "DumpTruck";
-    private static final int BUFSIZ = 1024 * 1024; // 1MB
-
-    private final Context context;
-    private final GarbageMonitor mGarbageMonitor;
-    private Uri hprofUri;
-    private long rss;
-    final StringBuilder body = new StringBuilder();
-
-    public DumpTruck(Context context, GarbageMonitor garbageMonitor) {
-        this.context = context;
-        mGarbageMonitor = garbageMonitor;
-    }
-
-    /**
-     * Capture memory for the given processes and zip them up for sharing.
-     *
-     * @param pids
-     * @return this, for chaining
-     */
-    public DumpTruck captureHeaps(List<Long> pids) {
-        final File dumpDir = new File(context.getCacheDir(), FILEPROVIDER_PATH);
-        dumpDir.mkdirs();
-        hprofUri = null;
-
-        body.setLength(0);
-        body.append("Build: ").append(Build.DISPLAY).append("\n\nProcesses:\n");
-
-        final ArrayList<String> paths = new ArrayList<String>();
-        final int myPid = android.os.Process.myPid();
-
-        for (Long pidL : pids) {
-            final int pid = pidL.intValue();
-            body.append("  pid ").append(pid);
-            GarbageMonitor.ProcessMemInfo info = mGarbageMonitor.getMemInfo(pid);
-            if (info != null) {
-                body.append(":")
-                        .append(" up=")
-                        .append(info.getUptime())
-                        .append(" rss=")
-                        .append(info.currentRss);
-                rss = info.currentRss;
-            }
-            if (pid == myPid) {
-                final String path =
-                        new File(dumpDir, String.format("heap-%d.ahprof", pid)).getPath();
-                Log.v(TAG, "Dumping memory info for process " + pid + " to " + path);
-                try {
-                    android.os.Debug.dumpHprofData(path); // will block
-                    paths.add(path);
-                    body.append(" (hprof attached)");
-                } catch (IOException e) {
-                    Log.e(TAG, "error dumping memory:", e);
-                    body.append("\n** Could not dump heap: \n").append(e.toString()).append("\n");
-                }
-            }
-            body.append("\n");
-        }
-
-        try {
-            final String zipfile =
-                    new File(dumpDir, String.format("hprof-%d.zip", System.currentTimeMillis()))
-                            .getCanonicalPath();
-            if (DumpTruck.zipUp(zipfile, paths)) {
-                final File pathFile = new File(zipfile);
-                hprofUri = FileProvider.getUriForFile(context, FILEPROVIDER_AUTHORITY, pathFile);
-                Log.v(TAG, "Heap dump accessible at URI: " + hprofUri);
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "unable to zip up heapdumps", e);
-            body.append("\n** Could not zip up files: \n").append(e.toString()).append("\n");
-        }
-
-        return this;
-    }
-
-    /**
-     * Get the Uri of the current heap dump. Be sure to call captureHeaps first.
-     *
-     * @return Uri to the dump served by the SystemUI file provider
-     */
-    public Uri getDumpUri() {
-        return hprofUri;
-    }
-
-    /**
-     * Get an ACTION_SEND intent suitable for startActivity() or attaching to a Notification.
-     *
-     * @return share intent
-     */
-    public Intent createShareIntent() {
-        Intent shareIntent = new Intent(Intent.ACTION_SEND_MULTIPLE);
-        shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-        shareIntent.putExtra(Intent.EXTRA_SUBJECT,
-                String.format("SystemUI memory dump (rss=%dM)", rss / 1024));
-
-        shareIntent.putExtra(Intent.EXTRA_TEXT, body.toString());
-
-        if (hprofUri != null) {
-            final ArrayList<Uri> uriList = new ArrayList<>();
-            uriList.add(hprofUri);
-            shareIntent.setType("application/zip");
-            shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList);
-
-            // Include URI in ClipData also, so that grantPermission picks it up.
-            // We don't use setData here because some apps interpret this as "to:".
-            ClipData clipdata = new ClipData(new ClipDescription("content",
-                    new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
-                    new ClipData.Item(hprofUri));
-            shareIntent.setClipData(clipdata);
-            shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-        }
-        return shareIntent;
-    }
-
-    private static boolean zipUp(String zipfilePath, ArrayList<String> paths) {
-        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipfilePath))) {
-            final byte[] buf = new byte[BUFSIZ];
-
-            for (String filename : paths) {
-                try (InputStream is = new BufferedInputStream(new FileInputStream(filename))) {
-                    ZipEntry entry = new ZipEntry(filename);
-                    zos.putNextEntry(entry);
-                    int len;
-                    while (0 < (len = is.read(buf, 0, BUFSIZ))) {
-                        zos.write(buf, 0, len);
-                    }
-                    zos.closeEntry();
-                }
-            }
-            return true;
-        } catch (IOException e) {
-            Log.e(TAG, "error zipping up profile data", e);
-        }
-        return false;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
deleted file mode 100644
index de392d3..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
+++ /dev/null
@@ -1,617 +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.systemui.util.leak;
-
-import static android.service.quicksettings.Tile.STATE_ACTIVE;
-import static android.telephony.ims.feature.ImsFeature.STATE_UNAVAILABLE;
-
-import static com.android.internal.logging.MetricsLogger.VIEW_UNKNOWN;
-
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.ColorStateList;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.ColorFilter;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Process;
-import android.os.SystemProperties;
-import android.provider.Settings;
-import android.text.format.DateUtils;
-import android.util.Log;
-import android.util.LongSparseArray;
-import android.view.View;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.systemui.CoreStartable;
-import com.android.systemui.Dumpable;
-import com.android.systemui.res.R;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.qs.QSTile;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSHost;
-import com.android.systemui.qs.QsEventLogger;
-import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
-import com.android.systemui.qs.tileimpl.QSTileImpl;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.util.concurrency.MessageRouter;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.inject.Inject;
-
-/**
- * Suite of tools to periodically inspect the System UI heap and possibly prompt the user to
- * capture heap dumps and report them. Includes the implementation of the "Dump SysUI Heap"
- * quick settings tile.
- */
-@SysUISingleton
-public class GarbageMonitor implements Dumpable {
-    // Feature switches
-    // ================
-
-    // Whether to use TrackedGarbage to trigger LeakReporter. Off by default unless you set the
-    // appropriate sysprop on a userdebug device.
-    public static final boolean LEAK_REPORTING_ENABLED = Build.IS_DEBUGGABLE
-            && SystemProperties.getBoolean("debug.enable_leak_reporting", false);
-    public static final String FORCE_ENABLE_LEAK_REPORTING = "sysui_force_enable_leak_reporting";
-
-    // Heap tracking: watch the current memory levels and update the MemoryTile if available.
-    // On for all userdebug devices.
-    public static final boolean HEAP_TRACKING_ENABLED = Build.IS_DEBUGGABLE;
-
-    // Tell QSTileHost.java to toss this into the default tileset?
-    public static final boolean ADD_MEMORY_TILE_TO_DEFAULT_ON_DEBUGGABLE_BUILDS = true;
-
-    // whether to use ActivityManager.setHeapLimit (and post a notification to the user asking
-    // to dump the heap). Off by default unless you set the appropriate sysprop on userdebug
-    private static final boolean ENABLE_AM_HEAP_LIMIT = Build.IS_DEBUGGABLE
-            && SystemProperties.getBoolean("debug.enable_sysui_heap_limit", false);
-
-    // Tuning params
-    // =============
-
-    // threshold for setHeapLimit(), in KB (overrides R.integer.watch_heap_limit)
-    private static final String SETTINGS_KEY_AM_HEAP_LIMIT = "systemui_am_heap_limit";
-
-    private static final long GARBAGE_INSPECTION_INTERVAL =
-            15 * DateUtils.MINUTE_IN_MILLIS; // 15 min
-    private static final long HEAP_TRACK_INTERVAL = 1 * DateUtils.MINUTE_IN_MILLIS; // 1 min
-    private static final int HEAP_TRACK_HISTORY_LEN = 720; // 12 hours
-
-    private static final int DO_GARBAGE_INSPECTION = 1000;
-    private static final int DO_HEAP_TRACK = 3000;
-
-    static final int GARBAGE_ALLOWANCE = 5;
-
-    private static final String TAG = "GarbageMonitor";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-
-    private final MessageRouter mMessageRouter;
-    private final TrackedGarbage mTrackedGarbage;
-    private final LeakReporter mLeakReporter;
-    private final Context mContext;
-    private final DelayableExecutor mDelayableExecutor;
-    private MemoryTile mQSTile;
-    private final DumpTruck mDumpTruck;
-
-    private final LongSparseArray<ProcessMemInfo> mData = new LongSparseArray<>();
-    private final ArrayList<Long> mPids = new ArrayList<>();
-
-    private long mHeapLimit;
-
-    /**
-     */
-    @Inject
-    public GarbageMonitor(
-            Context context,
-            @Background DelayableExecutor delayableExecutor,
-            @Background MessageRouter messageRouter,
-            LeakDetector leakDetector,
-            LeakReporter leakReporter,
-            DumpManager dumpManager) {
-        mContext = context.getApplicationContext();
-
-        mDelayableExecutor = delayableExecutor;
-        mMessageRouter = messageRouter;
-        mMessageRouter.subscribeTo(DO_GARBAGE_INSPECTION, this::doGarbageInspection);
-        mMessageRouter.subscribeTo(DO_HEAP_TRACK, this::doHeapTrack);
-
-        mTrackedGarbage = leakDetector.getTrackedGarbage();
-        mLeakReporter = leakReporter;
-
-        mDumpTruck = new DumpTruck(mContext, this);
-
-        dumpManager.registerDumpable(getClass().getSimpleName(), this);
-
-        if (ENABLE_AM_HEAP_LIMIT) {
-            mHeapLimit = Settings.Global.getInt(context.getContentResolver(),
-                    SETTINGS_KEY_AM_HEAP_LIMIT,
-                    mContext.getResources().getInteger(R.integer.watch_heap_limit));
-        }
-    }
-
-    public void startLeakMonitor() {
-        if (mTrackedGarbage == null) {
-            return;
-        }
-
-        mMessageRouter.sendMessage(DO_GARBAGE_INSPECTION);
-    }
-
-    public void startHeapTracking() {
-        startTrackingProcess(
-                android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis());
-        mMessageRouter.sendMessage(DO_HEAP_TRACK);
-    }
-
-    private boolean gcAndCheckGarbage() {
-        if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) {
-            Runtime.getRuntime().gc();
-            return true;
-        }
-        return false;
-    }
-
-    void reinspectGarbageAfterGc() {
-        int count = mTrackedGarbage.countOldGarbage();
-        if (count > GARBAGE_ALLOWANCE) {
-            mLeakReporter.dumpLeak(count);
-        }
-    }
-
-    public ProcessMemInfo getMemInfo(int pid) {
-        return mData.get(pid);
-    }
-
-    public List<Long> getTrackedProcesses() {
-        return mPids;
-    }
-
-    public void startTrackingProcess(long pid, String name, long start) {
-        synchronized (mPids) {
-            if (mPids.contains(pid)) return;
-
-            mPids.add(pid);
-            logPids();
-
-            mData.put(pid, new ProcessMemInfo(pid, name, start));
-        }
-    }
-
-    private void logPids() {
-        if (DEBUG) {
-            StringBuffer sb = new StringBuffer("Now tracking processes: ");
-            for (int i = 0; i < mPids.size(); i++) {
-                final int p = mPids.get(i).intValue();
-                sb.append(" ");
-            }
-            Log.v(TAG, sb.toString());
-        }
-    }
-
-    private void update() {
-        synchronized (mPids) {
-            for (int i = 0; i < mPids.size(); i++) {
-                final int pid = mPids.get(i).intValue();
-                // rssValues contains [VmRSS, RssFile, RssAnon, VmSwap].
-                long[] rssValues = Process.getRss(pid);
-                if (rssValues == null && rssValues.length == 0) {
-                    if (DEBUG) Log.e(TAG, "update: Process.getRss() didn't provide any values.");
-                    break;
-                }
-                long rss = rssValues[0];
-                final ProcessMemInfo info = mData.get(pid);
-                info.rss[info.head] = info.currentRss = rss;
-                info.head = (info.head + 1) % info.rss.length;
-                if (info.currentRss > info.max) info.max = info.currentRss;
-                if (info.currentRss == 0) {
-                    if (DEBUG) Log.v(TAG, "update: pid " + pid + " has rss=0, it probably died");
-                    mData.remove(pid);
-                }
-            }
-            for (int i = mPids.size() - 1; i >= 0; i--) {
-                final long pid = mPids.get(i).intValue();
-                if (mData.get(pid) == null) {
-                    mPids.remove(i);
-                    logPids();
-                }
-            }
-        }
-        if (mQSTile != null) mQSTile.update();
-    }
-
-    private void setTile(MemoryTile tile) {
-        mQSTile = tile;
-        if (tile != null) tile.update();
-    }
-
-    private static String formatBytes(long b) {
-        String[] SUFFIXES = {"B", "K", "M", "G", "T"};
-        int i;
-        for (i = 0; i < SUFFIXES.length; i++) {
-            if (b < 1024) break;
-            b /= 1024;
-        }
-        return b + SUFFIXES[i];
-    }
-
-    private Intent dumpHprofAndGetShareIntent() {
-        return mDumpTruck.captureHeaps(getTrackedProcesses()).createShareIntent();
-    }
-
-    @Override
-    public void dump(PrintWriter pw, @Nullable String[] args) {
-        pw.println("GarbageMonitor params:");
-        pw.println(String.format("   mHeapLimit=%d KB", mHeapLimit));
-        pw.println(String.format("   GARBAGE_INSPECTION_INTERVAL=%d (%.1f mins)",
-                GARBAGE_INSPECTION_INTERVAL,
-                (float) GARBAGE_INSPECTION_INTERVAL / DateUtils.MINUTE_IN_MILLIS));
-        final float htiMins = HEAP_TRACK_INTERVAL / DateUtils.MINUTE_IN_MILLIS;
-        pw.println(String.format("   HEAP_TRACK_INTERVAL=%d (%.1f mins)",
-                HEAP_TRACK_INTERVAL,
-                htiMins));
-        pw.println(String.format("   HEAP_TRACK_HISTORY_LEN=%d (%.1f hr total)",
-                HEAP_TRACK_HISTORY_LEN,
-                (float) HEAP_TRACK_HISTORY_LEN * htiMins / 60f));
-
-        pw.println("GarbageMonitor tracked processes:");
-
-        for (long pid : mPids) {
-            final ProcessMemInfo pmi = mData.get(pid);
-            if (pmi != null) {
-                pmi.dump(pw, args);
-            }
-        }
-    }
-
-
-    private static class MemoryIconDrawable extends Drawable {
-        long rss, limit;
-        final Drawable baseIcon;
-        final Paint paint = new Paint();
-        final float dp;
-
-        MemoryIconDrawable(Context context) {
-            baseIcon = context.getDrawable(R.drawable.ic_memory).mutate();
-            dp = context.getResources().getDisplayMetrics().density;
-            paint.setColor(Color.WHITE);
-        }
-
-        public void setRss(long rss) {
-            if (rss != this.rss) {
-                this.rss = rss;
-                invalidateSelf();
-            }
-        }
-
-        public void setLimit(long limit) {
-            if (limit != this.limit) {
-                this.limit = limit;
-                invalidateSelf();
-            }
-        }
-
-        @Override
-        public void draw(Canvas canvas) {
-            baseIcon.draw(canvas);
-
-            if (limit > 0 && rss > 0) {
-                float frac = Math.min(1f, (float) rss / limit);
-
-                final Rect bounds = getBounds();
-                canvas.translate(bounds.left + 8 * dp, bounds.top + 5 * dp);
-                //android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z"
-                canvas.drawRect(0, 14 * dp * (1 - frac), 8 * dp + 1, 14 * dp + 1, paint);
-            }
-        }
-
-        @Override
-        public void setBounds(int left, int top, int right, int bottom) {
-            super.setBounds(left, top, right, bottom);
-            baseIcon.setBounds(left, top, right, bottom);
-        }
-
-        @Override
-        public int getIntrinsicHeight() {
-            return baseIcon.getIntrinsicHeight();
-        }
-
-        @Override
-        public int getIntrinsicWidth() {
-            return baseIcon.getIntrinsicWidth();
-        }
-
-        @Override
-        public void setAlpha(int i) {
-            baseIcon.setAlpha(i);
-        }
-
-        @Override
-        public void setColorFilter(ColorFilter colorFilter) {
-            baseIcon.setColorFilter(colorFilter);
-            paint.setColorFilter(colorFilter);
-        }
-
-        @Override
-        public void setTint(int tint) {
-            super.setTint(tint);
-            baseIcon.setTint(tint);
-        }
-
-        @Override
-        public void setTintList(ColorStateList tint) {
-            super.setTintList(tint);
-            baseIcon.setTintList(tint);
-        }
-
-        @Override
-        public void setTintMode(PorterDuff.Mode tintMode) {
-            super.setTintMode(tintMode);
-            baseIcon.setTintMode(tintMode);
-        }
-
-        @Override
-        public int getOpacity() {
-            return PixelFormat.TRANSLUCENT;
-        }
-    }
-
-    private static class MemoryGraphIcon extends QSTile.Icon {
-        long rss, limit;
-
-        public void setRss(long rss) {
-            this.rss = rss;
-        }
-
-        public void setHeapLimit(long limit) {
-            this.limit = limit;
-        }
-
-        @Override
-        public Drawable getDrawable(Context context) {
-            final MemoryIconDrawable drawable = new MemoryIconDrawable(context);
-            drawable.setRss(rss);
-            drawable.setLimit(limit);
-            return drawable;
-        }
-    }
-
-    public static class MemoryTile extends QSTileImpl<QSTile.State> {
-        public static final String TILE_SPEC = "dbg:mem";
-
-        private final GarbageMonitor gm;
-        private ProcessMemInfo pmi;
-        private boolean dumpInProgress;
-        private final PanelInteractor mPanelInteractor;
-
-        @Inject
-        public MemoryTile(
-                QSHost host,
-                QsEventLogger uiEventLogger,
-                @Background Looper backgroundLooper,
-                @Main Handler mainHandler,
-                FalsingManager falsingManager,
-                MetricsLogger metricsLogger,
-                StatusBarStateController statusBarStateController,
-                ActivityStarter activityStarter,
-                QSLogger qsLogger,
-                GarbageMonitor monitor,
-                PanelInteractor panelInteractor
-        ) {
-            super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
-                    statusBarStateController, activityStarter, qsLogger);
-            gm = monitor;
-            mPanelInteractor = panelInteractor;
-        }
-
-        @Override
-        public State newTileState() {
-            return new QSTile.State();
-        }
-
-        @Override
-        public Intent getLongClickIntent() {
-            return new Intent();
-        }
-
-        @Override
-        protected void handleClick(@Nullable View view) {
-            if (dumpInProgress) return;
-
-            dumpInProgress = true;
-            refreshState();
-            new Thread("HeapDumpThread") {
-                @Override
-                public void run() {
-                    try {
-                        // wait for animations & state changes
-                        Thread.sleep(500);
-                    } catch (InterruptedException ignored) { }
-                    final Intent shareIntent = gm.dumpHprofAndGetShareIntent();
-                    mHandler.post(() -> {
-                        dumpInProgress = false;
-                        refreshState();
-                        mPanelInteractor.collapsePanels();
-                        mActivityStarter.postStartActivityDismissingKeyguard(shareIntent, 0);
-                    });
-                }
-            }.start();
-        }
-
-        @Override
-        public int getMetricsCategory() {
-            return VIEW_UNKNOWN;
-        }
-
-        @Override
-        public void handleSetListening(boolean listening) {
-            super.handleSetListening(listening);
-            if (gm != null) gm.setTile(listening ? this : null);
-
-            final ActivityManager am = mContext.getSystemService(ActivityManager.class);
-            if (listening && gm.mHeapLimit > 0) {
-                am.setWatchHeapLimit(1024 * gm.mHeapLimit); // why is this in bytes?
-            } else {
-                am.clearWatchHeapLimit();
-            }
-        }
-
-        @Override
-        public CharSequence getTileLabel() {
-            return getState().label;
-        }
-
-        @Override
-        protected void handleUpdateState(State state, Object arg) {
-            pmi = gm.getMemInfo(Process.myPid());
-            final MemoryGraphIcon icon = new MemoryGraphIcon();
-            icon.setHeapLimit(gm.mHeapLimit);
-            state.state = dumpInProgress ? STATE_UNAVAILABLE : STATE_ACTIVE;
-            state.label = dumpInProgress
-                    ? "Dumping..."
-                    : mContext.getString(R.string.heap_dump_tile_name);
-            if (pmi != null) {
-                icon.setRss(pmi.currentRss);
-                state.secondaryLabel =
-                        String.format(
-                                "rss: %s / %s",
-                                formatBytes(pmi.currentRss * 1024),
-                                formatBytes(gm.mHeapLimit * 1024));
-            } else {
-                icon.setRss(0);
-                state.secondaryLabel = null;
-            }
-            state.icon = icon;
-        }
-
-        public void update() {
-            refreshState();
-        }
-
-        public long getRss() {
-            return pmi != null ? pmi.currentRss : 0;
-        }
-
-        public long getHeapLimit() {
-            return gm != null ? gm.mHeapLimit : 0;
-        }
-    }
-
-    /** */
-    public static class ProcessMemInfo implements Dumpable {
-        public long pid;
-        public String name;
-        public long startTime;
-        public long currentRss;
-        public long[] rss = new long[HEAP_TRACK_HISTORY_LEN];
-        public long max = 1;
-        public int head = 0;
-
-        public ProcessMemInfo(long pid, String name, long start) {
-            this.pid = pid;
-            this.name = name;
-            this.startTime = start;
-        }
-
-        public long getUptime() {
-            return System.currentTimeMillis() - startTime;
-        }
-
-        @Override
-        public void dump(PrintWriter pw, @Nullable String[] args) {
-            pw.print("{ \"pid\": ");
-            pw.print(pid);
-            pw.print(", \"name\": \"");
-            pw.print(name.replace('"', '-'));
-            pw.print("\", \"start\": ");
-            pw.print(startTime);
-            pw.print(", \"rss\": [");
-            // write rss values starting from the oldest, which is rss[head], wrapping around to
-            // rss[(head-1) % rss.length]
-            for (int i = 0; i < rss.length; i++) {
-                if (i > 0) pw.print(",");
-                pw.print(rss[(head + i) % rss.length]);
-            }
-            pw.println("] }");
-        }
-    }
-
-    /** */
-    @SysUISingleton
-    public static class Service implements CoreStartable,  Dumpable {
-        private final Context mContext;
-        private final GarbageMonitor mGarbageMonitor;
-
-        @Inject
-        public Service(Context context, GarbageMonitor garbageMonitor) {
-            mContext = context;
-            mGarbageMonitor = garbageMonitor;
-        }
-
-        @Override
-        public void start() {
-            boolean forceEnable =
-                    Settings.Secure.getInt(
-                                    mContext.getContentResolver(), FORCE_ENABLE_LEAK_REPORTING, 0)
-                            != 0;
-            if (LEAK_REPORTING_ENABLED || forceEnable) {
-                mGarbageMonitor.startLeakMonitor();
-            }
-            if (HEAP_TRACKING_ENABLED || forceEnable) {
-                mGarbageMonitor.startHeapTracking();
-            }
-        }
-
-        @Override
-        public void dump(PrintWriter pw, @Nullable String[] args) {
-            if (mGarbageMonitor != null) mGarbageMonitor.dump(pw, args);
-        }
-    }
-
-    private void doGarbageInspection(int id) {
-        if (gcAndCheckGarbage()) {
-            mDelayableExecutor.executeDelayed(this::reinspectGarbageAfterGc, 100);
-        }
-
-        mMessageRouter.cancelMessages(DO_GARBAGE_INSPECTION);
-        mMessageRouter.sendMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL);
-    }
-
-    private void doHeapTrack(int id) {
-        update();
-        mMessageRouter.cancelMessages(DO_HEAP_TRACK);
-        mMessageRouter.sendMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt
deleted file mode 100644
index e975200..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt
+++ /dev/null
@@ -1,39 +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.util.leak
-
-import com.android.systemui.CoreStartable
-import com.android.systemui.qs.tileimpl.QSTileImpl
-import dagger.Binds
-import dagger.Module
-import dagger.multibindings.ClassKey
-import dagger.multibindings.IntoMap
-import dagger.multibindings.StringKey
-
-@Module
-interface GarbageMonitorModule {
-    /** Inject into GarbageMonitor.Service. */
-    @Binds
-    @IntoMap
-    @ClassKey(GarbageMonitor::class)
-    fun bindGarbageMonitorService(sysui: GarbageMonitor.Service): CoreStartable
-
-    @Binds
-    @IntoMap
-    @StringKey(GarbageMonitor.MemoryTile.TILE_SPEC)
-    fun bindMemoryTile(memoryTile: GarbageMonitor.MemoryTile): QSTileImpl<*>
-}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
index 88f63ad..a249961 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
@@ -34,14 +34,12 @@
 import android.widget.RelativeLayout;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
 import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager;
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.plugins.clocks.ClockAnimations;
 import com.android.systemui.plugins.clocks.ClockController;
@@ -56,14 +54,9 @@
 import com.android.systemui.shared.clocks.ClockRegistry;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel;
-import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsState;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -185,9 +178,7 @@
                 mKeyguardSliceViewController,
                 mNotificationIconAreaController,
                 mSmartspaceController,
-                mock(SystemBarUtilsState.class),
-                mock(ScreenOffAnimationController.class),
-                mock(StatusBarIconViewBindingFailureTracker.class),
+                mock(NotificationIconContainerAlwaysOnDisplayViewBinder.class),
                 mKeyguardUnlockAnimationController,
                 mSecureSettings,
                 mExecutor,
@@ -195,11 +186,6 @@
                 mDumpManager,
                 mClockEventController,
                 mLogBuffer,
-                mock(NotificationIconContainerAlwaysOnDisplayViewModel.class),
-                mock(KeyguardRootViewModel.class),
-                mock(ConfigurationState.class),
-                mock(DozeParameters.class),
-                mock(AlwaysOnDisplayNotificationIconViewStore.class),
                 KeyguardInteractorFactory.create(mFakeFeatureFlags).getKeyguardInteractor(),
                 mKeyguardClockInteractor,
                 mFakeFeatureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt
index fd5a584..1b6aaab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt
@@ -22,17 +22,13 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
-import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
-import com.android.systemui.keyguard.ui.viewmodel.deviceEntryIconViewModelTransitionsMock
+import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import com.android.systemui.statusbar.phone.systemUIDialogManager
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -48,25 +44,14 @@
 @SmallTest
 @RunWith(JUnit4::class)
 class DeviceEntryUdfpsTouchOverlayViewModelTest : SysuiTestCase() {
-    val kosmos =
+    private val kosmos =
         testKosmos().apply {
             fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, true) }
         }
-    val testScope = kosmos.testScope
-
-    private val testDeviceEntryIconTransitionAlpha = MutableStateFlow(0f)
-    private val testDeviceEntryIconTransition: DeviceEntryIconTransition
-        get() =
-            object : DeviceEntryIconTransition {
-                override val deviceEntryParentViewAlpha: Flow<Float> =
-                    testDeviceEntryIconTransitionAlpha.asStateFlow()
-            }
-
-    init {
-        kosmos.deviceEntryIconViewModelTransitionsMock.add(testDeviceEntryIconTransition)
-    }
     private val systemUIDialogManager = kosmos.systemUIDialogManager
     private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
+    private val testScope = kosmos.testScope
+    private val deviceEntryIconViewModelTransition = kosmos.fakeDeviceEntryIconViewModelTransition
     private val underTest = kosmos.deviceEntryUdfpsTouchOverlayViewModel
 
     @Captor
@@ -82,7 +67,7 @@
         testScope.runTest {
             val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
 
-            testDeviceEntryIconTransitionAlpha.value = 1f
+            deviceEntryIconViewModelTransition.setDeviceEntryParentViewAlpha(1f)
             runCurrent()
 
             verify(systemUIDialogManager).registerListener(sysuiDialogListenerCaptor.capture())
@@ -96,7 +81,7 @@
         testScope.runTest {
             val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
 
-            testDeviceEntryIconTransitionAlpha.value = .3f
+            deviceEntryIconViewModelTransition.setDeviceEntryParentViewAlpha(.3f)
             runCurrent()
 
             verify(systemUIDialogManager).registerListener(sysuiDialogListenerCaptor.capture())
@@ -110,7 +95,7 @@
         testScope.runTest {
             val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
 
-            testDeviceEntryIconTransitionAlpha.value = 1f
+            deviceEntryIconViewModelTransition.setDeviceEntryParentViewAlpha(1f)
             runCurrent()
 
             verify(systemUIDialogManager).registerListener(sysuiDialogListenerCaptor.capture())
@@ -124,7 +109,7 @@
         testScope.runTest {
             val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
 
-            testDeviceEntryIconTransitionAlpha.value = 0f
+            deviceEntryIconViewModelTransition.setDeviceEntryParentViewAlpha(0f)
             runCurrent()
 
             bouncerRepository.setAlternateVisible(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
new file mode 100644
index 0000000..93ce86a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.deviceentry.domain.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.fakeAccessibilityRepository
+import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.data.ui.viewmodel.udfpsAccessibilityOverlayViewModel
+import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UdfpsAccessibilityOverlayViewModelTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            fakeFeatureFlagsClassic.apply { set(FULL_SCREEN_USER_SWITCHER, false) }
+        }
+    private val deviceEntryIconTransition = kosmos.fakeDeviceEntryIconViewModelTransition
+    private val testScope = kosmos.testScope
+    private val accessibilityRepository = kosmos.fakeAccessibilityRepository
+    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
+    private val deviceEntryFingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository
+    private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
+    private val shadeRepository = kosmos.fakeShadeRepository
+    private val underTest = kosmos.udfpsAccessibilityOverlayViewModel
+
+    @Test
+    fun visible() =
+        testScope.runTest {
+            val visible by collectLastValue(underTest.visible)
+            setupVisibleStateOnLockscreen()
+            assertThat(visible).isTrue()
+        }
+
+    @Test
+    fun touchExplorationNotEnabled_overlayNotVisible() =
+        testScope.runTest {
+            val visible by collectLastValue(underTest.visible)
+            setupVisibleStateOnLockscreen()
+            accessibilityRepository.isTouchExplorationEnabled.value = false
+            assertThat(visible).isFalse()
+        }
+
+    @Test
+    fun deviceEntryFgIconViewModelAod_overlayNotVisible() =
+        testScope.runTest {
+            val visible by collectLastValue(underTest.visible)
+            setupVisibleStateOnLockscreen()
+
+            // AOD
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    value = 0f,
+                    transitionState = TransitionState.STARTED,
+                )
+            )
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED,
+                )
+            )
+            assertThat(visible).isFalse()
+        }
+
+    @Test
+    fun deviceUnlocked_overlayNotVisible() =
+        testScope.runTest {
+            val visible by collectLastValue(underTest.visible)
+            setupVisibleStateOnLockscreen()
+            deviceEntryRepository.setUnlocked(true)
+            assertThat(visible).isFalse()
+        }
+
+    @Test
+    fun deviceEntryViewAlpha0_overlayNotVisible() =
+        testScope.runTest {
+            val visible by collectLastValue(underTest.visible)
+            setupVisibleStateOnLockscreen()
+            deviceEntryIconTransition.setDeviceEntryParentViewAlpha(0f)
+            assertThat(visible).isFalse()
+        }
+
+    private suspend fun setupVisibleStateOnLockscreen() {
+        // A11y enabled
+        accessibilityRepository.isTouchExplorationEnabled.value = true
+
+        // Transition alpha is 1f
+        deviceEntryIconTransition.setDeviceEntryParentViewAlpha(1f)
+
+        // Listening for UDFPS
+        fingerprintPropertyRepository.supportsUdfps()
+        deviceEntryFingerprintAuthRepository.setIsRunning(true)
+        deviceEntryRepository.setUnlocked(false)
+
+        // Lockscreen
+        keyguardTransitionRepository.sendTransitionStep(
+            TransitionStep(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                value = 0f,
+                transitionState = TransitionState.STARTED,
+            )
+        )
+        keyguardTransitionRepository.sendTransitionStep(
+            TransitionStep(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                value = 1f,
+                transitionState = TransitionState.FINISHED,
+            )
+        )
+
+        // Shade not expanded
+        shadeRepository.qsExpansion.value = 0f
+        shadeRepository.lockscreenShadeExpansion.value = 0f
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
index 3109e76..ad86ee9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
+import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection
 import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines
 import com.android.systemui.util.mockito.whenever
@@ -70,7 +71,8 @@
     @Mock private lateinit var communalTutorialIndicatorSection: CommunalTutorialIndicatorSection
     @Mock private lateinit var clockSection: ClockSection
     @Mock private lateinit var smartspaceSection: SmartspaceSection
-
+    @Mock
+    private lateinit var udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -90,6 +92,7 @@
                 communalTutorialIndicatorSection,
                 clockSection,
                 smartspaceSection,
+                udfpsAccessibilityOverlaySection,
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt
deleted file mode 100644
index 6512290..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.viewmodel
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
-import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-@ExperimentalCoroutinesApi
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class UdfpsAodViewModelTest : SysuiTestCase() {
-    private val defaultPadding = 12
-    private lateinit var underTest: UdfpsAodViewModel
-
-    private lateinit var testScope: TestScope
-    private lateinit var configRepository: FakeConfigurationRepository
-    private lateinit var bouncerRepository: KeyguardBouncerRepository
-    private lateinit var keyguardRepository: FakeKeyguardRepository
-    private lateinit var shadeRepository: FakeShadeRepository
-    private lateinit var keyguardInteractor: KeyguardInteractor
-
-    @Mock private lateinit var dialogManager: SystemUIDialogManager
-    @Mock private lateinit var burnInHelper: BurnInHelperWrapper
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        overrideResource(com.android.systemui.res.R.dimen.lock_icon_padding, defaultPadding)
-        testScope = TestScope()
-        shadeRepository = FakeShadeRepository()
-        KeyguardInteractorFactory.create().also {
-            keyguardInteractor = it.keyguardInteractor
-            keyguardRepository = it.repository
-            configRepository = it.configurationRepository
-            bouncerRepository = it.bouncerRepository
-        }
-        val udfpsKeyguardInteractor =
-            UdfpsKeyguardInteractor(
-                configRepository,
-                BurnInInteractor(
-                    context,
-                    burnInHelper,
-                    testScope.backgroundScope,
-                    configRepository,
-                    keyguardInteractor,
-                ),
-                keyguardInteractor,
-                shadeRepository,
-                dialogManager,
-            )
-
-        underTest =
-            UdfpsAodViewModel(
-                udfpsKeyguardInteractor,
-                context,
-            )
-    }
-
-    @Test
-    fun alphaAndVisibleUpdates_onDozeAmountChanges() =
-        testScope.runTest {
-            val alpha by collectLastValue(underTest.alpha)
-            val visible by collectLastValue(underTest.isVisible)
-
-            keyguardRepository.setDozeAmount(0f)
-            runCurrent()
-            assertThat(alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-
-            keyguardRepository.setDozeAmount(.65f)
-            runCurrent()
-            assertThat(alpha).isEqualTo(.65f)
-            assertThat(visible).isTrue()
-
-            keyguardRepository.setDozeAmount(.23f)
-            runCurrent()
-            assertThat(alpha).isEqualTo(.23f)
-            assertThat(visible).isTrue()
-
-            keyguardRepository.setDozeAmount(1f)
-            runCurrent()
-            assertThat(alpha).isEqualTo(1f)
-            assertThat(visible).isTrue()
-        }
-
-    @Test
-    fun paddingUpdates_onScaleForResolutionChanges() =
-        testScope.runTest {
-            val padding by collectLastValue(underTest.padding)
-
-            configRepository.setScaleForResolution(1f)
-            runCurrent()
-            assertThat(padding).isEqualTo(defaultPadding)
-
-            configRepository.setScaleForResolution(2f)
-            runCurrent()
-            assertThat(padding).isEqualTo(defaultPadding * 2)
-
-            configRepository.setScaleForResolution(.5f)
-            runCurrent()
-            assertThat(padding).isEqualTo((defaultPadding * .5f).toInt())
-        }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
deleted file mode 100644
index 95b2fe5..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.viewmodel
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
-import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.keyguard.data.repository.FakeCommandQueue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
-import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-/** Tests UdfpsFingerprintViewModel specific flows. */
-@ExperimentalCoroutinesApi
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class UdfpsFingerprintViewModelTest : SysuiTestCase() {
-    private val defaultPadding = 12
-    private lateinit var underTest: FingerprintViewModel
-
-    private lateinit var testScope: TestScope
-    private lateinit var configRepository: FakeConfigurationRepository
-    private lateinit var bouncerRepository: KeyguardBouncerRepository
-    private lateinit var keyguardRepository: FakeKeyguardRepository
-    private lateinit var fakeCommandQueue: FakeCommandQueue
-    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
-    private lateinit var shadeRepository: FakeShadeRepository
-
-    @Mock private lateinit var burnInHelper: BurnInHelperWrapper
-    @Mock private lateinit var dialogManager: SystemUIDialogManager
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        overrideResource(com.android.systemui.res.R.dimen.lock_icon_padding, defaultPadding)
-        testScope = TestScope()
-        configRepository = FakeConfigurationRepository()
-        keyguardRepository = FakeKeyguardRepository()
-        bouncerRepository = FakeKeyguardBouncerRepository()
-        fakeCommandQueue = FakeCommandQueue()
-        bouncerRepository = FakeKeyguardBouncerRepository()
-        transitionRepository = FakeKeyguardTransitionRepository()
-        shadeRepository = FakeShadeRepository()
-        val keyguardInteractor =
-            KeyguardInteractorFactory.create(
-                    repository = keyguardRepository,
-                )
-                .keyguardInteractor
-
-        val transitionInteractor =
-            KeyguardTransitionInteractorFactory.create(
-                    scope = testScope.backgroundScope,
-                    repository = transitionRepository,
-                    keyguardInteractor = keyguardInteractor,
-                )
-                .keyguardTransitionInteractor
-
-        underTest =
-            FingerprintViewModel(
-                context,
-                transitionInteractor,
-                UdfpsKeyguardInteractor(
-                    configRepository,
-                    BurnInInteractor(
-                        context,
-                        burnInHelper,
-                        testScope.backgroundScope,
-                        configRepository,
-                        keyguardInteractor,
-                    ),
-                    keyguardInteractor,
-                    shadeRepository,
-                    dialogManager,
-                ),
-                keyguardInteractor,
-            )
-    }
-
-    @Test
-    fun paddingUpdates_onScaleForResolutionChanges() =
-        testScope.runTest {
-            val padding by collectLastValue(underTest.padding)
-
-            configRepository.setScaleForResolution(1f)
-            runCurrent()
-            assertThat(padding).isEqualTo(defaultPadding)
-
-            configRepository.setScaleForResolution(2f)
-            runCurrent()
-            assertThat(padding).isEqualTo(defaultPadding * 2)
-
-            configRepository.setScaleForResolution(.5f)
-            runCurrent()
-            assertThat(padding).isEqualTo((defaultPadding * .5).toInt())
-        }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
deleted file mode 100644
index 848a94b..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
+++ /dev/null
@@ -1,749 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.viewmodel
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.settingslib.Utils
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.StatusBarState
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.mock
-import com.android.wm.shell.animation.Interpolators
-import com.google.common.collect.Range
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.MockitoAnnotations
-
-/** Tests UDFPS lockscreen view model transitions. */
-@ExperimentalCoroutinesApi
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class UdfpsLockscreenViewModelTest : SysuiTestCase() {
-    private val lockscreenColorResId = android.R.attr.textColorPrimary
-    private val alternateBouncerResId = com.android.internal.R.attr.materialColorOnPrimaryFixed
-    private val lockscreenColor = Utils.getColorAttrDefaultColor(context, lockscreenColorResId)
-    private val alternateBouncerColor =
-        Utils.getColorAttrDefaultColor(context, alternateBouncerResId)
-
-    @Mock private lateinit var dialogManager: SystemUIDialogManager
-
-    private lateinit var underTest: UdfpsLockscreenViewModel
-    private lateinit var testScope: TestScope
-    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
-    private lateinit var configRepository: FakeConfigurationRepository
-    private lateinit var keyguardRepository: FakeKeyguardRepository
-    private lateinit var keyguardInteractor: KeyguardInteractor
-    private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
-    private lateinit var shadeRepository: FakeShadeRepository
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        testScope = TestScope()
-        transitionRepository = FakeKeyguardTransitionRepository()
-        shadeRepository = FakeShadeRepository()
-        KeyguardInteractorFactory.create().also {
-            keyguardInteractor = it.keyguardInteractor
-            keyguardRepository = it.repository
-            configRepository = it.configurationRepository
-            bouncerRepository = it.bouncerRepository
-        }
-
-        val transitionInteractor =
-            KeyguardTransitionInteractorFactory.create(
-                    scope = testScope.backgroundScope,
-                    repository = transitionRepository,
-                    keyguardInteractor = keyguardInteractor,
-                )
-                .keyguardTransitionInteractor
-
-        underTest =
-            UdfpsLockscreenViewModel(
-                context,
-                lockscreenColorResId,
-                alternateBouncerResId,
-                transitionInteractor,
-                UdfpsKeyguardInteractor(
-                    configRepository,
-                    BurnInInteractor(
-                        context,
-                        burnInHelperWrapper = mock(),
-                        testScope.backgroundScope,
-                        configRepository,
-                        keyguardInteractor,
-                    ),
-                    keyguardInteractor,
-                    shadeRepository,
-                    dialogManager,
-                ),
-                keyguardInteractor,
-            )
-    }
-
-    @Test
-    fun goneToAodTransition() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-
-            // TransitionState.STARTED: gone -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.AOD,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "goneToAodTransition",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-
-            // TransitionState.RUNNING: gone -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.AOD,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "goneToAodTransition",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-
-            // TransitionState.FINISHED: gone -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.AOD,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "goneToAodTransition",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-        }
-
-    @Test
-    fun lockscreenToAod() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-
-            // TransitionState.STARTED: lockscreen -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "lockscreenToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: lockscreen -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "lockscreenToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f))
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: lockscreen -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "lockscreenToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isFalse()
-        }
-
-    @Test
-    fun lockscreenShadeLockedToAod() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-            keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
-
-            // TransitionState.STARTED: lockscreen -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "lockscreenToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-
-            // TransitionState.RUNNING: lockscreen -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "lockscreenToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-
-            // TransitionState.FINISHED: lockscreen -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "lockscreenToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-        }
-
-    @Test
-    fun aodToLockscreen() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-
-            // TransitionState.STARTED: AOD -> lockscreen
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.AOD,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "aodToLockscreen",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isFalse()
-
-            // TransitionState.RUNNING: AOD -> lockscreen
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.AOD,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "aodToLockscreen",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isIn(Range.closed(.59f, .61f))
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: AOD -> lockscreen
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.AOD,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "aodToLockscreen",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-        }
-
-    @Test
-    fun lockscreenToAlternateBouncer() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-
-            // TransitionState.STARTED: lockscreen -> alternate bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.ALTERNATE_BOUNCER,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "lockscreenToAlternateBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: lockscreen -> alternate bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.ALTERNATE_BOUNCER,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "lockscreenToAlternateBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: lockscreen -> alternate bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.ALTERNATE_BOUNCER,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "lockscreenToAlternateBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-        }
-
-    fun alternateBouncerToPrimaryBouncer() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-
-            // TransitionState.STARTED: alternate bouncer -> primary bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.ALTERNATE_BOUNCER,
-                    to = KeyguardState.PRIMARY_BOUNCER,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "alternateBouncerToPrimaryBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: alternate bouncer -> primary bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.ALTERNATE_BOUNCER,
-                    to = KeyguardState.PRIMARY_BOUNCER,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "alternateBouncerToPrimaryBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isIn(Range.closed(.59f, .61f))
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: alternate bouncer -> primary bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.ALTERNATE_BOUNCER,
-                    to = KeyguardState.PRIMARY_BOUNCER,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "alternateBouncerToPrimaryBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isFalse()
-        }
-
-    fun alternateBouncerToAod() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-
-            // TransitionState.STARTED: alternate bouncer -> aod
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.ALTERNATE_BOUNCER,
-                    to = KeyguardState.AOD,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "alternateBouncerToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: alternate bouncer -> aod
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.ALTERNATE_BOUNCER,
-                    to = KeyguardState.AOD,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "alternateBouncerToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f))
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: alternate bouncer -> aod
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.ALTERNATE_BOUNCER,
-                    to = KeyguardState.AOD,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "alternateBouncerToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isFalse()
-        }
-
-    @Test
-    fun lockscreenToOccluded() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-
-            // TransitionState.STARTED: lockscreen -> occluded
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.OCCLUDED,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "lockscreenToOccluded",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: lockscreen -> occluded
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.OCCLUDED,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "lockscreenToOccluded",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f))
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: lockscreen -> occluded
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.OCCLUDED,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "lockscreenToOccluded",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isFalse()
-        }
-
-    @Test
-    fun occludedToLockscreen() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-
-            // TransitionState.STARTED: occluded -> lockscreen
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.OCCLUDED,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "occludedToLockscreen",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: occluded -> lockscreen
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.OCCLUDED,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "occludedToLockscreen",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: occluded -> lockscreen
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.OCCLUDED,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "occludedToLockscreen",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-        }
-
-    @Test
-    fun qsProgressChange() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-            givenTransitionToLockscreenFinished()
-
-            // qsExpansion = 0f
-            shadeRepository.setQsExpansion(0f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(visible).isEqualTo(true)
-
-            // qsExpansion = .25
-            shadeRepository.setQsExpansion(.2f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(.6f)
-            assertThat(visible).isEqualTo(true)
-
-            // qsExpansion = .5
-            shadeRepository.setQsExpansion(.5f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isEqualTo(false)
-
-            // qsExpansion = 1
-            shadeRepository.setQsExpansion(1f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isEqualTo(false)
-        }
-
-    @Test
-    fun shadeExpansionChanged() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-            givenTransitionToLockscreenFinished()
-
-            // shadeExpansion = 0f
-            shadeRepository.setUdfpsTransitionToFullShadeProgress(0f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(visible).isEqualTo(true)
-
-            // shadeExpansion = .2
-            shadeRepository.setUdfpsTransitionToFullShadeProgress(.2f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(.8f)
-            assertThat(visible).isEqualTo(true)
-
-            // shadeExpansion = .5
-            shadeRepository.setUdfpsTransitionToFullShadeProgress(.5f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(.5f)
-            assertThat(visible).isEqualTo(true)
-
-            // shadeExpansion = 1
-            shadeRepository.setUdfpsTransitionToFullShadeProgress(1f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isEqualTo(false)
-        }
-
-    @Test
-    fun dialogHideAffordancesRequestChanged() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            givenTransitionToLockscreenFinished()
-            runCurrent()
-            val captor = argumentCaptor<SystemUIDialogManager.Listener>()
-            Mockito.verify(dialogManager).registerListener(captor.capture())
-
-            captor.value.shouldHideAffordances(true)
-            assertThat(transition?.alpha).isEqualTo(0f)
-
-            captor.value.shouldHideAffordances(false)
-            assertThat(transition?.alpha).isEqualTo(1f)
-        }
-
-    @Test
-    fun occludedToAlternateBouncer() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-
-            // TransitionState.STARTED: occluded -> alternate bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.OCCLUDED,
-                    to = KeyguardState.ALTERNATE_BOUNCER,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "occludedToAlternateBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(0f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: occluded -> alternate bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.OCCLUDED,
-                    to = KeyguardState.ALTERNATE_BOUNCER,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "occludedToAlternateBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale)
-                .isEqualTo(Interpolators.FAST_OUT_SLOW_IN.getInterpolation(.6f))
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: occluded -> alternate bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.OCCLUDED,
-                    to = KeyguardState.ALTERNATE_BOUNCER,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "occludedToAlternateBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-        }
-
-    private suspend fun givenTransitionToLockscreenFinished() {
-        transitionRepository.sendTransitionSteps(
-            from = KeyguardState.AOD,
-            to = KeyguardState.LOCKSCREEN,
-            testScope
-        )
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
index 1f99303..0a464e6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
@@ -22,11 +22,11 @@
 import android.testing.TestableLooper
 import android.view.View
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.controls.models.player.MediaViewHolder
 import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
 import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.res.R
 import com.android.systemui.util.animation.MeasurementInput
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.animation.TransitionViewState
@@ -37,6 +37,7 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.floatThat
 import org.mockito.Mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
@@ -183,10 +184,12 @@
         // detail widgets occupy [90, 100]
         whenever(detailWidgetState.y).thenReturn(90F)
         whenever(detailWidgetState.height).thenReturn(10)
+        whenever(detailWidgetState.alpha).thenReturn(1F)
         // control widgets occupy [150, 170]
         whenever(controlWidgetState.y).thenReturn(150F)
         whenever(controlWidgetState.height).thenReturn(20)
-        // in current beizer, when the progress reach 0.38, the result will be 0.5
+        whenever(controlWidgetState.alpha).thenReturn(1F)
+        // in current bezier, when the progress reach 0.38, the result will be 0.5
         mediaViewController.squishViewState(mockViewState, 181.4F / 200F)
         verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
         verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
@@ -196,6 +199,34 @@
     }
 
     @Test
+    fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_invisibleElements() {
+        whenever(mockViewState.copy()).thenReturn(mockCopiedState)
+        whenever(mockCopiedState.widgetStates)
+            .thenReturn(
+                mutableMapOf(
+                    R.id.media_progress_bar to controlWidgetState,
+                    R.id.header_artist to detailWidgetState
+                )
+            )
+        whenever(mockCopiedState.measureHeight).thenReturn(200)
+        // detail widgets occupy [90, 100]
+        whenever(detailWidgetState.y).thenReturn(90F)
+        whenever(detailWidgetState.height).thenReturn(10)
+        whenever(detailWidgetState.alpha).thenReturn(0F)
+        // control widgets occupy [150, 170]
+        whenever(controlWidgetState.y).thenReturn(150F)
+        whenever(controlWidgetState.height).thenReturn(20)
+        whenever(controlWidgetState.alpha).thenReturn(0F)
+        // Verify that alpha remains 0 throughout squishing
+        mediaViewController.squishViewState(mockViewState, 181.4F / 200F)
+        verify(controlWidgetState, never()).alpha = floatThat { it > 0 }
+        verify(detailWidgetState, never()).alpha = floatThat { it > 0 }
+        mediaViewController.squishViewState(mockViewState, 200F / 200F)
+        verify(controlWidgetState, never()).alpha = floatThat { it > 0 }
+        verify(detailWidgetState, never()).alpha = floatThat { it > 0 }
+    }
+
+    @Test
     fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() {
         whenever(mockViewState.copy()).thenReturn(mockCopiedState)
         whenever(mockCopiedState.widgetStates)
@@ -210,12 +241,15 @@
         // media container widgets occupy [20, 300]
         whenever(mediaContainerWidgetState.y).thenReturn(20F)
         whenever(mediaContainerWidgetState.height).thenReturn(280)
+        whenever(mediaContainerWidgetState.alpha).thenReturn(1F)
         // media title widgets occupy [320, 330]
         whenever(mediaTitleWidgetState.y).thenReturn(320F)
         whenever(mediaTitleWidgetState.height).thenReturn(10)
+        whenever(mediaTitleWidgetState.alpha).thenReturn(1F)
         // media subtitle widgets occupy [340, 350]
         whenever(mediaSubTitleWidgetState.y).thenReturn(340F)
         whenever(mediaSubTitleWidgetState.height).thenReturn(10)
+        whenever(mediaSubTitleWidgetState.alpha).thenReturn(1F)
 
         // in current beizer, when the progress reach 0.38, the result will be 0.5
         mediaViewController.squishViewState(mockViewState, 307.6F / 360F)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index 4d42324..b7618d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -75,7 +75,6 @@
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runCurrent
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyInt
@@ -463,7 +462,6 @@
 
     // region setNoteTaskShortcutEnabled
     @Test
-    @Ignore("b/316332684")
     fun setNoteTaskShortcutEnabled_setTrue() {
         createNoteTaskController().setNoteTaskShortcutEnabled(value = true, userTracker.userHandle)
 
@@ -480,7 +478,6 @@
     }
 
     @Test
-    @Ignore("b/316332684")
     fun setNoteTaskShortcutEnabled_setFalse() {
         createNoteTaskController().setNoteTaskShortcutEnabled(value = false, userTracker.userHandle)
 
@@ -497,7 +494,6 @@
     }
 
     @Test
-    @Ignore("b/316332684")
     fun setNoteTaskShortcutEnabled_workProfileUser_setTrue() {
         whenever(context.createContextAsUser(eq(workUserInfo.userHandle), any()))
             .thenReturn(workProfileContext)
@@ -519,7 +515,6 @@
     }
 
     @Test
-    @Ignore("b/316332684")
     fun setNoteTaskShortcutEnabled_workProfileUser_setFalse() {
         whenever(context.createContextAsUser(eq(workUserInfo.userHandle), any()))
             .thenReturn(workProfileContext)
@@ -738,7 +733,6 @@
 
     // region internalUpdateNoteTaskAsUser
     @Test
-    @Ignore("b/316332684")
     fun updateNoteTaskAsUserInternal_withNotesRole_withShortcuts_shouldUpdateShortcuts() {
         createNoteTaskController(isEnabled = true)
             .launchUpdateNoteTaskAsUser(userTracker.userHandle)
@@ -772,7 +766,6 @@
     }
 
     @Test
-    @Ignore("b/316332684")
     fun updateNoteTaskAsUserInternal_noNotesRole_shouldDisableShortcuts() {
         whenever(roleManager.getRoleHoldersAsUser(ROLE_NOTES, userTracker.userHandle))
             .thenReturn(emptyList())
@@ -796,7 +789,6 @@
     }
 
     @Test
-    @Ignore("b/316332684")
     fun updateNoteTaskAsUserInternal_flagDisabled_shouldDisableShortcuts() {
         createNoteTaskController(isEnabled = false)
             .launchUpdateNoteTaskAsUser(userTracker.userHandle)
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 5e2423a..ef7798e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -447,10 +447,6 @@
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles_default, "spec1,spec1");
         List<String> specs = QSTileHost.loadTileSpecs(mContext, "default");
-
-        // Remove spurious tiles, like dbg:mem
-        specs.removeIf(spec -> !"spec1".equals(spec));
-        assertEquals(1, specs.size());
     }
 
     @Test
@@ -458,10 +454,6 @@
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles_default, "spec1");
         List<String> specs = QSTileHost.loadTileSpecs(mContext, "default,spec1");
-
-        // Remove spurious tiles, like dbg:mem
-        specs.removeIf(spec -> !"spec1".equals(spec));
-        assertEquals(1, specs.size());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
index 067218a..5201e5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
@@ -50,7 +50,6 @@
 import com.android.systemui.qs.tiles.ScreenRecordTile
 import com.android.systemui.qs.tiles.UiModeNightTile
 import com.android.systemui.qs.tiles.WorkModeTile
-import com.android.systemui.util.leak.GarbageMonitor
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -117,7 +116,6 @@
     @Mock private lateinit var dataSaverTile: DataSaverTile
     @Mock private lateinit var nightDisplayTile: NightDisplayTile
     @Mock private lateinit var nfcTile: NfcTile
-    @Mock private lateinit var memoryTile: GarbageMonitor.MemoryTile
     @Mock private lateinit var darkModeTile: UiModeNightTile
     @Mock private lateinit var screenRecordTile: ScreenRecordTile
     @Mock private lateinit var reduceBrightColorsTile: ReduceBrightColorsTile
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt
index b90ccc0..9941661 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,35 +16,23 @@
 
 package com.android.systemui.scene.shared.flag
 
-import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.FakeFeatureFlagsImpl
-import com.android.systemui.Flags as AconfigFlags
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.compose.ComposeFacade
 import com.android.systemui.flags.Flags
-import com.android.systemui.flags.ReleasedFlag
-import com.android.systemui.flags.ResourceBooleanFlag
-import com.android.systemui.flags.UnreleasedFlag
+import com.android.systemui.flags.setFlagValue
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.media.controls.util.MediaInSceneContainerFlag
-import com.android.systemui.res.R
-import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth
+import org.junit.Assume
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
 
 @SmallTest
-@RunWith(Parameterized::class)
-internal class SceneContainerFlagsTest(
-    private val testCase: TestCase,
-) : SysuiTestCase() {
-
-    @Rule @JvmField val setFlagsRule: SetFlagsRule = SetFlagsRule()
-
-    private lateinit var underTest: SceneContainerFlags
+@RunWith(AndroidJUnit4::class)
+internal class SceneContainerFlagsTest : SysuiTestCase() {
 
     @Before
     fun setUp() {
@@ -52,83 +40,39 @@
         //  Flags.SCENE_CONTAINER_ENABLED is no longer needed.
         val field = Flags::class.java.getField("SCENE_CONTAINER_ENABLED")
         field.isAccessible = true
-        field.set(null, true)
+        field.set(null, true) // note: this does not work with multivalent tests
+    }
 
-        val featureFlags =
-            FakeFeatureFlagsClassic().apply {
-                SceneContainerFlagsImpl.classicFlagTokens.forEach { flagToken ->
-                    when (flagToken) {
-                        is ResourceBooleanFlag -> set(flagToken, testCase.areAllFlagsSet)
-                        is ReleasedFlag -> set(flagToken, testCase.areAllFlagsSet)
-                        is UnreleasedFlag -> set(flagToken, testCase.areAllFlagsSet)
-                        else -> error("Unsupported flag type ${flagToken.javaClass}")
-                    }
-                }
-            }
-        // TODO(b/306421592): get the aconfig FeatureFlags from the SetFlagsRule.
-        val aconfigFlags = FakeFeatureFlagsImpl()
-
+    private fun setAconfigFlagsEnabled(enabled: Boolean) {
         listOf(
-                AconfigFlags.FLAG_SCENE_CONTAINER,
-                AconfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
+                com.android.systemui.Flags.FLAG_SCENE_CONTAINER,
+                com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
                 KeyguardShadeMigrationNssl.FLAG_NAME,
                 MediaInSceneContainerFlag.FLAG_NAME,
             )
-            .forEach { flagToken ->
-                setFlagsRule.enableFlags(flagToken)
-                aconfigFlags.setFlag(flagToken, testCase.areAllFlagsSet)
-                overrideResource(
-                    R.bool.config_sceneContainerFrameworkEnabled,
-                    testCase.isResourceConfigEnabled
-                )
-            }
-
-        underTest =
-            SceneContainerFlagsImpl(
-                context = context,
-                featureFlagsClassic = featureFlags,
-                isComposeAvailable = testCase.isComposeAvailable,
-            )
+            .forEach { flagName -> mSetFlagsRule.setFlagValue(flagName, enabled) }
     }
 
     @Test
-    fun isEnabled() {
-        assertThat(underTest.isEnabled()).isEqualTo(testCase.expectedEnabled)
+    fun isNotEnabled_withoutAconfigFlags() {
+        setAconfigFlagsEnabled(false)
+        Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(false)
+        Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(false)
     }
 
-    internal data class TestCase(
-        val isComposeAvailable: Boolean,
-        val areAllFlagsSet: Boolean,
-        val isResourceConfigEnabled: Boolean,
-        val expectedEnabled: Boolean,
-    ) {
-        override fun toString(): String {
-            return "(compose=$isComposeAvailable + flags=$areAllFlagsSet) + XML" +
-                " config=$isResourceConfigEnabled -> expected=$expectedEnabled"
-        }
+    @Test
+    fun isEnabled_withAconfigFlags_withCompose() {
+        Assume.assumeTrue(ComposeFacade.isComposeAvailable())
+        setAconfigFlagsEnabled(true)
+        Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(true)
+        Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(true)
     }
 
-    companion object {
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun testCases() = buildList {
-            repeat(8) { combination ->
-                val isComposeAvailable = combination and 0b100 != 0
-                val areAllFlagsSet = combination and 0b010 != 0
-                val isResourceConfigEnabled = combination and 0b001 != 0
-
-                val expectedEnabled =
-                    isComposeAvailable && areAllFlagsSet && isResourceConfigEnabled
-
-                add(
-                    TestCase(
-                        isComposeAvailable = isComposeAvailable,
-                        areAllFlagsSet = areAllFlagsSet,
-                        expectedEnabled = expectedEnabled,
-                        isResourceConfigEnabled = isResourceConfigEnabled,
-                    )
-                )
-            }
-        }
+    @Test
+    fun isNotEnabled_withAconfigFlags_withoutCompose() {
+        Assume.assumeFalse(ComposeFacade.isComposeAvailable())
+        setAconfigFlagsEnabled(true)
+        Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(false)
+        Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(false)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
index d4e8d37..72fc65b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
@@ -10,8 +10,6 @@
 import androidx.constraintlayout.widget.Guideline
 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.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
@@ -39,7 +37,6 @@
     lateinit var detectionNoticeView: ViewGroup
     lateinit var container: FrameLayout
 
-    var featureFlags = FakeFeatureFlags()
     lateinit var screenshotView: ViewGroup
 
     val userHandle = UserHandle.of(5)
@@ -55,7 +52,6 @@
             MessageContainerController(
                 workProfileMessageController,
                 screenshotDetectionController,
-                featureFlags
             )
         screenshotView = ConstraintLayout(mContext)
         workProfileData = WorkProfileMessageController.WorkProfileFirstRunData(appName, icon)
@@ -105,8 +101,6 @@
 
     @Test
     fun testOnScreenshotTakenScreenshotData_nothingToShow() {
-        featureFlags.set(Flags.SCREENSHOT_DETECTION, true)
-
         messageContainer.onScreenshotTaken(screenshotData)
 
         verify(workProfileMessageController, never()).populateView(any(), any(), any())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
new file mode 100644
index 0000000..80f8cf1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.notifications.data.repository
+
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.shared.settings.data.repository.FakeSecureSettingsRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class NotificationSettingsRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: NotificationSettingsRepository
+
+    private lateinit var testScope: TestScope
+    private lateinit var secureSettingsRepository: FakeSecureSettingsRepository
+
+    @Before
+    fun setUp() {
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
+        secureSettingsRepository = FakeSecureSettingsRepository()
+
+        underTest =
+            NotificationSettingsRepository(
+                scope = testScope.backgroundScope,
+                backgroundDispatcher = testDispatcher,
+                secureSettingsRepository = secureSettingsRepository,
+            )
+    }
+
+    @Test
+    fun testGetIsShowNotificationsOnLockscreenEnabled() =
+        testScope.runTest {
+            val showNotifs by collectLastValue(underTest.isShowNotificationsOnLockScreenEnabled)
+
+            secureSettingsRepository.set(
+                name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+                value = 1,
+            )
+            assertThat(showNotifs).isEqualTo(true)
+
+            secureSettingsRepository.set(
+                name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+                value = 0,
+            )
+            assertThat(showNotifs).isEqualTo(false)
+        }
+
+    @Test
+    fun testSetIsShowNotificationsOnLockscreenEnabled() =
+        testScope.runTest {
+            val showNotifs by collectLastValue(underTest.isShowNotificationsOnLockScreenEnabled)
+
+            underTest.setShowNotificationsOnLockscreenEnabled(true)
+            assertThat(showNotifs).isEqualTo(true)
+
+            underTest.setShowNotificationsOnLockscreenEnabled(false)
+            assertThat(showNotifs).isEqualTo(false)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
index a56fb2c..7d8cf36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
@@ -1,11 +1,10 @@
 package com.android.systemui.statusbar.notification
 
+import android.platform.test.annotations.EnableFlags
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import org.junit.Assert.assertEquals
@@ -20,8 +19,7 @@
 @RunWith(JUnit4::class)
 class RoundableTest : SysuiTestCase() {
     private val targetView: View = mock()
-    private val featureFlags = FakeFeatureFlags()
-    private val roundable = FakeRoundable(targetView = targetView, featureFlags = featureFlags)
+    private val roundable = FakeRoundable(targetView = targetView)
 
     @Test
     fun defaultConfig_shouldNotHaveRoundedCorner() {
@@ -150,36 +148,36 @@
     }
 
     @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun getCornerRadii_radius_maxed_to_height() {
         whenever(targetView.height).thenReturn(10)
-        featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
         roundable.requestRoundness(1f, 1f, SOURCE1)
 
         assertCornerRadiiEquals(5f, 5f)
     }
 
     @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun getCornerRadii_topRadius_maxed_to_height() {
         whenever(targetView.height).thenReturn(5)
-        featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
         roundable.requestRoundness(1f, 0f, SOURCE1)
 
         assertCornerRadiiEquals(5f, 0f)
     }
 
     @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun getCornerRadii_bottomRadius_maxed_to_height() {
         whenever(targetView.height).thenReturn(5)
-        featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
         roundable.requestRoundness(0f, 1f, SOURCE1)
 
         assertCornerRadiiEquals(0f, 5f)
     }
 
     @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun getCornerRadii_radii_kept() {
         whenever(targetView.height).thenReturn(100)
-        featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
         roundable.requestRoundness(1f, 1f, SOURCE1)
 
         assertCornerRadiiEquals(MAX_RADIUS, MAX_RADIUS)
@@ -193,14 +191,12 @@
     class FakeRoundable(
         targetView: View,
         radius: Float = MAX_RADIUS,
-        featureFlags: FeatureFlags
     ) : Roundable {
         override val roundableState =
             RoundableState(
                 targetView = targetView,
                 roundable = this,
                 maxRadius = radius,
-                featureFlags = featureFlags
             )
 
         override val clipHeight: Int
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
index f3094cd..170f651 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
@@ -80,7 +80,7 @@
             assertThat(isPulseExpanding).isFalse()
 
             withArgCaptor { verify(mockWakeUpCoordinator).addListener(capture()) }
-                .onPulseExpansionChanged(true)
+                .onPulseExpandingChanged(true)
             runCurrent()
 
             assertThat(isPulseExpanding).isTrue()
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 cb73108..0cd834d 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
@@ -57,7 +57,6 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -568,9 +567,6 @@
             NotificationEntry entry,
             @InflationFlag int extraInflationFlags)
             throws Exception {
-        // NOTE: This flag is read when the ExpandableNotificationRow is inflated, so it needs to be
-        //  set, but we do not want to override an existing value that is needed by a specific test.
-        mFeatureFlags.setDefault(Flags.IMPROVED_HUN_ANIMATIONS);
 
         LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
                 mContext.LAYOUT_INFLATER_SERVICE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index c8dbdc5..2df6e46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -28,8 +28,8 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.mock
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
 
 /** Tests for {@link NotificationShelf}. */
 @SmallTest
@@ -53,7 +53,6 @@
         MockitoAnnotations.initMocks(this)
         mDependency.injectTestDependency(FeatureFlags::class.java, flags)
         flags.set(Flags.SENSITIVE_REVEAL_ANIM, useSensitiveReveal)
-        flags.setDefault(Flags.IMPROVED_HUN_ANIMATIONS)
         val root = FrameLayout(context)
         shelf =
             LayoutInflater.from(root.context)
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 ba5ba2c..ad7dee3 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
@@ -82,7 +82,6 @@
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
-import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -116,7 +115,6 @@
     private AmbientState mAmbientState;
     private TestableResources mTestableResources;
     @Rule public MockitoRule mockito = MockitoJUnit.rule();
-    @Mock private NotificationsController mNotificationsController;
     @Mock private SysuiStatusBarStateController mBarState;
     @Mock private GroupMembershipManager mGroupMembershipManger;
     @Mock private GroupExpansionManager mGroupExpansionManager;
@@ -193,7 +191,7 @@
         mStackScrollerInternal.initView(getContext(), mNotificationSwipeHelper,
                 mNotificationStackSizeCalculator);
         mStackScroller = spy(mStackScrollerInternal);
-        mStackScroller.setNotificationsController(mNotificationsController);
+        mStackScroller.setResetUserExpandedStatesRunnable(()->{});
         mStackScroller.setEmptyShadeView(mEmptyShadeView);
         when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true);
         when(mStackScrollLayoutController.getNotificationRoundnessManager())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 08ef477..f266f03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -2,21 +2,28 @@
 
 import android.annotation.DimenRes
 import android.content.pm.PackageManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
 import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.res.R
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
 import com.android.systemui.statusbar.EmptyShadeView
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.RoundableState
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView.FooterViewState
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Expect
@@ -24,6 +31,7 @@
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Rule
@@ -37,22 +45,26 @@
 @SmallTest
 class StackScrollAlgorithmTest : SysuiTestCase() {
 
-    @JvmField @Rule
-    var expect: Expect = Expect.create()
+    @JvmField @Rule var expect: Expect = Expect.create()
 
     private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>()
 
     private val hostView = FrameLayout(context)
     private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView)
     private val notificationRow = mock<ExpandableNotificationRow>()
+    private val notificationEntry = mock<NotificationEntry>()
     private val dumpManager = mock<DumpManager>()
+    @OptIn(ExperimentalCoroutinesApi::class)
     private val mStatusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>()
     private val notificationShelf = mock<NotificationShelf>()
-    private val emptyShadeView = EmptyShadeView(context, /* attrs= */ null).apply {
-        layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
-    }
-    private val footerView = FooterView(context, /*attrs=*/null)
-    private val ambientState = AmbientState(
+    private val emptyShadeView =
+        EmptyShadeView(context, /* attrs= */ null).apply {
+            layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
+        }
+    private val footerView = FooterView(context, /*attrs=*/ null)
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private val ambientState =
+        AmbientState(
             context,
             dumpManager,
             /* sectionProvider */ { _, _ -> false },
@@ -62,13 +74,14 @@
         )
 
     private val testableResources = mContext.getOrCreateTestableResources()
+    private val featureFlags = mock<FeatureFlagsClassic>()
     private val maxPanelHeight =
         mContext.resources.displayMetrics.heightPixels -
-                px(R.dimen.notification_panel_margin_top) -
-                px(R.dimen.notification_panel_margin_bottom)
+            px(R.dimen.notification_panel_margin_top) -
+            px(R.dimen.notification_panel_margin_bottom)
 
     private fun px(@DimenRes id: Int): Float =
-            testableResources.resources.getDimensionPixelSize(id).toFloat()
+        testableResources.resources.getDimensionPixelSize(id).toFloat()
 
     private val bigGap = px(R.dimen.notification_section_divider_height)
     private val smallGap = px(R.dimen.notification_section_divider_height_lockscreen)
@@ -76,9 +89,12 @@
     @Before
     fun setUp() {
         Assume.assumeFalse(isTv())
-
+        mDependency.injectTestDependency(FeatureFlags::class.java, featureFlags)
         whenever(notificationShelf.viewState).thenReturn(ExpandableViewState())
         whenever(notificationRow.viewState).thenReturn(ExpandableViewState())
+        whenever(notificationRow.entry).thenReturn(notificationEntry)
+        whenever(notificationRow.roundableState)
+            .thenReturn(RoundableState(notificationRow, notificationRow, 0f))
         ambientState.isSmallScreen = true
 
         hostView.addView(notificationRow)
@@ -92,7 +108,7 @@
     fun resetViewStates_defaultHun_yTranslationIsInset() {
         whenever(notificationRow.isPinned).thenReturn(true)
         whenever(notificationRow.isHeadsUp).thenReturn(true)
-        resetViewStates_hunYTranslationIsInset()
+        resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset)
     }
 
     @Test
@@ -103,18 +119,87 @@
     }
 
     @Test
+    @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_hunAnimatingAway_yTranslationIsInset() {
         whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
-        resetViewStates_hunYTranslationIsInset()
+        resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset)
     }
 
     @Test
+    @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_hunAnimatingAway_StackMarginChangesHunYTranslation() {
         whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
         resetViewStates_stackMargin_changesHunYTranslation()
     }
 
     @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun resetViewStates_defaultHun_newHeadsUpAnim_yTranslationIsInset() {
+        whenever(notificationRow.isPinned).thenReturn(true)
+        whenever(notificationRow.isHeadsUp).thenReturn(true)
+        resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset)
+    }
+
+    @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun resetViewStates_defaultHunWithStackMargin_newHeadsUpAnim_changesHunYTranslation() {
+        whenever(notificationRow.isPinned).thenReturn(true)
+        whenever(notificationRow.isHeadsUp).thenReturn(true)
+        resetViewStates_stackMargin_changesHunYTranslation()
+    }
+
+    @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun resetViewStates_defaultHun_showingQS_newHeadsUpAnim_hunTranslatedToMax() {
+        // Given: the shade is open and scrolled to the bottom to show the QuickSettings
+        val maxHunTranslation = 2000f
+        ambientState.maxHeadsUpTranslation = maxHunTranslation
+        ambientState.setLayoutMinHeight(2500) // Mock the height of shade
+        ambientState.stackY = 2500f // Scroll over the max translation
+        stackScrollAlgorithm.setIsExpanded(true) // Mark the shade open
+        whenever(notificationRow.mustStayOnScreen()).thenReturn(true)
+        whenever(notificationRow.isHeadsUp).thenReturn(true)
+        whenever(notificationRow.isAboveShelf).thenReturn(true)
+
+        resetViewStates_hunYTranslationIs(maxHunTranslation)
+    }
+
+    @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun resetViewStates_hunAnimatingAway_showingQS_newHeadsUpAnim_hunTranslatedToBottomOfScreen() {
+        // Given: the shade is open and scrolled to the bottom to show the QuickSettings
+        val bottomOfScreen = 2600f
+        val maxHunTranslation = 2000f
+        ambientState.maxHeadsUpTranslation = maxHunTranslation
+        ambientState.setLayoutMinHeight(2500) // Mock the height of shade
+        ambientState.stackY = 2500f // Scroll over the max translation
+        stackScrollAlgorithm.setIsExpanded(true) // Mark the shade open
+        stackScrollAlgorithm.setHeadsUpAppearHeightBottom(bottomOfScreen.toInt())
+        whenever(notificationRow.mustStayOnScreen()).thenReturn(true)
+        whenever(notificationRow.isHeadsUp).thenReturn(true)
+        whenever(notificationRow.isAboveShelf).thenReturn(true)
+        whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
+
+        resetViewStates_hunYTranslationIs(
+            expected = bottomOfScreen + stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen
+        )
+    }
+
+    @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun resetViewStates_hunAnimatingAway_newHeadsUpAnim_hunTranslatedToTopOfScreen() {
+        val topMargin = 100f
+        ambientState.maxHeadsUpTranslation = 2000f
+        ambientState.stackTopMargin = topMargin.toInt()
+        whenever(notificationRow.intrinsicHeight).thenReturn(100)
+        whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
+
+        resetViewStates_hunYTranslationIs(
+            expected = -topMargin - stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen
+        )
+    }
+
+    @Test
     fun resetViewStates_hunAnimatingAway_bottomNotClipped() {
         whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
 
@@ -136,6 +221,7 @@
     }
 
     @Test
+    @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_hunsOverlappingAndBottomHunAnimatingAway_bottomHunClipped() {
         val topHun = mockExpandableNotificationRow()
         val bottomHun = mockExpandableNotificationRow()
@@ -156,7 +242,7 @@
         stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
 
         val marginBottom =
-                context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom)
+            context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom)
         val fullHeight = ambientState.layoutMaxHeight + marginBottom - ambientState.stackY
         val centeredY = ambientState.stackY + fullHeight / 2f - emptyShadeView.height / 2f
         assertThat(emptyShadeView.viewState.yTranslation).isEqualTo(centeredY)
@@ -174,33 +260,37 @@
         assertThat(notificationRow.viewState.alpha).isEqualTo(1f)
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun resetViewStates_expansionChanging_notificationBecomesTransparent() {
         whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false)
         resetViewStates_expansionChanging_notificationAlphaUpdated(
-                expansionFraction = 0.25f,
-                expectedAlpha = 0.0f
+            expansionFraction = 0.25f,
+            expectedAlpha = 0.0f
         )
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun resetViewStates_expansionChangingWhileBouncerInTransit_viewBecomesTransparent() {
         whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true)
         resetViewStates_expansionChanging_notificationAlphaUpdated(
-                expansionFraction = 0.85f,
-                expectedAlpha = 0.0f
+            expansionFraction = 0.85f,
+            expectedAlpha = 0.0f
         )
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun resetViewStates_expansionChanging_notificationAlphaUpdated() {
         whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false)
         resetViewStates_expansionChanging_notificationAlphaUpdated(
-                expansionFraction = 0.6f,
-                expectedAlpha = getContentAlpha(0.6f)
+            expansionFraction = 0.6f,
+            expectedAlpha = getContentAlpha(0.6f)
         )
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun resetViewStates_largeScreen_expansionChanging_alphaUpdated_largeScreenValue() {
         val expansionFraction = 0.6f
@@ -216,13 +306,14 @@
         )
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun expansionChanging_largeScreen_bouncerInTransit_alphaUpdated_bouncerValues() {
         ambientState.isSmallScreen = false
         whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true)
         resetViewStates_expansionChanging_notificationAlphaUpdated(
-                expansionFraction = 0.95f,
-                expectedAlpha = aboutToShowBouncerProgress(0.95f),
+            expansionFraction = 0.95f,
+            expectedAlpha = aboutToShowBouncerProgress(0.95f),
         )
     }
 
@@ -235,10 +326,8 @@
 
         stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
 
-        verify(notificationShelf).updateState(
-                /* algorithmState= */any(),
-                /* ambientState= */eq(ambientState)
-        )
+        verify(notificationShelf)
+            .updateState(/* algorithmState= */ any(), /* ambientState= */ eq(ambientState))
     }
 
     @Test
@@ -397,22 +486,31 @@
 
     @Test
     fun getGapForLocation_onLockscreen_returnsSmallGap() {
-        val gap = stackScrollAlgorithm.getGapForLocation(
-                /* fractionToShade= */ 0f, /* onKeyguard= */ true)
+        val gap =
+            stackScrollAlgorithm.getGapForLocation(
+                /* fractionToShade= */ 0f,
+                /* onKeyguard= */ true
+            )
         assertThat(gap).isEqualTo(smallGap)
     }
 
     @Test
     fun getGapForLocation_goingToShade_interpolatesGap() {
-        val gap = stackScrollAlgorithm.getGapForLocation(
-                /* fractionToShade= */ 0.5f, /* onKeyguard= */ true)
+        val gap =
+            stackScrollAlgorithm.getGapForLocation(
+                /* fractionToShade= */ 0.5f,
+                /* onKeyguard= */ true
+            )
         assertThat(gap).isEqualTo(smallGap * 0.5f + bigGap * 0.5f)
     }
 
     @Test
     fun getGapForLocation_notOnLockscreen_returnsBigGap() {
-        val gap = stackScrollAlgorithm.getGapForLocation(
-                /* fractionToShade= */ 0f, /* onKeyguard= */ false)
+        val gap =
+            stackScrollAlgorithm.getGapForLocation(
+                /* fractionToShade= */ 0f,
+                /* onKeyguard= */ false
+            )
         assertThat(gap).isEqualTo(bigGap)
     }
 
@@ -469,12 +567,14 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.headsUpIsVisible = false
 
-        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
-                /* isShadeExpanded= */ true,
-                /* mustStayOnScreen= */ true,
-                /* isViewEndVisible= */ true,
-                /* viewEnd= */ 0f,
-                /* maxHunY= */ 10f)
+        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
+            expandableViewState,
+            /* isShadeExpanded= */ true,
+            /* mustStayOnScreen= */ true,
+            /* isViewEndVisible= */ true,
+            /* viewEnd= */ 0f,
+            /* maxHunY= */ 10f
+        )
 
         assertTrue(expandableViewState.headsUpIsVisible)
     }
@@ -484,12 +584,14 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.headsUpIsVisible = true
 
-        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
-                /* isShadeExpanded= */ true,
-                /* mustStayOnScreen= */ true,
-                /* isViewEndVisible= */ true,
-                /* viewEnd= */ 10f,
-                /* maxHunY= */ 0f)
+        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
+            expandableViewState,
+            /* isShadeExpanded= */ true,
+            /* mustStayOnScreen= */ true,
+            /* isViewEndVisible= */ true,
+            /* viewEnd= */ 10f,
+            /* maxHunY= */ 0f
+        )
 
         assertFalse(expandableViewState.headsUpIsVisible)
     }
@@ -499,12 +601,14 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.headsUpIsVisible = true
 
-        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
-                /* isShadeExpanded= */ false,
-                /* mustStayOnScreen= */ true,
-                /* isViewEndVisible= */ true,
-                /* viewEnd= */ 10f,
-                /* maxHunY= */ 1f)
+        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
+            expandableViewState,
+            /* isShadeExpanded= */ false,
+            /* mustStayOnScreen= */ true,
+            /* isViewEndVisible= */ true,
+            /* viewEnd= */ 10f,
+            /* maxHunY= */ 1f
+        )
 
         assertTrue(expandableViewState.headsUpIsVisible)
     }
@@ -514,12 +618,14 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.headsUpIsVisible = true
 
-        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
-                /* isShadeExpanded= */ true,
-                /* mustStayOnScreen= */ false,
-                /* isViewEndVisible= */ true,
-                /* viewEnd= */ 10f,
-                /* maxHunY= */ 1f)
+        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
+            expandableViewState,
+            /* isShadeExpanded= */ true,
+            /* mustStayOnScreen= */ false,
+            /* isViewEndVisible= */ true,
+            /* viewEnd= */ 10f,
+            /* maxHunY= */ 1f
+        )
 
         assertTrue(expandableViewState.headsUpIsVisible)
     }
@@ -529,12 +635,14 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.headsUpIsVisible = true
 
-        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
-                /* isShadeExpanded= */ true,
-                /* mustStayOnScreen= */ true,
-                /* isViewEndVisible= */ false,
-                /* viewEnd= */ 10f,
-                /* maxHunY= */ 1f)
+        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
+            expandableViewState,
+            /* isShadeExpanded= */ true,
+            /* mustStayOnScreen= */ true,
+            /* isViewEndVisible= */ false,
+            /* viewEnd= */ 10f,
+            /* maxHunY= */ 1f
+        )
 
         assertTrue(expandableViewState.headsUpIsVisible)
     }
@@ -544,9 +652,12 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.yTranslation = 50f
 
-        stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
-                /* stackTranslation= */ 0f,
-                /* collapsedHeight= */ 1f, expandableViewState)
+        stackScrollAlgorithm.clampHunToTop(
+            /* quickQsOffsetHeight= */ 10f,
+            /* stackTranslation= */ 0f,
+            /* collapsedHeight= */ 1f,
+            expandableViewState
+        )
 
         // qqs (10 + 0) < viewY (50)
         assertEquals(50f, expandableViewState.yTranslation)
@@ -557,9 +668,12 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.yTranslation = -10f
 
-        stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
-                /* stackTranslation= */ 0f,
-                /* collapsedHeight= */ 1f, expandableViewState)
+        stackScrollAlgorithm.clampHunToTop(
+            /* quickQsOffsetHeight= */ 10f,
+            /* stackTranslation= */ 0f,
+            /* collapsedHeight= */ 1f,
+            expandableViewState
+        )
 
         // qqs (10 + 0) > viewY (-10)
         assertEquals(10f, expandableViewState.yTranslation)
@@ -571,9 +685,12 @@
         expandableViewState.height = 20
         expandableViewState.yTranslation = -100f
 
-        stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
-                /* stackTranslation= */ 0f,
-                /* collapsedHeight= */ 10f, expandableViewState)
+        stackScrollAlgorithm.clampHunToTop(
+            /* quickQsOffsetHeight= */ 10f,
+            /* stackTranslation= */ 0f,
+            /* collapsedHeight= */ 10f,
+            expandableViewState
+        )
 
         // newTranslation = max(10, -100) = 10
         // distToRealY = 10 - (-100f) = 110
@@ -587,9 +704,12 @@
         expandableViewState.height = 20
         expandableViewState.yTranslation = 5f
 
-        stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
-                /* stackTranslation= */ 0f,
-                /* collapsedHeight= */ 10f, expandableViewState)
+        stackScrollAlgorithm.clampHunToTop(
+            /* quickQsOffsetHeight= */ 10f,
+            /* stackTranslation= */ 0f,
+            /* collapsedHeight= */ 10f,
+            expandableViewState
+        )
 
         // newTranslation = max(10, 5) = 10
         // distToRealY = 10 - 5 = 5
@@ -599,41 +719,49 @@
 
     @Test
     fun computeCornerRoundnessForPinnedHun_stackBelowScreen_round() {
-        val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
+        val currentRoundness =
+            stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
                 /* hostViewHeight= */ 100f,
                 /* stackY= */ 110f,
                 /* viewMaxHeight= */ 20f,
-                /* originalCornerRoundness= */ 0f)
+                /* originalCornerRoundness= */ 0f
+            )
         assertEquals(1f, currentRoundness)
     }
 
     @Test
     fun computeCornerRoundnessForPinnedHun_stackAboveScreenBelowPinPoint_halfRound() {
-        val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
+        val currentRoundness =
+            stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
                 /* hostViewHeight= */ 100f,
                 /* stackY= */ 90f,
                 /* viewMaxHeight= */ 20f,
-                /* originalCornerRoundness= */ 0f)
+                /* originalCornerRoundness= */ 0f
+            )
         assertEquals(0.5f, currentRoundness)
     }
 
     @Test
     fun computeCornerRoundnessForPinnedHun_stackAbovePinPoint_notRound() {
-        val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
+        val currentRoundness =
+            stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
                 /* hostViewHeight= */ 100f,
                 /* stackY= */ 0f,
                 /* viewMaxHeight= */ 20f,
-                /* originalCornerRoundness= */ 0f)
+                /* originalCornerRoundness= */ 0f
+            )
         assertEquals(0f, currentRoundness)
     }
 
     @Test
     fun computeCornerRoundnessForPinnedHun_originallyRoundAndStackAbovePinPoint_round() {
-        val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
+        val currentRoundness =
+            stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
                 /* hostViewHeight= */ 100f,
                 /* stackY= */ 0f,
                 /* viewMaxHeight= */ 20f,
-                /* originalCornerRoundness= */ 1f)
+                /* originalCornerRoundness= */ 1f
+            )
         assertEquals(1f, currentRoundness)
     }
 
@@ -642,23 +770,20 @@
         // Given: shade is opened, yTranslation of HUN is 0,
         // the height of HUN equals to the height of QQS Panel,
         // and HUN fully overlaps with QQS Panel
-        ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) +
-                px(R.dimen.qqs_layout_padding_bottom)
-        val childHunView = createHunViewMock(
-                isShadeOpen = true,
-                fullyVisible = false,
-                headerVisibleAmount = 1f
-        )
+        ambientState.stackTranslation =
+            px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom)
+        val childHunView =
+            createHunViewMock(isShadeOpen = true, fullyVisible = false, headerVisibleAmount = 1f)
         val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
         algorithmState.visibleChildren.add(childHunView)
 
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
-                /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
-                /* StackScrollAlgorithmState= */ algorithmState,
-                /* ambientState= */ ambientState,
-                /* shouldElevateHun= */ true
+            /* i= */ 0,
+            /* childrenOnTop= */ 0.0f,
+            /* StackScrollAlgorithmState= */ algorithmState,
+            /* ambientState= */ ambientState,
+            /* shouldElevateHun= */ true
         )
 
         // Then: full shadow would be applied
@@ -670,13 +795,10 @@
         // Given: shade is opened, yTranslation of HUN is greater than 0,
         // the height of HUN is equal to the height of QQS Panel,
         // and HUN partially overlaps with QQS Panel
-        ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) +
-                px(R.dimen.qqs_layout_padding_bottom)
-        val childHunView = createHunViewMock(
-                isShadeOpen = true,
-                fullyVisible = false,
-                headerVisibleAmount = 1f
-        )
+        ambientState.stackTranslation =
+            px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom)
+        val childHunView =
+            createHunViewMock(isShadeOpen = true, fullyVisible = false, headerVisibleAmount = 1f)
         // Use half of the HUN's height as overlap
         childHunView.viewState.yTranslation = (childHunView.viewState.height + 1 shr 1).toFloat()
         val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
@@ -684,17 +806,17 @@
 
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
-                /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
-                /* StackScrollAlgorithmState= */ algorithmState,
-                /* ambientState= */ ambientState,
-                /* shouldElevateHun= */ true
+            /* i= */ 0,
+            /* childrenOnTop= */ 0.0f,
+            /* StackScrollAlgorithmState= */ algorithmState,
+            /* ambientState= */ ambientState,
+            /* shouldElevateHun= */ true
         )
 
         // Then: HUN should have shadow, but not as full size
         assertThat(childHunView.viewState.zTranslation).isGreaterThan(0.0f)
         assertThat(childHunView.viewState.zTranslation)
-                .isLessThan(px(R.dimen.heads_up_pinned_elevation))
+            .isLessThan(px(R.dimen.heads_up_pinned_elevation))
     }
 
     @Test
@@ -702,28 +824,25 @@
         // Given: shade is opened, yTranslation of HUN is equal to QQS Panel's height,
         // the height of HUN is equal to the height of QQS Panel,
         // and HUN doesn't overlap with QQS Panel
-        ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) +
-                px(R.dimen.qqs_layout_padding_bottom)
+        ambientState.stackTranslation =
+            px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom)
         // Mock the height of shade
         ambientState.setLayoutMinHeight(1000)
-        val childHunView = createHunViewMock(
-                isShadeOpen = true,
-                fullyVisible = true,
-                headerVisibleAmount = 1f
-        )
+        val childHunView =
+            createHunViewMock(isShadeOpen = true, fullyVisible = true, headerVisibleAmount = 1f)
         // HUN doesn't overlap with QQS Panel
-        childHunView.viewState.yTranslation = ambientState.topPadding +
-                ambientState.stackTranslation
+        childHunView.viewState.yTranslation =
+            ambientState.topPadding + ambientState.stackTranslation
         val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
         algorithmState.visibleChildren.add(childHunView)
 
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
-                /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
-                /* StackScrollAlgorithmState= */ algorithmState,
-                /* ambientState= */ ambientState,
-                /* shouldElevateHun= */ true
+            /* i= */ 0,
+            /* childrenOnTop= */ 0.0f,
+            /* StackScrollAlgorithmState= */ algorithmState,
+            /* ambientState= */ ambientState,
+            /* shouldElevateHun= */ true
         )
 
         // Then: HUN should not have shadow
@@ -737,11 +856,8 @@
         ambientState.stackTranslation = -ambientState.topPadding
         // Mock the height of shade
         ambientState.setLayoutMinHeight(1000)
-        val childHunView = createHunViewMock(
-                isShadeOpen = false,
-                fullyVisible = false,
-                headerVisibleAmount = 0f
-        )
+        val childHunView =
+            createHunViewMock(isShadeOpen = false, fullyVisible = false, headerVisibleAmount = 0f)
         childHunView.viewState.yTranslation = 0f
         // Shade is closed, thus childHunView's headerVisibleAmount is 0
         childHunView.headerVisibleAmount = 0f
@@ -750,11 +866,11 @@
 
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
-                /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
-                /* StackScrollAlgorithmState= */ algorithmState,
-                /* ambientState= */ ambientState,
-                /* shouldElevateHun= */ true
+            /* i= */ 0,
+            /* childrenOnTop= */ 0.0f,
+            /* StackScrollAlgorithmState= */ algorithmState,
+            /* ambientState= */ ambientState,
+            /* shouldElevateHun= */ true
         )
 
         // Then: HUN should have full shadow
@@ -768,11 +884,8 @@
         ambientState.stackTranslation = -ambientState.topPadding
         // Mock the height of shade
         ambientState.setLayoutMinHeight(1000)
-        val childHunView = createHunViewMock(
-                isShadeOpen = false,
-                fullyVisible = false,
-                headerVisibleAmount = 0.5f
-        )
+        val childHunView =
+            createHunViewMock(isShadeOpen = false, fullyVisible = false, headerVisibleAmount = 0.5f)
         childHunView.viewState.yTranslation = 0f
         // Shade is being opened, thus childHunView's headerVisibleAmount is between 0 and 1
         // use 0.5 as headerVisibleAmount here
@@ -782,17 +895,17 @@
 
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
-                /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
-                /* StackScrollAlgorithmState= */ algorithmState,
-                /* ambientState= */ ambientState,
-                /* shouldElevateHun= */ true
+            /* i= */ 0,
+            /* childrenOnTop= */ 0.0f,
+            /* StackScrollAlgorithmState= */ algorithmState,
+            /* ambientState= */ ambientState,
+            /* shouldElevateHun= */ true
         )
 
         // Then: HUN should have shadow, but not as full size
         assertThat(childHunView.viewState.zTranslation).isGreaterThan(0.0f)
         assertThat(childHunView.viewState.zTranslation)
-                .isLessThan(px(R.dimen.heads_up_pinned_elevation))
+            .isLessThan(px(R.dimen.heads_up_pinned_elevation))
     }
 
     @Test
@@ -862,134 +975,174 @@
         // stackScrollAlgorithm.resetViewStates is called.
         ambientState.dozeAmount = 0.5f
         setExpansionFractionWithoutShelfDuringAodToLockScreen(
-                ambientState,
-                algorithmState,
-                fraction = 0.5f
+            ambientState,
+            algorithmState,
+            fraction = 0.5f
         )
         stackScrollAlgorithm.resetViewStates(ambientState, 0)
 
         // Then: pulsingNotificationView should show at full height
         assertEquals(
-                stackScrollAlgorithm.getMaxAllowedChildHeight(pulsingNotificationView),
-                pulsingNotificationView.viewState.height
+            stackScrollAlgorithm.getMaxAllowedChildHeight(pulsingNotificationView),
+            pulsingNotificationView.viewState.height
         )
 
         // After: reset dozeAmount and expansionFraction
         ambientState.dozeAmount = 0f
         setExpansionFractionWithoutShelfDuringAodToLockScreen(
-                ambientState,
-                algorithmState,
-                fraction = 1f
+            ambientState,
+            algorithmState,
+            fraction = 1f
         )
     }
 
     // region shouldPinHunToBottomOfExpandedQs
     @Test
     fun shouldHunBeVisibleWhenScrolled_mustStayOnScreenFalse_false() {
-        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
-            /* mustStayOnScreen= */false,
-            /* headsUpIsVisible= */false,
-            /* showingPulsing= */false,
-            /* isOnKeyguard=*/false,
-            /*headsUpOnKeyguard=*/false
-        )).isFalse()
+        assertThat(
+                stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+                    /* mustStayOnScreen= */ false,
+                    /* headsUpIsVisible= */ false,
+                    /* showingPulsing= */ false,
+                    /* isOnKeyguard=*/ false,
+                    /*headsUpOnKeyguard=*/ false
+                )
+            )
+            .isFalse()
     }
 
     @Test
     fun shouldPinHunToBottomOfExpandedQs_headsUpIsVisible_false() {
-        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
-            /* mustStayOnScreen= */true,
-            /* headsUpIsVisible= */true,
-            /* showingPulsing= */false,
-            /* isOnKeyguard=*/false,
-            /*headsUpOnKeyguard=*/false
-        )).isFalse()
+        assertThat(
+                stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+                    /* mustStayOnScreen= */ true,
+                    /* headsUpIsVisible= */ true,
+                    /* showingPulsing= */ false,
+                    /* isOnKeyguard=*/ false,
+                    /*headsUpOnKeyguard=*/ false
+                )
+            )
+            .isFalse()
     }
 
     @Test
     fun shouldHunBeVisibleWhenScrolled_showingPulsing_false() {
-        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
-            /* mustStayOnScreen= */true,
-            /* headsUpIsVisible= */false,
-            /* showingPulsing= */true,
-            /* isOnKeyguard=*/false,
-            /* headsUpOnKeyguard= */false
-        )).isFalse()
+        assertThat(
+                stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+                    /* mustStayOnScreen= */ true,
+                    /* headsUpIsVisible= */ false,
+                    /* showingPulsing= */ true,
+                    /* isOnKeyguard=*/ false,
+                    /* headsUpOnKeyguard= */ false
+                )
+            )
+            .isFalse()
     }
 
     @Test
     fun shouldHunBeVisibleWhenScrolled_isOnKeyguard_false() {
-        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
-            /* mustStayOnScreen= */true,
-            /* headsUpIsVisible= */false,
-            /* showingPulsing= */false,
-            /* isOnKeyguard=*/true,
-            /* headsUpOnKeyguard= */false
-        )).isFalse()
+        assertThat(
+                stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+                    /* mustStayOnScreen= */ true,
+                    /* headsUpIsVisible= */ false,
+                    /* showingPulsing= */ false,
+                    /* isOnKeyguard=*/ true,
+                    /* headsUpOnKeyguard= */ false
+                )
+            )
+            .isFalse()
     }
 
     @Test
     fun shouldHunBeVisibleWhenScrolled_isNotOnKeyguard_true() {
-        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
-            /* mustStayOnScreen= */true,
-            /* headsUpIsVisible= */false,
-            /* showingPulsing= */false,
-            /* isOnKeyguard=*/false,
-            /* headsUpOnKeyguard= */false
-        )).isTrue()
+        assertThat(
+                stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+                    /* mustStayOnScreen= */ true,
+                    /* headsUpIsVisible= */ false,
+                    /* showingPulsing= */ false,
+                    /* isOnKeyguard=*/ false,
+                    /* headsUpOnKeyguard= */ false
+                )
+            )
+            .isTrue()
     }
 
     @Test
     fun shouldHunBeVisibleWhenScrolled_headsUpOnKeyguard_true() {
-        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
-            /* mustStayOnScreen= */true,
-            /* headsUpIsVisible= */false,
-            /* showingPulsing= */false,
-            /* isOnKeyguard=*/true,
-            /* headsUpOnKeyguard= */true
-        )).isTrue()
+        assertThat(
+                stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+                    /* mustStayOnScreen= */ true,
+                    /* headsUpIsVisible= */ false,
+                    /* showingPulsing= */ false,
+                    /* isOnKeyguard=*/ true,
+                    /* headsUpOnKeyguard= */ true
+                )
+            )
+            .isTrue()
+    }
+
+    @Test
+    fun shouldHunAppearFromBottom_hunAtMaxHunTranslation() {
+        ambientState.maxHeadsUpTranslation = 400f
+        val viewState =
+            ExpandableViewState().apply {
+                height = 100
+                yTranslation = ambientState.maxHeadsUpTranslation - height // move it to the max
+            }
+
+        assertTrue(stackScrollAlgorithm.shouldHunAppearFromBottom(ambientState, viewState))
+    }
+
+    @Test
+    fun shouldHunAppearFromBottom_hunBelowMaxHunTranslation() {
+        ambientState.maxHeadsUpTranslation = 400f
+        val viewState =
+            ExpandableViewState().apply {
+                height = 100
+                yTranslation =
+                    ambientState.maxHeadsUpTranslation - height - 1 // move it below the max
+            }
+
+        assertFalse(stackScrollAlgorithm.shouldHunAppearFromBottom(ambientState, viewState))
     }
     // endregion
 
     private fun createHunViewMock(
-            isShadeOpen: Boolean,
-            fullyVisible: Boolean,
-            headerVisibleAmount: Float
+        isShadeOpen: Boolean,
+        fullyVisible: Boolean,
+        headerVisibleAmount: Float
     ) =
-            mock<ExpandableNotificationRow>().apply {
-                val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible)
-                whenever(this.viewState).thenReturn(childViewStateMock)
+        mock<ExpandableNotificationRow>().apply {
+            val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible)
+            whenever(this.viewState).thenReturn(childViewStateMock)
 
-                whenever(this.mustStayOnScreen()).thenReturn(true)
-                whenever(this.headerVisibleAmount).thenReturn(headerVisibleAmount)
-            }
-
+            whenever(this.mustStayOnScreen()).thenReturn(true)
+            whenever(this.headerVisibleAmount).thenReturn(headerVisibleAmount)
+        }
 
     private fun createHunChildViewState(isShadeOpen: Boolean, fullyVisible: Boolean) =
-            ExpandableViewState().apply {
-                // Mock the HUN's height with ambientState.topPadding +
-                // ambientState.stackTranslation
-                height = (ambientState.topPadding + ambientState.stackTranslation).toInt()
-                if (isShadeOpen && fullyVisible) {
-                    yTranslation =
-                            ambientState.topPadding + ambientState.stackTranslation
-                } else {
-                    yTranslation = 0f
-                }
-                headsUpIsVisible = fullyVisible
+        ExpandableViewState().apply {
+            // Mock the HUN's height with ambientState.topPadding +
+            // ambientState.stackTranslation
+            height = (ambientState.topPadding + ambientState.stackTranslation).toInt()
+            if (isShadeOpen && fullyVisible) {
+                yTranslation = ambientState.topPadding + ambientState.stackTranslation
+            } else {
+                yTranslation = 0f
             }
+            headsUpIsVisible = fullyVisible
+        }
 
-    private fun createPulsingViewMock(
-    ) =
-            mock<ExpandableNotificationRow>().apply {
-                whenever(this.viewState).thenReturn(ExpandableViewState())
-                whenever(this.showingPulsing()).thenReturn(true)
-            }
+    private fun createPulsingViewMock() =
+        mock<ExpandableNotificationRow>().apply {
+            whenever(this.viewState).thenReturn(ExpandableViewState())
+            whenever(this.showingPulsing()).thenReturn(true)
+        }
 
     private fun setExpansionFractionWithoutShelfDuringAodToLockScreen(
-            ambientState: AmbientState,
-            algorithmState: StackScrollAlgorithm.StackScrollAlgorithmState,
-            fraction: Float
+        ambientState: AmbientState,
+        algorithmState: StackScrollAlgorithm.StackScrollAlgorithmState,
+        fraction: Float
     ) {
         // showingShelf: false
         algorithmState.firstViewInShelf = null
@@ -1002,11 +1155,10 @@
         ambientState.stackHeight = ambientState.stackEndHeight * fraction
     }
 
-    private fun resetViewStates_hunYTranslationIsInset() {
+    private fun resetViewStates_hunYTranslationIs(expected: Float) {
         stackScrollAlgorithm.resetViewStates(ambientState, 0)
 
-        assertThat(notificationRow.viewState.yTranslation)
-                .isEqualTo(stackScrollAlgorithm.mHeadsUpInset)
+        assertThat(notificationRow.viewState.yTranslation).isEqualTo(expected)
     }
 
     private fun resetViewStates_stackMargin_changesHunYTranslation() {
@@ -1025,13 +1177,13 @@
     }
 
     private fun resetViewStates_hunsOverlapping_bottomHunClipped(
-            topHun: ExpandableNotificationRow,
-            bottomHun: ExpandableNotificationRow
+        topHun: ExpandableNotificationRow,
+        bottomHun: ExpandableNotificationRow
     ) {
-        val topHunHeight = mContext.resources.getDimensionPixelSize(
-                R.dimen.notification_content_min_height)
-        val bottomHunHeight = mContext.resources.getDimensionPixelSize(
-                R.dimen.notification_max_heads_up_height)
+        val topHunHeight =
+            mContext.resources.getDimensionPixelSize(R.dimen.notification_content_min_height)
+        val bottomHunHeight =
+            mContext.resources.getDimensionPixelSize(R.dimen.notification_max_heads_up_height)
         whenever(topHun.intrinsicHeight).thenReturn(topHunHeight)
         whenever(bottomHun.intrinsicHeight).thenReturn(bottomHunHeight)
 
@@ -1054,8 +1206,8 @@
     }
 
     private fun resetViewStates_expansionChanging_notificationAlphaUpdated(
-            expansionFraction: Float,
-            expectedAlpha: Float,
+        expansionFraction: Float,
+        expectedAlpha: Float,
     ) {
         ambientState.isExpansionChanging = true
         ambientState.expansionFraction = expansionFraction
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
new file mode 100644
index 0000000..5a57035
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack
+
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.any
+import org.mockito.Mockito.description
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.verify
+
+private const val VIEW_HEIGHT = 100
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class StackStateAnimatorTest : SysuiTestCase() {
+
+    private lateinit var stackStateAnimator: StackStateAnimator
+    private val stackScroller: NotificationStackScrollLayout = mock()
+    private val view: ExpandableView = mock()
+    private val viewState: ExpandableViewState =
+        ExpandableViewState().apply { height = VIEW_HEIGHT }
+    private val runnableCaptor: ArgumentCaptor<Runnable> = argumentCaptor()
+    @Before
+    fun setUp() {
+        whenever(stackScroller.context).thenReturn(context)
+        whenever(view.viewState).thenReturn(viewState)
+        stackStateAnimator = StackStateAnimator(stackScroller)
+    }
+
+    @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun startAnimationForEvents_headsUpFromTop_startsHeadsUpAppearAnim() {
+        val topMargin = 50f
+        val expectedStartY = -topMargin - stackStateAnimator.mHeadsUpAppearStartAboveScreen
+        val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR)
+        stackStateAnimator.setStackTopMargin(topMargin.toInt())
+
+        stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0)
+
+        verify(view).setActualHeight(VIEW_HEIGHT, false)
+        verify(view, description("should animate from the top")).translationY = expectedStartY
+        verify(view)
+            .performAddAnimation(
+                /* delay= */ 0L,
+                /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(),
+                /* isHeadsUpAppear= */ true,
+                /* onEndRunnable= */ null
+            )
+    }
+
+    @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim() {
+        val screenHeight = 2000f
+        val expectedStartY = screenHeight + stackStateAnimator.mHeadsUpAppearStartAboveScreen
+        val event =
+            AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR).apply {
+                headsUpFromBottom = true
+            }
+        stackStateAnimator.setHeadsUpAppearHeightBottom(screenHeight.toInt())
+
+        stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0)
+
+        verify(view).setActualHeight(VIEW_HEIGHT, false)
+        verify(view, description("should animate from the bottom")).translationY = expectedStartY
+        verify(view)
+            .performAddAnimation(
+                /* delay= */ 0L,
+                /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(),
+                /* isHeadsUpAppear= */ true,
+                /* onEndRunnable= */ null
+            )
+    }
+
+    @Test
+    fun startAnimationForEvents_startsHeadsUpDisappearAnim() {
+        val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR)
+        stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0)
+
+        verify(view)
+            .performRemoveAnimation(
+                /* duration= */ eq(ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong()),
+                /* delay= */ eq(0L),
+                /* translationDirection= */ eq(0f),
+                /* isHeadsUpAnimation= */ eq(true),
+                /* onStartedRunnable= */ any(),
+                /* onFinishedRunnable= */ runnableCaptor.capture(),
+                /* animationListener= */ any()
+            )
+
+        runnableCaptor.value.run() // execute the end runnable
+
+        verify(view, description("should be called at the end of the animation"))
+            .removeFromTransientContainer()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt
index da543d4..cd6bb5f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt
@@ -17,10 +17,10 @@
 package com.android.systemui.statusbar.notification.stack
 
 import android.testing.AndroidTestingRunner
-import android.util.Log
-import android.util.Log.TerribleFailureHandler
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.assertDoesNotLogWtf
+import com.android.systemui.log.assertLogsWtf
 import kotlin.math.log2
 import kotlin.math.sqrt
 import org.junit.Assert
@@ -32,61 +32,36 @@
 class ViewStateTest : SysuiTestCase() {
     private val viewState = ViewState()
 
-    private var wtfHandler: TerribleFailureHandler? = null
-    private var wtfCount = 0
-
     @Suppress("DIVISION_BY_ZERO")
     @Test
     fun testWtfs() {
-        interceptWtfs()
-
         // Setting valid values doesn't cause any wtfs.
-        viewState.alpha = 0.1f
-        viewState.xTranslation = 0f
-        viewState.yTranslation = 10f
-        viewState.zTranslation = 20f
-        viewState.scaleX = 0.5f
-        viewState.scaleY = 0.25f
-
-        expectWtfs(0)
+        assertDoesNotLogWtf {
+            viewState.alpha = 0.1f
+            viewState.xTranslation = 0f
+            viewState.yTranslation = 10f
+            viewState.zTranslation = 20f
+            viewState.scaleX = 0.5f
+            viewState.scaleY = 0.25f
+        }
 
         // Setting NaN values leads to wtfs being logged, and the value not being changed.
-        viewState.alpha = 0.0f / 0.0f
-        expectWtfs(1)
+        assertLogsWtf { viewState.alpha = 0.0f / 0.0f }
         Assert.assertEquals(viewState.alpha, 0.1f)
 
-        viewState.xTranslation = Float.NaN
-        expectWtfs(2)
+        assertLogsWtf { viewState.xTranslation = Float.NaN }
         Assert.assertEquals(viewState.xTranslation, 0f)
 
-        viewState.yTranslation = log2(-10.0).toFloat()
-        expectWtfs(3)
+        assertLogsWtf { viewState.yTranslation = log2(-10.0).toFloat() }
         Assert.assertEquals(viewState.yTranslation, 10f)
 
-        viewState.zTranslation = sqrt(-1.0).toFloat()
-        expectWtfs(4)
+        assertLogsWtf { viewState.zTranslation = sqrt(-1.0).toFloat() }
         Assert.assertEquals(viewState.zTranslation, 20f)
 
-        viewState.scaleX = Float.POSITIVE_INFINITY + Float.NEGATIVE_INFINITY
-        expectWtfs(5)
+        assertLogsWtf { viewState.scaleX = Float.POSITIVE_INFINITY + Float.NEGATIVE_INFINITY }
         Assert.assertEquals(viewState.scaleX, 0.5f)
 
-        viewState.scaleY = Float.POSITIVE_INFINITY * 0
-        expectWtfs(6)
+        assertLogsWtf { viewState.scaleY = Float.POSITIVE_INFINITY * 0 }
         Assert.assertEquals(viewState.scaleY, 0.25f)
     }
-
-    private fun interceptWtfs() {
-        wtfCount = 0
-        wtfHandler =
-            Log.setWtfHandler { _: String?, e: Log.TerribleFailure, _: Boolean ->
-                Log.e("ViewStateTest", "Observed WTF: $e")
-                wtfCount++
-            }
-    }
-
-    private fun expectWtfs(expectedWtfCount: Int) {
-        Assert.assertNotNull(wtfHandler)
-        Assert.assertEquals(expectedWtfCount, wtfCount)
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 1cc611c..14751c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -43,7 +43,6 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiBaseFragmentTest;
 import com.android.systemui.animation.AnimatorTestRule;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.log.LogBuffer;
@@ -57,9 +56,7 @@
 import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarNotificationIconViewStore;
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager;
@@ -70,7 +67,6 @@
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeCollapsedStatusBarViewBinder;
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeCollapsedStatusBarViewModel;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsState;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
 import com.android.systemui.util.CarrierConfigTracker;
@@ -702,7 +698,7 @@
                 mKeyguardStateController,
                 mShadeViewController,
                 mStatusBarStateController,
-                mock(StatusBarIconViewBindingFailureTracker.class),
+                mock(NotificationIconContainerStatusBarViewBinder.class),
                 mCommandQueue,
                 mCarrierConfigTracker,
                 new CollapsedStatusBarFragmentLogger(
@@ -715,10 +711,6 @@
                 mDumpManager,
                 mStatusBarWindowStateController,
                 mKeyguardUpdateMonitor,
-                mock(NotificationIconContainerStatusBarViewModel.class),
-                mock(ConfigurationState.class),
-                mock(SystemBarUtilsState.class),
-                mock(StatusBarNotificationIconViewStore.class),
                 mock(DemoModeController.class));
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index 0f779d9..44fa132 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -16,7 +16,8 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
 
-import android.platform.test.flag.junit.SetFlagsRule
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH
@@ -73,8 +74,6 @@
 class MobileIconViewModelTest : SysuiTestCase() {
     private var connectivityRepository = FakeConnectivityRepository()
 
-    private val setFlagsRule = SetFlagsRule()
-
     private lateinit var underTest: MobileIconViewModel
     private lateinit var interactor: MobileIconInteractorImpl
     private lateinit var iconsInteractor: MobileIconsInteractorImpl
@@ -561,11 +560,9 @@
         }
 
     @Test
+    @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
     fun dataActivity_configOn_testIndicators_staticFlagOff() =
         testScope.runTest {
-            // GIVEN STATUS_BAR_STATIC_NETWORK_INDICATORS flag is off
-            setFlagsRule.disableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
-
             // Create a new view model here so the constants are properly read
             whenever(constants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
@@ -618,11 +615,9 @@
         }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
     fun dataActivity_configOn_testIndicators_staticFlagOn() =
         testScope.runTest {
-            // GIVEN STATUS_BAR_STATIC_NETWORK_INDICATORS flag is on
-            setFlagsRule.enableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
-
             // Create a new view model here so the constants are properly read
             whenever(constants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
index bf851eb..6714c94 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
@@ -1115,6 +1115,7 @@
                 broadcastDispatcher = fakeBroadcastDispatcher,
                 keyguardUpdateMonitor = keyguardUpdateMonitor,
                 backgroundDispatcher = utils.testDispatcher,
+                mainDispatcher = utils.testDispatcher,
                 activityManager = activityManager,
                 refreshUsersScheduler = refreshUsersScheduler,
                 guestUserInteractor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
index d1870b1..21d4549 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
@@ -258,6 +258,7 @@
                     broadcastDispatcher = fakeBroadcastDispatcher,
                     keyguardUpdateMonitor = keyguardUpdateMonitor,
                     backgroundDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
                     activityManager = activityManager,
                     refreshUsersScheduler = refreshUsersScheduler,
                     guestUserInteractor = guestUserInteractor,
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 b7b24f6..d0804be 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
@@ -170,6 +170,7 @@
                         broadcastDispatcher = fakeBroadcastDispatcher,
                         keyguardUpdateMonitor = keyguardUpdateMonitor,
                         backgroundDispatcher = testDispatcher,
+                        mainDispatcher = testDispatcher,
                         activityManager = activityManager,
                         refreshUsersScheduler = refreshUsersScheduler,
                         guestUserInteractor = guestUserInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java
deleted file mode 100644
index a2b016f..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java
+++ /dev/null
@@ -1,113 +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.systemui.util.leak;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.testing.AndroidTestingRunner;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.concurrency.MessageRouterImpl;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class GarbageMonitorTest extends SysuiTestCase {
-
-    @Mock private LeakReporter mLeakReporter;
-    @Mock private TrackedGarbage mTrackedGarbage;
-    @Mock private DumpManager mDumpManager;
-    private GarbageMonitor mGarbageMonitor;
-    private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mGarbageMonitor =
-                new GarbageMonitor(
-                        mContext,
-                        mFakeExecutor,
-                        new MessageRouterImpl(mFakeExecutor),
-                        new LeakDetector(null, mTrackedGarbage, null, mDumpManager),
-                        mLeakReporter,
-                        mDumpManager);
-    }
-
-    @Test
-    public void testALittleGarbage_doesntDump() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE);
-
-        mGarbageMonitor.reinspectGarbageAfterGc();
-
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
-    }
-
-    @Test
-    public void testTransientGarbage_doesntDump() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-
-        // Start the leak monitor. Nothing gets reported immediately.
-        mGarbageMonitor.startLeakMonitor();
-        mFakeExecutor.runAllReady();
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
-
-        // Garbage gets reset to 0 before the leak reporte actually gets called.
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(0);
-        mFakeExecutor.advanceClockToLast();
-        mFakeExecutor.runAllReady();
-
-        // Therefore nothing gets dumped.
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
-    }
-
-    @Test
-    public void testLotsOfPersistentGarbage_dumps() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-
-        mGarbageMonitor.reinspectGarbageAfterGc();
-
-        verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-    }
-
-    @Test
-    public void testLotsOfPersistentGarbage_dumpsAfterAtime() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-
-        // Start the leak monitor. Nothing gets reported immediately.
-        mGarbageMonitor.startLeakMonitor();
-        mFakeExecutor.runAllReady();
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
-
-        mFakeExecutor.advanceClockToLast();
-        mFakeExecutor.runAllReady();
-
-        verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt
index e59e475..7801684 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt
@@ -28,8 +28,8 @@
 import com.android.systemui.model.SysUiStateTest
 import com.android.wm.shell.bubbles.Bubble
 import com.android.wm.shell.bubbles.BubbleEducationController
-import com.android.wm.shell.bubbles.PREF_MANAGED_EDUCATION
-import com.android.wm.shell.bubbles.PREF_STACK_EDUCATION
+import com.android.wm.shell.bubbles.ManageEducationView.Companion.PREF_MANAGED_EDUCATION
+import com.android.wm.shell.bubbles.StackEducationView.Companion.PREF_STACK_EDUCATION
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.MoreExecutors.directExecutor
 import org.junit.Assert.assertEquals
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 b217195..814ea19 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -29,8 +29,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -50,6 +48,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+
 import android.app.ActivityManager;
 import android.app.IActivityManager;
 import android.app.INotificationManager;
@@ -186,7 +186,7 @@
 import com.android.wm.shell.bubbles.BubbleViewInfoTask;
 import com.android.wm.shell.bubbles.BubbleViewProvider;
 import com.android.wm.shell.bubbles.Bubbles;
-import com.android.wm.shell.bubbles.StackEducationViewKt;
+import com.android.wm.shell.bubbles.StackEducationView;
 import com.android.wm.shell.bubbles.properties.BubbleProperties;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.FloatingContentCoordinator;
@@ -1930,7 +1930,7 @@
     @Test
     public void testShowStackEdu_isNotConversationBubble() {
         // Setup
-        setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, false);
+        setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, false);
         BubbleEntry bubbleEntry = createBubbleEntry(false /* isConversation */);
         mBubbleController.updateBubble(bubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
@@ -1948,7 +1948,7 @@
     @Test
     public void testShowStackEdu_isConversationBubble() {
         // Setup
-        setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, false);
+        setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, false);
         BubbleEntry bubbleEntry = createBubbleEntry(true /* isConversation */);
         mBubbleController.updateBubble(bubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
@@ -1966,7 +1966,7 @@
     @Test
     public void testShowStackEdu_isSeenConversationBubble() {
         // Setup
-        setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, true);
+        setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, true);
         BubbleEntry bubbleEntry = createBubbleEntry(true /* isConversation */);
         mBubbleController.updateBubble(bubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryKosmos.kt
index a464fa8..fa79580 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryKosmos.kt
@@ -16,10 +16,8 @@
 
 package com.android.systemui.accessibility.data.repository
 
-import android.view.accessibility.accessibilityManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 
-val Kosmos.accessibilityRepository by Fixture {
-    AccessibilityRepository.invoke(a11yManager = accessibilityManager)
-}
+val Kosmos.fakeAccessibilityRepository by Fixture { FakeAccessibilityRepository() }
+val Kosmos.accessibilityRepository by Fixture { fakeAccessibilityRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/AuthControllerKosmos.kt
similarity index 71%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/AuthControllerKosmos.kt
index f7f16a4..7f70785 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/AuthControllerKosmos.kt
@@ -14,12 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.icon.ui.viewbinder
+package com.android.systemui.biometrics
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.notifCollection
+import com.android.systemui.util.mockito.mock
 
-val Kosmos.shelfNotificationIconViewStore by Fixture {
-    ShelfNotificationIconViewStore(notifCollection = notifCollection)
-}
+var Kosmos.authController by Fixture { mock<AuthController>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorKosmos.kt
new file mode 100644
index 0000000..cbfc768
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.biometrics.authController
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+
+val Kosmos.udfpsOverlayInteractor by Fixture {
+    UdfpsOverlayInteractor(
+        context = applicationContext,
+        authController = authController,
+        selectedUserInteractor = selectedUserInteractor,
+        scope = applicationCoroutineScope,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt
new file mode 100644
index 0000000..9175f09
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.deviceentry.data.ui.viewmodel
+
+import com.android.systemui.accessibility.domain.interactor.accessibilityInteractor
+import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor
+import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel
+import com.android.systemui.keyguard.ui.viewmodel.deviceEntryForegroundIconViewModel
+import com.android.systemui.keyguard.ui.viewmodel.deviceEntryIconViewModel
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.udfpsAccessibilityOverlayViewModel by
+    Kosmos.Fixture {
+        UdfpsAccessibilityOverlayViewModel(
+            udfpsOverlayInteractor = udfpsOverlayInteractor,
+            accessibilityInteractor = accessibilityInteractor,
+            deviceEntryIconViewModel = deviceEntryIconViewModel,
+            deviceEntryFgIconViewModel = deviceEntryForegroundIconViewModel,
+        )
+    }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryFgIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryFgIconViewModelKosmos.kt
new file mode 100644
index 0000000..4bfe4f5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryFgIconViewModelKosmos.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor
+import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.deviceEntryForegroundIconViewModel by Fixture {
+    DeviceEntryForegroundViewModel(
+        context = applicationContext,
+        configurationRepository = configurationRepository,
+        deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
+        transitionInteractor = keyguardTransitionInteractor,
+        deviceEntryIconViewModel = deviceEntryIconViewModel,
+        udfpsOverlayInteractor = udfpsOverlayInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
index 67e9289..5ceefde 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
@@ -28,8 +28,10 @@
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager
 
+val Kosmos.fakeDeviceEntryIconViewModelTransition by Fixture { FakeDeviceEntryIconTransition() }
+
 val Kosmos.deviceEntryIconViewModelTransitionsMock by Fixture {
-    mutableSetOf<DeviceEntryIconTransition>()
+    setOf<DeviceEntryIconTransition>(fakeDeviceEntryIconViewModelTransition)
 }
 
 val Kosmos.deviceEntryIconViewModel by Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/FakeDeviceEntryIconTransition.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/FakeDeviceEntryIconTransition.kt
new file mode 100644
index 0000000..6d872a3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/FakeDeviceEntryIconTransition.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeDeviceEntryIconTransition : DeviceEntryIconTransition {
+    private val _deviceEntryParentViewAlpha: MutableStateFlow<Float> = MutableStateFlow(0f)
+    override val deviceEntryParentViewAlpha: Flow<Float> = _deviceEntryParentViewAlpha.asStateFlow()
+
+    fun setDeviceEntryParentViewAlpha(alpha: Float) {
+        _deviceEntryParentViewAlpha.value = alpha
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt
index 6ccb3bc..5e67182 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt
@@ -20,6 +20,29 @@
 import android.util.Log.TerribleFailureHandler
 import junit.framework.Assert
 
+/** Asserts that the given block does not make a call to Log.wtf */
+fun assertDoesNotLogWtf(
+    message: String = "Expected Log.wtf not to be called",
+    notLoggingBlock: () -> Unit,
+) {
+    var caught: TerribleFailureLog? = null
+    val newHandler = TerribleFailureHandler { tag, failure, system ->
+        caught = TerribleFailureLog(tag, failure, system)
+    }
+    val oldHandler = Log.setWtfHandler(newHandler)
+    try {
+        notLoggingBlock()
+    } finally {
+        Log.setWtfHandler(oldHandler)
+    }
+    caught?.let { throw AssertionError("$message: $it", it.failure) }
+}
+
+fun assertDoesNotLogWtf(
+    message: String = "Expected Log.wtf not to be called",
+    notLoggingRunnable: Runnable,
+) = assertDoesNotLogWtf(message = message) { notLoggingRunnable.run() }
+
 /**
  * Assert that the given block makes a call to Log.wtf
  *
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt
new file mode 100644
index 0000000..d705248
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.custom
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.external.FakeCustomTileStatePersister
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileDefaultsRepository
+import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTilePackageUpdatesRepository
+import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileRepository
+import com.android.systemui.qs.tiles.impl.custom.data.repository.FakePackageManagerAdapterFacade
+
+var Kosmos.tileSpec: TileSpec.CustomTileSpec by Kosmos.Fixture()
+
+val Kosmos.customTileStatePersister: FakeCustomTileStatePersister by
+    Kosmos.Fixture { FakeCustomTileStatePersister() }
+
+val Kosmos.customTileRepository: FakeCustomTileRepository by
+    Kosmos.Fixture {
+        FakeCustomTileRepository(
+            customTileStatePersister,
+            packageManagerAdapterFacade,
+            testScope.testScheduler,
+        )
+    }
+
+val Kosmos.customTileDefaultsRepository: FakeCustomTileDefaultsRepository by
+    Kosmos.Fixture { FakeCustomTileDefaultsRepository() }
+
+val Kosmos.customTilePackagesUpdatesRepository: FakeCustomTilePackageUpdatesRepository by
+    Kosmos.Fixture { FakeCustomTilePackageUpdatesRepository() }
+
+val Kosmos.packageManagerAdapterFacade: FakePackageManagerAdapterFacade by
+    Kosmos.Fixture { FakePackageManagerAdapterFacade(tileSpec) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt
index ccf0391..ba803d8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt
@@ -19,21 +19,21 @@
 import android.os.UserHandle
 import android.service.quicksettings.Tile
 import com.android.systemui.qs.external.FakeCustomTileStatePersister
-import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.flow.Flow
 
 class FakeCustomTileRepository(
-    tileSpec: TileSpec.CustomTileSpec,
     customTileStatePersister: FakeCustomTileStatePersister,
+    private val packageManagerAdapterFacade: FakePackageManagerAdapterFacade,
     testBackgroundContext: CoroutineContext,
 ) : CustomTileRepository {
 
     private val realDelegate: CustomTileRepository =
         CustomTileRepositoryImpl(
-            tileSpec,
+            packageManagerAdapterFacade.tileSpec,
             customTileStatePersister,
+            packageManagerAdapterFacade.packageManagerAdapter,
             testBackgroundContext,
         )
 
@@ -44,6 +44,10 @@
 
     override fun getTile(user: UserHandle): Tile? = realDelegate.getTile(user)
 
+    override suspend fun isTileActive(): Boolean = realDelegate.isTileActive()
+
+    override suspend fun isTileToggleable(): Boolean = realDelegate.isTileToggleable()
+
     override suspend fun updateWithTile(
         user: UserHandle,
         newTile: Tile,
@@ -55,4 +59,8 @@
         defaults: CustomTileDefaults,
         isPersistable: Boolean,
     ) = realDelegate.updateWithDefaults(user, defaults, isPersistable)
+
+    fun setTileActive(isActive: Boolean) = packageManagerAdapterFacade.setIsActive(isActive)
+
+    fun setTileToggleable(isActive: Boolean) = packageManagerAdapterFacade.setIsToggleable(isActive)
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt
new file mode 100644
index 0000000..c9a7655
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.custom.data.repository
+
+import android.content.pm.ServiceInfo
+import android.os.Bundle
+import com.android.systemui.qs.external.PackageManagerAdapter
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+
+class FakePackageManagerAdapterFacade(
+    val tileSpec: TileSpec.CustomTileSpec,
+    val packageManagerAdapter: PackageManagerAdapter = mock {},
+) {
+
+    private var isToggleable: Boolean = false
+    private var isActive: Boolean = false
+
+    init {
+        whenever(packageManagerAdapter.getServiceInfo(eq(tileSpec.componentName), any()))
+            .thenAnswer {
+                ServiceInfo().apply {
+                    metaData =
+                        Bundle().apply {
+                            putBoolean(
+                                android.service.quicksettings.TileService.META_DATA_TOGGLEABLE_TILE,
+                                isToggleable
+                            )
+                            putBoolean(
+                                android.service.quicksettings.TileService.META_DATA_ACTIVE_TILE,
+                                isActive
+                            )
+                        }
+                }
+            }
+    }
+
+    fun setIsActive(isActive: Boolean) {
+        this.isActive = isActive
+    }
+
+    fun setIsToggleable(isToggleable: Boolean) {
+        this.isToggleable = isToggleable
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
similarity index 64%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
index f7f16a4..67fecb4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
@@ -16,9 +16,22 @@
 
 package com.android.systemui.statusbar.notification.icon.ui.viewbinder
 
+import com.android.systemui.common.ui.configurationState
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.notifCollection
+import com.android.systemui.statusbar.ui.systemBarUtilsState
+
+val Kosmos.notificationIconContainerShelfViewBinder by Fixture {
+    NotificationIconContainerShelfViewBinder(
+        notificationIconContainerShelfViewModel,
+        configurationState,
+        systemBarUtilsState,
+        statusBarIconViewBindingFailureTracker,
+        shelfNotificationIconViewStore,
+    )
+}
 
 val Kosmos.shelfNotificationIconViewStore by Fixture {
     ShelfNotificationIconViewStore(notifCollection = notifCollection)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt
index 988172c..b906b60 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.activatableNotificationViewModel
 import com.android.systemui.statusbar.notification.shelf.domain.interactor.notificationShelfInteractor
 
@@ -26,6 +25,5 @@
     NotificationShelfViewModel(
         interactor = notificationShelfInteractor,
         activatableViewModel = activatableNotificationViewModel,
-        icons = notificationIconContainerShelfViewModel,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index ca5b401..04716b9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -22,11 +22,9 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.shelfNotificationIconViewStore
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.statusBarIconViewBindingFailureTracker
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.notificationIconContainerShelfViewBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
 import com.android.systemui.statusbar.phone.notificationIconAreaController
-import com.android.systemui.statusbar.ui.systemBarUtilsState
 
 val Kosmos.notificationListViewBinder by Fixture {
     NotificationListViewBinder(
@@ -35,9 +33,7 @@
         configuration = configurationState,
         falsingManager = falsingManager,
         iconAreaController = notificationIconAreaController,
-        iconViewBindingFailureTracker = statusBarIconViewBindingFailureTracker,
         metricsLogger = metricsLogger,
-        shelfIconViewStore = shelfNotificationIconViewStore,
-        systemBarUtilsState = systemBarUtilsState,
+        nicBinder = notificationIconContainerShelfViewBinder,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
index 42c77aa..4e2dc7a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
@@ -47,6 +47,7 @@
             broadcastDispatcher = broadcastDispatcher,
             keyguardUpdateMonitor = keyguardUpdateMonitor,
             backgroundDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
             activityManager = activityManager,
             refreshUsersScheduler = refreshUsersScheduler,
             guestUserInteractor = guestUserInteractor,
diff --git a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
index a159a5e..5a548fd 100644
--- a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
+++ b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
@@ -54,9 +54,7 @@
           "exclude-annotation": "android.support.test.filters.FlakyTest"
         }
       ]
-    }
-  ],
-  "postsubmit": [
+    },
     {
       "name": "CtsVirtualDevicesCameraTestCases",
       "options": [
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
index 6940ffe..f24c4cc 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -70,7 +70,7 @@
 
             @Override
             public void onProcessCaptureRequest(int streamId, int frameId) throws RemoteException {
-                camera.onProcessCaptureRequest(streamId, frameId, /*metadata=*/ null);
+                camera.onProcessCaptureRequest(streamId, frameId);
             }
 
             @Override
diff --git a/services/core/Android.bp b/services/core/Android.bp
index b5c9ffd..5111b08 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -103,7 +103,6 @@
         "android.hardware.power-java_shared",
     ],
     srcs: [
-        ":android.hardware.biometrics.face-V4-java-source",
         ":android.hardware.tv.hdmi.connection-V1-java-source",
         ":android.hardware.tv.hdmi.earc-V1-java-source",
         ":statslog-art-java-gen",
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index df8f17a..3ae55271 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -70,7 +70,6 @@
 import static android.os.PowerExemptionManager.REASON_OPT_OUT_REQUESTED;
 import static android.os.PowerExemptionManager.REASON_OP_ACTIVATE_PLATFORM_VPN;
 import static android.os.PowerExemptionManager.REASON_OP_ACTIVATE_VPN;
-import static android.os.PowerExemptionManager.REASON_OTHER;
 import static android.os.PowerExemptionManager.REASON_PACKAGE_INSTALLER;
 import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT;
 import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI;
@@ -127,7 +126,6 @@
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
 
 import android.Manifest;
-import android.Manifest.permission;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -1085,7 +1083,7 @@
             // Use that as a shortcut if possible to avoid having to recheck all the conditions.
             final boolean whileInUseAllowsUiJobScheduling =
                     ActivityManagerService.doesReasonCodeAllowSchedulingUserInitiatedJobs(
-                            r.getFgsAllowWIU());
+                            r.getFgsAllowWiu_forStart());
             r.updateAllowUiJobScheduling(whileInUseAllowsUiJobScheduling
                     || mAm.canScheduleUserInitiatedJobs(callingUid, callingPid, callingPackage));
         } else {
@@ -2320,7 +2318,7 @@
 
                     // If the foreground service is not started from TOP process, do not allow it to
                     // have while-in-use location/camera/microphone access.
-                    if (!r.isFgsAllowedWIU()) {
+                    if (!r.isFgsAllowedWiu_forCapabilities()) {
                         Slog.w(TAG,
                                 "Foreground service started from background can not have "
                                         + "location/camera/microphone access: service "
@@ -2436,7 +2434,7 @@
                         // mAllowWhileInUsePermissionInFgs.
                         r.mAllowStartForegroundAtEntering = r.getFgsAllowStart();
                         r.mAllowWhileInUsePermissionInFgsAtEntering =
-                                r.isFgsAllowedWIU();
+                                r.isFgsAllowedWiu_forCapabilities();
                         r.mStartForegroundCount++;
                         r.mFgsEnterTime = SystemClock.uptimeMillis();
                         if (!stopProcStatsOp) {
@@ -2632,7 +2630,7 @@
                 policy.getForegroundServiceTypePolicyInfo(type, defaultToType);
         final @ForegroundServicePolicyCheckCode int code = policy.checkForegroundServiceTypePolicy(
                 mAm.mContext, r.packageName, r.app.uid, r.app.getPid(),
-                r.isFgsAllowedWIU(), policyInfo);
+                r.isFgsAllowedWiu_forStart(), policyInfo);
         RuntimeException exception = null;
         switch (code) {
             case FGS_TYPE_POLICY_CHECK_DEPRECATED: {
@@ -7580,78 +7578,76 @@
      * @param callingUid caller app's uid.
      * @param intent intent to start/bind service.
      * @param r the service to start.
-     * @param isBindService True if it's called from bindService().
+     * @param inBindService True if it's called from bindService().
      * @param forBoundFgs set to true if it's called from Service.startForeground() for a
      *                    service that's not started but bound.
-     * @return true if allow, false otherwise.
      */
     private void setFgsRestrictionLocked(String callingPackage,
             int callingPid, int callingUid, Intent intent, ServiceRecord r, int userId,
-            BackgroundStartPrivileges backgroundStartPrivileges, boolean isBindService,
+            BackgroundStartPrivileges backgroundStartPrivileges, boolean inBindService,
             boolean forBoundFgs) {
 
-        @ReasonCode int allowWIU;
+        @ReasonCode int allowWiu;
         @ReasonCode int allowStart;
 
         // If called from bindService(), do not update the actual fields, but instead
         // keep it in a separate set of fields.
-        if (isBindService) {
-            allowWIU = r.mAllowWIUInBindService;
-            allowStart = r.mAllowStartInBindService;
+        if (inBindService) {
+            allowWiu = r.mAllowWiu_inBindService;
+            allowStart = r.mAllowStart_inBindService;
         } else {
-            allowWIU = r.mAllowWhileInUsePermissionInFgsReasonNoBinding;
-            allowStart = r.mAllowStartForegroundNoBinding;
+            allowWiu = r.mAllowWiu_noBinding;
+            allowStart = r.mAllowStart_noBinding;
         }
 
-        // Check DeviceConfig flag.
-        if (!mAm.mConstants.mFlagBackgroundFgsStartRestrictionEnabled) {
-            if (allowWIU == REASON_DENIED) {
-                // BGFGS start restrictions are disabled. We're allowing while-in-use permissions.
-                // Note REASON_OTHER since there's no other suitable reason.
-                allowWIU = REASON_OTHER;
-            }
-        }
-
-        if ((allowWIU == REASON_DENIED)
-                || (allowStart == REASON_DENIED)) {
+        if ((allowWiu == REASON_DENIED) || (allowStart == REASON_DENIED)) {
             @ReasonCode final int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked(
                     callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges);
             // We store them to compare the old and new while-in-use logics to each other.
             // (They're not used for any other purposes.)
-            if (allowWIU == REASON_DENIED) {
-                allowWIU = allowWhileInUse;
+            if (allowWiu == REASON_DENIED) {
+                allowWiu = allowWhileInUse;
             }
             if (allowStart == REASON_DENIED) {
                 allowStart = shouldAllowFgsStartForegroundWithBindingCheckLocked(
                         allowWhileInUse, callingPackage, callingPid, callingUid, intent, r,
-                        backgroundStartPrivileges, isBindService);
+                        backgroundStartPrivileges, inBindService);
             }
         }
 
-        if (isBindService) {
-            r.mAllowWIUInBindService = allowWIU;
-            r.mAllowStartInBindService = allowStart;
+        if (inBindService) {
+            r.mAllowWiu_inBindService = allowWiu;
+            r.mAllowStart_inBindService = allowStart;
         } else {
             if (!forBoundFgs) {
-                // This is for "normal" situation.
-                r.mAllowWhileInUsePermissionInFgsReasonNoBinding = allowWIU;
-                r.mAllowStartForegroundNoBinding = allowStart;
+                // This is for "normal" situation -- either:
+                // - in Context.start[Foreground]Service()
+                // - or, in Service.startForeground() on a started service.
+                r.mAllowWiu_noBinding = allowWiu;
+                r.mAllowStart_noBinding = allowStart;
             } else {
-                // This logic is only for logging, so we only update the "by-binding" fields.
-                if (r.mAllowWIUByBindings == REASON_DENIED) {
-                    r.mAllowWIUByBindings = allowWIU;
+                // Service.startForeground() is called on a service that's not started, but bound.
+                // In this case, we set them to "byBindings", not "noBinding", because
+                // we don't want to use them when we calculate the "legacy" code.
+                //
+                // We don't want to set them to "no binding" codes, because on U-QPR1 and below,
+                // we didn't call setFgsRestrictionLocked() in the code path which sets
+                // forBoundFgs to true, and we wanted to preserve the original behavior in other
+                // places to compare the legacy and new logic.
+                if (r.mAllowWiu_byBindings == REASON_DENIED) {
+                    r.mAllowWiu_byBindings = allowWiu;
                 }
-                if (r.mAllowStartByBindings == REASON_DENIED) {
-                    r.mAllowStartByBindings = allowStart;
+                if (r.mAllowStart_byBindings == REASON_DENIED) {
+                    r.mAllowStart_byBindings = allowStart;
                 }
             }
             // Also do a binding client check, unless called from bindService().
-            if (r.mAllowWIUByBindings == REASON_DENIED) {
-                r.mAllowWIUByBindings =
+            if (r.mAllowWiu_byBindings == REASON_DENIED) {
+                r.mAllowWiu_byBindings =
                         shouldAllowFgsWhileInUsePermissionByBindingsLocked(callingUid);
             }
-            if (r.mAllowStartByBindings == REASON_DENIED) {
-                r.mAllowStartByBindings = r.mAllowWIUByBindings;
+            if (r.mAllowStart_byBindings == REASON_DENIED) {
+                r.mAllowStart_byBindings = r.mAllowWiu_byBindings;
             }
         }
     }
@@ -7660,13 +7656,13 @@
      * Reset various while-in-use and BFSL related information.
      */
     void resetFgsRestrictionLocked(ServiceRecord r) {
-        r.clearFgsAllowWIU();
+        r.clearFgsAllowWiu();
         r.clearFgsAllowStart();
 
         r.mInfoAllowStartForeground = null;
         r.mInfoTempFgsAllowListReason = null;
         r.mLoggedInfoAllowStartForeground = false;
-        r.updateAllowUiJobScheduling(r.isFgsAllowedWIU());
+        r.updateAllowUiJobScheduling(r.isFgsAllowedWiu_forStart());
     }
 
     boolean canStartForegroundServiceLocked(int callingPid, int callingUid, String callingPackage) {
@@ -8284,7 +8280,8 @@
             allowWhileInUsePermissionInFgs = r.mAllowWhileInUsePermissionInFgsAtEntering;
             fgsStartReasonCode = r.mAllowStartForegroundAtEntering;
         } else {
-            allowWhileInUsePermissionInFgs = r.isFgsAllowedWIU();
+            // TODO: Also log "forStart"
+            allowWhileInUsePermissionInFgs = r.isFgsAllowedWiu_forCapabilities();
             fgsStartReasonCode = r.getFgsAllowStart();
         }
         final int callerTargetSdkVersion = r.mRecentCallerApplicationInfo != null
@@ -8323,12 +8320,12 @@
                 mAm.getUidProcessCapabilityLocked(r.mRecentCallingUid),
                 0,
                 0,
-                r.mAllowWhileInUsePermissionInFgsReasonNoBinding,
-                r.mAllowWIUInBindService,
-                r.mAllowWIUByBindings,
-                r.mAllowStartForegroundNoBinding,
-                r.mAllowStartInBindService,
-                r.mAllowStartByBindings,
+                r.mAllowWiu_noBinding,
+                r.mAllowWiu_inBindService,
+                r.mAllowWiu_byBindings,
+                r.mAllowStart_noBinding,
+                r.mAllowStart_inBindService,
+                r.mAllowStart_byBindings,
                 fgsStartApi,
                 fgsRestrictionRecalculated);
 
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index d0ab287..626b70b 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -275,7 +275,7 @@
     @VisibleForTesting static final String DEFAULT_COMPACT_PROC_STATE_THROTTLE =
             String.valueOf(ActivityManager.PROCESS_STATE_RECEIVER);
     @VisibleForTesting static final long DEFAULT_FREEZER_DEBOUNCE_TIMEOUT = 10_000L;
-    @VisibleForTesting static final boolean DEFAULT_FREEZER_EXEMPT_INST_PKG = true;
+    @VisibleForTesting static final boolean DEFAULT_FREEZER_EXEMPT_INST_PKG = false;
     @VisibleForTesting static final boolean DEFAULT_FREEZER_BINDER_ENABLED = true;
     @VisibleForTesting static final long DEFAULT_FREEZER_BINDER_DIVISOR = 4;
     @VisibleForTesting static final int DEFAULT_FREEZER_BINDER_OFFSET = 500;
diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
index fc8ad6bc..05303f6f 100644
--- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
+++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
@@ -507,7 +507,8 @@
                 r.appInfo.uid,
                 r.shortInstanceName,
                 fgsState, // FGS State
-                r.isFgsAllowedWIU(), // allowWhileInUsePermissionInFgs
+                // TODO: Also log "forStart"
+                r.isFgsAllowedWiu_forCapabilities(), // allowWhileInUsePermissionInFgs
                 r.getFgsAllowStart(), // fgsStartReasonCode
                 r.appInfo.targetSdkVersion,
                 r.mRecentCallingUid,
@@ -535,12 +536,12 @@
                 ActivityManager.PROCESS_CAPABILITY_NONE,
                 apiDurationBeforeFgsStart,
                 apiDurationAfterFgsEnd,
-                r.mAllowWhileInUsePermissionInFgsReasonNoBinding,
-                r.mAllowWIUInBindService,
-                r.mAllowWIUByBindings,
-                r.mAllowStartForegroundNoBinding,
-                r.mAllowStartInBindService,
-                r.mAllowStartByBindings,
+                r.mAllowWiu_noBinding,
+                r.mAllowWiu_inBindService,
+                r.mAllowWiu_byBindings,
+                r.mAllowStart_noBinding,
+                r.mAllowStart_inBindService,
+                r.mAllowStart_byBindings,
                 FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA,
                 false
         );
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 3424578a..b507a60 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2254,7 +2254,7 @@
 
             if (s.isForeground) {
                 final int fgsType = s.foregroundServiceType;
-                if (s.isFgsAllowedWIU()) {
+                if (s.isFgsAllowedWiu_forCapabilities()) {
                     capabilityFromFGS |=
                             (fgsType & FOREGROUND_SERVICE_TYPE_LOCATION)
                                     != 0 ? PROCESS_CAPABILITY_FOREGROUND_LOCATION : 0;
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 0fba739..08b129e 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -37,6 +37,9 @@
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.RemoteServiceException.CannotPostForegroundServiceNotificationException;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -81,6 +84,37 @@
     // Maximum number of times it can fail during execution before giving up.
     static final int MAX_DONE_EXECUTING_COUNT = 6;
 
+
+    // Compat IDs for the new FGS logic. For now, we just disable all of them.
+    // TODO: Enable them at some point, but only for V+ builds.
+
+    /**
+     * Compat ID to enable the new FGS start logic, for permission calculation used for
+     * per-FGS-type eligibility calculation.
+     * (See also android.app.ForegroundServiceTypePolicy)
+     */
+    @ChangeId
+    // @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Disabled
+    static final long USE_NEW_WIU_LOGIC_FOR_START = 311208629L;
+
+    /**
+     * Compat ID to enable the new FGS start logic, for capability calculation.
+     */
+    @ChangeId
+    // Always enabled
+    @Disabled
+    static final long USE_NEW_WIU_LOGIC_FOR_CAPABILITIES = 313677553L;
+
+    /**
+     * Compat ID to enable the new FGS start logic, for deciding whether to allow FGS start from
+     * the background.
+     */
+    @ChangeId
+    // @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Disabled
+    static final long USE_NEW_BFSL_LOGIC = 311208749L;
+
     final ActivityManagerService ams;
     final ComponentName name; // service component.
     final ComponentName instanceName; // service component's per-instance name.
@@ -178,7 +212,7 @@
     // If it's not DENIED, while-in-use permissions are allowed.
     // while-in-use permissions in FGS started from background might be restricted.
     @PowerExemptionManager.ReasonCode
-    int mAllowWhileInUsePermissionInFgsReasonNoBinding = REASON_DENIED;
+    int mAllowWiu_noBinding = REASON_DENIED;
 
     // A copy of mAllowWhileInUsePermissionInFgs's value when the service is entering FGS state.
     boolean mAllowWhileInUsePermissionInFgsAtEntering;
@@ -208,7 +242,7 @@
     // allow the service becomes foreground service? Service started from background may not be
     // allowed to become a foreground service.
     @PowerExemptionManager.ReasonCode
-    int mAllowStartForegroundNoBinding = REASON_DENIED;
+    int mAllowStart_noBinding = REASON_DENIED;
     // A copy of mAllowStartForeground's value when the service is entering FGS state.
     @PowerExemptionManager.ReasonCode
     int mAllowStartForegroundAtEntering = REASON_DENIED;
@@ -220,72 +254,172 @@
     boolean mLoggedInfoAllowStartForeground;
 
     @PowerExemptionManager.ReasonCode
-    int mAllowWIUInBindService = REASON_DENIED;
+    int mAllowWiu_inBindService = REASON_DENIED;
 
     @PowerExemptionManager.ReasonCode
-    int mAllowWIUByBindings = REASON_DENIED;
+    int mAllowWiu_byBindings = REASON_DENIED;
 
     @PowerExemptionManager.ReasonCode
-    int mAllowStartInBindService = REASON_DENIED;
+    int mAllowStart_inBindService = REASON_DENIED;
 
     @PowerExemptionManager.ReasonCode
-    int mAllowStartByBindings = REASON_DENIED;
+    int mAllowStart_byBindings = REASON_DENIED;
 
-    @PowerExemptionManager.ReasonCode
-    int getFgsAllowWIU() {
-        return mAllowWhileInUsePermissionInFgsReasonNoBinding != REASON_DENIED
-                ? mAllowWhileInUsePermissionInFgsReasonNoBinding
-                : mAllowWIUInBindService;
+    /**
+     * Whether to use the new "while-in-use permission" logic for FGS start
+     */
+    private boolean useNewWiuLogic_forStart() {
+        return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
+                && CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_START, appInfo.uid);
     }
 
-    boolean isFgsAllowedWIU() {
-        return getFgsAllowWIU() != REASON_DENIED;
+    /**
+     * Whether to use the new "while-in-use permission" logic for capabilities
+     */
+    private boolean useNewWiuLogic_forCapabilities() {
+        return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
+                && CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_CAPABILITIES, appInfo.uid);
     }
 
+    /**
+     * Whether to use the new "FGS BG start exemption" logic.
+     */
+    private boolean useNewBfslLogic() {
+        return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
+                && CompatChanges.isChangeEnabled(USE_NEW_BFSL_LOGIC, appInfo.uid);
+    }
+
+
+    @PowerExemptionManager.ReasonCode
+    private int getFgsAllowWiu_legacy() {
+        // In the legacy mode (U-), we use mAllowWiu_inBindService and mAllowWiu_noBinding.
+        return reasonOr(
+                mAllowWiu_noBinding,
+                mAllowWiu_inBindService
+        );
+    }
+
+    @PowerExemptionManager.ReasonCode
+    private int getFgsAllowWiu_new() {
+        // In the new mode, use by-bindings instead of in-bind-service
+        return reasonOr(
+                mAllowWiu_noBinding,
+                mAllowWiu_byBindings
+        );
+    }
+
+    /**
+     * We use this logic for ForegroundServiceTypePolicy and UIDT eligibility check.
+     */
+    @PowerExemptionManager.ReasonCode
+    int getFgsAllowWiu_forStart() {
+        if (useNewWiuLogic_forStart()) {
+            return getFgsAllowWiu_new();
+        } else {
+            return getFgsAllowWiu_legacy();
+        }
+    }
+
+    /**
+     * We use this logic for the capability calculation in oom-adjuster.
+     */
+    @PowerExemptionManager.ReasonCode
+    int getFgsAllowWiu_forCapabilities() {
+        if (useNewWiuLogic_forCapabilities()) {
+            return getFgsAllowWiu_new();
+        } else {
+            // If alwaysUseNewLogicForWiuCapabilities() isn't set, just use the same logic as
+            // getFgsAllowWiu_forStart().
+            return getFgsAllowWiu_forStart();
+        }
+    }
+
+    /**
+     * We use this logic for ForegroundServiceTypePolicy and UIDT eligibility check.
+     */
+    boolean isFgsAllowedWiu_forStart() {
+        return getFgsAllowWiu_forStart() != REASON_DENIED;
+    }
+
+    /**
+     * We use this logic for the capability calculation in oom-adjuster.
+     */
+    boolean isFgsAllowedWiu_forCapabilities() {
+        return getFgsAllowWiu_forCapabilities() != REASON_DENIED;
+    }
+
+    @PowerExemptionManager.ReasonCode
+    private int getFgsAllowStart_legacy() {
+        // This is used for BFSL (background FGS launch) exemption.
+        // Originally -- on U-QPR1 and before -- we only used [in-bind-service] + [no-binding].
+        // This would exclude certain "valid" situations, so in U-QPR2, we started
+        // using [by-bindings] too.
+        return reasonOr(
+                mAllowStart_noBinding,
+                mAllowStart_inBindService,
+                mAllowStart_byBindings
+        );
+    }
+
+    @PowerExemptionManager.ReasonCode
+    private int getFgsAllowStart_new() {
+        // In the new mode, we don't use [in-bind-service].
+        return reasonOr(
+                mAllowStart_noBinding,
+                mAllowStart_byBindings
+        );
+    }
+
+    /**
+     * Calculate a BFSL exemption code.
+     */
     @PowerExemptionManager.ReasonCode
     int getFgsAllowStart() {
-        return mAllowStartForegroundNoBinding != REASON_DENIED
-                ? mAllowStartForegroundNoBinding
-                : (mAllowStartByBindings != REASON_DENIED
-                ? mAllowStartByBindings
-                : mAllowStartInBindService);
+        if (useNewBfslLogic()) {
+            return getFgsAllowStart_new();
+        } else {
+            return getFgsAllowStart_legacy();
+        }
     }
 
+    /**
+     * Return whether BFSL is allowed or not.
+     */
     boolean isFgsAllowedStart() {
         return getFgsAllowStart() != REASON_DENIED;
     }
 
-    void clearFgsAllowWIU() {
-        mAllowWhileInUsePermissionInFgsReasonNoBinding = REASON_DENIED;
-        mAllowWIUInBindService = REASON_DENIED;
-        mAllowWIUByBindings = REASON_DENIED;
+    void clearFgsAllowWiu() {
+        mAllowWiu_noBinding = REASON_DENIED;
+        mAllowWiu_inBindService = REASON_DENIED;
+        mAllowWiu_byBindings = REASON_DENIED;
     }
 
     void clearFgsAllowStart() {
-        mAllowStartForegroundNoBinding = REASON_DENIED;
-        mAllowStartInBindService = REASON_DENIED;
-        mAllowStartByBindings = REASON_DENIED;
+        mAllowStart_noBinding = REASON_DENIED;
+        mAllowStart_inBindService = REASON_DENIED;
+        mAllowStart_byBindings = REASON_DENIED;
     }
 
     @PowerExemptionManager.ReasonCode
-    int reasonOr(@PowerExemptionManager.ReasonCode int first,
+    static int reasonOr(
+            @PowerExemptionManager.ReasonCode int first,
             @PowerExemptionManager.ReasonCode int second) {
         return first != REASON_DENIED ? first : second;
     }
 
-    boolean allowedChanged(@PowerExemptionManager.ReasonCode int first,
-            @PowerExemptionManager.ReasonCode int second) {
-        return (first == REASON_DENIED) != (second == REASON_DENIED);
+    @PowerExemptionManager.ReasonCode
+    static int reasonOr(
+            @PowerExemptionManager.ReasonCode int first,
+            @PowerExemptionManager.ReasonCode int second,
+            @PowerExemptionManager.ReasonCode int third) {
+        return first != REASON_DENIED ? first : reasonOr(second, third);
     }
 
-    String changeMessage(@PowerExemptionManager.ReasonCode int first,
-            @PowerExemptionManager.ReasonCode int second) {
-        return reasonOr(first, second) == REASON_DENIED ? "DENIED"
-                : ("ALLOWED ("
-                + reasonCodeToString(first)
-                + "+"
-                + reasonCodeToString(second)
-                + ")");
+    boolean allowedChanged(
+            @PowerExemptionManager.ReasonCode int legacyCode,
+            @PowerExemptionManager.ReasonCode int newCode) {
+        return (legacyCode == REASON_DENIED) != (newCode == REASON_DENIED);
     }
 
     private String getFgsInfoForWtf() {
@@ -295,15 +429,14 @@
     }
 
     void maybeLogFgsLogicChange() {
-        final int origWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding,
-                mAllowWIUInBindService);
-        final int newWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding,
-                mAllowWIUByBindings);
-        final int origStart = reasonOr(mAllowStartForegroundNoBinding, mAllowStartInBindService);
-        final int newStart = reasonOr(mAllowStartForegroundNoBinding, mAllowStartByBindings);
+        final int wiuLegacy = getFgsAllowWiu_legacy();
+        final int wiuNew = getFgsAllowWiu_new();
 
-        final boolean wiuChanged = allowedChanged(origWiu, newWiu);
-        final boolean startChanged = allowedChanged(origStart, newStart);
+        final int startLegacy = getFgsAllowStart_legacy();
+        final int startNew = getFgsAllowStart_new();
+
+        final boolean wiuChanged = allowedChanged(wiuLegacy, wiuNew);
+        final boolean startChanged = allowedChanged(startLegacy, startNew);
 
         if (!wiuChanged && !startChanged) {
             return;
@@ -311,15 +444,14 @@
         final String message = "FGS logic changed:"
                 + (wiuChanged ? " [WIU changed]" : "")
                 + (startChanged ? " [BFSL changed]" : "")
-                + " OW:" // Orig-WIU
-                + changeMessage(mAllowWhileInUsePermissionInFgsReasonNoBinding,
-                        mAllowWIUInBindService)
-                + " NW:" // New-WIU
-                + changeMessage(mAllowWhileInUsePermissionInFgsReasonNoBinding, mAllowWIUByBindings)
-                + " OS:" // Orig-start
-                + changeMessage(mAllowStartForegroundNoBinding, mAllowStartInBindService)
-                + " NS:" // New-start
-                + changeMessage(mAllowStartForegroundNoBinding, mAllowStartByBindings)
+                + " Orig WIU:"
+                + reasonCodeToString(wiuLegacy)
+                + " New WIU:"
+                + reasonCodeToString(wiuNew)
+                + " Orig BFSL:"
+                + reasonCodeToString(startLegacy)
+                + " New BFSL:"
+                + reasonCodeToString(startNew)
                 + getFgsInfoForWtf();
         Slog.wtf(TAG_SERVICE, message);
     }
@@ -587,6 +719,7 @@
                 proto.write(ServiceRecordProto.AppInfo.RES_DIR, appInfo.publicSourceDir);
             }
             proto.write(ServiceRecordProto.AppInfo.DATA_DIR, appInfo.dataDir);
+            proto.write(ServiceRecordProto.AppInfo.TARGET_SDK_VERSION, appInfo.targetSdkVersion);
             proto.end(appInfoToken);
         }
         if (app != null) {
@@ -611,8 +744,10 @@
         ProtoUtils.toDuration(proto, ServiceRecordProto.LAST_ACTIVITY_TIME, lastActivity, now);
         ProtoUtils.toDuration(proto, ServiceRecordProto.RESTART_TIME, restartTime, now);
         proto.write(ServiceRecordProto.CREATED_FROM_FG, createdFromFg);
+
+        // TODO: Log "forStart" too.
         proto.write(ServiceRecordProto.ALLOW_WHILE_IN_USE_PERMISSION_IN_FGS,
-                isFgsAllowedWIU());
+                isFgsAllowedWiu_forCapabilities());
 
         if (startRequested || delayedStop || lastStartId != 0) {
             long startToken = proto.start(ServiceRecordProto.START);
@@ -691,15 +826,25 @@
             proto.end(shortFgsToken);
         }
 
+        // TODO: Dump all the mAllowWiu* and mAllowStart* fields as needed.
+
         proto.end(token);
     }
 
+    void dumpReasonCode(PrintWriter pw, String prefix, String fieldName, int code) {
+        pw.print(prefix);
+        pw.print(fieldName);
+        pw.print("=");
+        pw.println(PowerExemptionManager.reasonCodeToString(code));
+    }
+
     void dump(PrintWriter pw, String prefix) {
         pw.print(prefix); pw.print("intent={");
                 pw.print(intent.getIntent().toShortString(false, true, false, false));
                 pw.println('}');
         pw.print(prefix); pw.print("packageName="); pw.println(packageName);
         pw.print(prefix); pw.print("processName="); pw.println(processName);
+        pw.print(prefix); pw.print("targetSdkVersion="); pw.println(appInfo.targetSdkVersion);
         if (permission != null) {
             pw.print(prefix); pw.print("permission="); pw.println(permission);
         }
@@ -727,26 +872,38 @@
             pw.print(prefix); pw.print("mIsAllowedBgActivityStartsByStart=");
             pw.println(mBackgroundStartPrivilegesByStartMerged);
         }
-        pw.print(prefix); pw.print("mAllowWhileInUsePermissionInFgsReason=");
-        pw.println(PowerExemptionManager.reasonCodeToString(
-                mAllowWhileInUsePermissionInFgsReasonNoBinding));
 
-        pw.print(prefix); pw.print("mAllowWIUInBindService=");
-        pw.println(PowerExemptionManager.reasonCodeToString(mAllowWIUInBindService));
-        pw.print(prefix); pw.print("mAllowWIUByBindings=");
-        pw.println(PowerExemptionManager.reasonCodeToString(mAllowWIUByBindings));
+        pw.print(prefix); pw.print("useNewWiuLogic_forCapabilities()=");
+        pw.println(useNewWiuLogic_forCapabilities());
+        pw.print(prefix); pw.print("useNewWiuLogic_forStart()=");
+        pw.println(useNewWiuLogic_forStart());
+        pw.print(prefix); pw.print("useNewBfslLogic()=");
+        pw.println(useNewBfslLogic());
+
+        dumpReasonCode(pw, prefix, "mAllowWiu_noBinding", mAllowWiu_noBinding);
+        dumpReasonCode(pw, prefix, "mAllowWiu_inBindService", mAllowWiu_inBindService);
+        dumpReasonCode(pw, prefix, "mAllowWiu_byBindings", mAllowWiu_byBindings);
+
+        dumpReasonCode(pw, prefix, "getFgsAllowWiu_legacy", getFgsAllowWiu_legacy());
+        dumpReasonCode(pw, prefix, "getFgsAllowWiu_new", getFgsAllowWiu_new());
+
+        dumpReasonCode(pw, prefix, "getFgsAllowWiu_forStart", getFgsAllowWiu_forStart());
+        dumpReasonCode(pw, prefix, "getFgsAllowWiu_forCapabilities",
+                getFgsAllowWiu_forCapabilities());
 
         pw.print(prefix); pw.print("allowUiJobScheduling="); pw.println(mAllowUiJobScheduling);
         pw.print(prefix); pw.print("recentCallingPackage=");
                 pw.println(mRecentCallingPackage);
         pw.print(prefix); pw.print("recentCallingUid=");
         pw.println(mRecentCallingUid);
-        pw.print(prefix); pw.print("allowStartForeground=");
-        pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartForegroundNoBinding));
-        pw.print(prefix); pw.print("mAllowStartInBindService=");
-        pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartInBindService));
-        pw.print(prefix); pw.print("mAllowStartByBindings=");
-        pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartByBindings));
+
+        dumpReasonCode(pw, prefix, "mAllowStart_noBinding", mAllowStart_noBinding);
+        dumpReasonCode(pw, prefix, "mAllowStart_inBindService", mAllowStart_inBindService);
+        dumpReasonCode(pw, prefix, "mAllowStart_byBindings", mAllowStart_byBindings);
+
+        dumpReasonCode(pw, prefix, "getFgsAllowStart_legacy", getFgsAllowStart_legacy());
+        dumpReasonCode(pw, prefix, "getFgsAllowStart_new", getFgsAllowStart_new());
+        dumpReasonCode(pw, prefix, "getFgsAllowStart", getFgsAllowStart());
 
         pw.print(prefix); pw.print("startForegroundCount=");
         pw.println(mStartForegroundCount);
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index a6b532c..badd7f0 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -3050,8 +3050,8 @@
 
     /**
      * Returns whether the given user requires credential entry at this time. This is used to
-     * intercept activity launches for locked work apps due to work challenge being triggered
-     * or when the profile user is yet to be unlocked.
+     * intercept activity launches for apps corresponding to locked profiles due to separate
+     * challenge being triggered or when the profile user is yet to be unlocked.
      */
     protected boolean shouldConfirmCredentials(@UserIdInt int userId) {
         if (getStartedUserState(userId) == null) {
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index d9e8ddd..654aebd 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -28,3 +28,10 @@
     description: "Restrict network access for certain applications in BFGS process state"
     bug: "304347838"
 }
+# Whether to use the new while-in-use / BG-FGS-start logic
+flag {
+     namespace: "backstage_power"
+     name: "new_fgs_restriction_logic"
+     description: "Enable the new FGS restriction logic"
+     bug: "276963716"
+}
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index b3fb9c9..8b7e56e 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -20,6 +20,7 @@
 import static android.content.Intent.ACTION_PACKAGE_REMOVED;
 import static android.content.Intent.EXTRA_REPLACING;
 import static android.server.app.Flags.gameDefaultFrameRate;
+import static android.server.app.Flags.disableGameModeWhenAppTop;
 
 import static com.android.internal.R.styleable.GameModeConfig_allowGameAngleDriver;
 import static com.android.internal.R.styleable.GameModeConfig_allowGameDownscaling;
@@ -181,7 +182,9 @@
     @Nullable
     final MyUidObserver mUidObserver;
     @GuardedBy("mUidObserverLock")
-    private final Set<Integer> mForegroundGameUids = new HashSet<>();
+    private final Set<Integer> mGameForegroundUids = new HashSet<>();
+    @GuardedBy("mUidObserverLock")
+    private final Set<Integer> mNonGameForegroundUids = new HashSet<>();
     private final GameManagerServiceSystemPropertiesWrapper mSysProps;
     private float mGameDefaultFrameRateValue;
 
@@ -238,12 +241,10 @@
                 FileUtils.S_IRUSR | FileUtils.S_IWUSR
                         | FileUtils.S_IRGRP | FileUtils.S_IWGRP,
                 -1, -1);
-        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_GAME_SERVICE)) {
+        if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_GAME_SERVICE)) {
             mGameServiceController = new GameServiceController(
                     context, BackgroundThread.getExecutor(),
-                    new GameServiceProviderSelectorImpl(
-                            context.getResources(),
-                            context.getPackageManager()),
+                    new GameServiceProviderSelectorImpl(context.getResources(), mPackageManager),
                     new GameServiceProviderInstanceFactoryImpl(context));
         } else {
             mGameServiceController = null;
@@ -2245,7 +2246,7 @@
 
         // Update all foreground games' frame rate.
         synchronized (mUidObserverLock) {
-            for (int uid : mForegroundGameUids) {
+            for (int uid : mGameForegroundUids) {
                 setGameDefaultFrameRateOverride(uid, getGameDefaultFrameRate(isEnabled));
             }
         }
@@ -2261,31 +2262,44 @@
         @Override
         public void onUidGone(int uid, boolean disabled) {
             synchronized (mUidObserverLock) {
-                disableGameMode(uid);
+                handleUidMovedOffTop(uid);
             }
         }
 
         @Override
         public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
+            switch (procState) {
+                case ActivityManager.PROCESS_STATE_TOP:
+                    handleUidMovedToTop(uid);
+                    return;
+                default:
+                    handleUidMovedOffTop(uid);
+            }
+        }
+
+        private void handleUidMovedToTop(int uid) {
+            final String[] packages = mPackageManager.getPackagesForUid(uid);
+            if (packages == null || packages.length == 0) {
+                return;
+            }
+
+            final int userId = mContext.getUserId();
+            final boolean isNotGame = Arrays.stream(packages).noneMatch(
+                    p -> isPackageGame(p, userId));
             synchronized (mUidObserverLock) {
-
-                if (procState != ActivityManager.PROCESS_STATE_TOP) {
-                    disableGameMode(uid);
+                if (isNotGame) {
+                    if (disableGameModeWhenAppTop()) {
+                        if (!mGameForegroundUids.isEmpty() && mNonGameForegroundUids.isEmpty()) {
+                            Slog.v(TAG, "Game power mode OFF (first non-game in foreground)");
+                            mPowerManagerInternal.setPowerMode(Mode.GAME, false);
+                        }
+                        mNonGameForegroundUids.add(uid);
+                    }
                     return;
                 }
-
-                final String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
-                if (packages == null || packages.length == 0) {
-                    return;
-                }
-
-                final int userId = mContext.getUserId();
-                if (!Arrays.stream(packages).anyMatch(p -> isPackageGame(p, userId))) {
-                    return;
-                }
-
-                if (mForegroundGameUids.isEmpty()) {
-                    Slog.v(TAG, "Game power mode ON (process state was changed to foreground)");
+                if (mGameForegroundUids.isEmpty() && (!disableGameModeWhenAppTop()
+                        || mNonGameForegroundUids.isEmpty())) {
+                    Slog.v(TAG, "Game power mode ON (first game in foreground)");
                     mPowerManagerInternal.setPowerMode(Mode.GAME, true);
                 }
                 final boolean isGameDefaultFrameRateDisabled =
@@ -2293,22 +2307,26 @@
                                 PROPERTY_DEBUG_GFX_GAME_DEFAULT_FRAME_RATE_DISABLED, false);
                 setGameDefaultFrameRateOverride(uid,
                         getGameDefaultFrameRate(!isGameDefaultFrameRateDisabled));
-                mForegroundGameUids.add(uid);
+                mGameForegroundUids.add(uid);
             }
         }
 
-        private void disableGameMode(int uid) {
+        private void handleUidMovedOffTop(int uid) {
             synchronized (mUidObserverLock) {
-                if (!mForegroundGameUids.contains(uid)) {
-                    return;
+                if (mGameForegroundUids.contains(uid)) {
+                    mGameForegroundUids.remove(uid);
+                    if (mGameForegroundUids.isEmpty() && (!disableGameModeWhenAppTop()
+                            || mNonGameForegroundUids.isEmpty())) {
+                        Slog.v(TAG, "Game power mode OFF (no games in foreground)");
+                        mPowerManagerInternal.setPowerMode(Mode.GAME, false);
+                    }
+                } else if (disableGameModeWhenAppTop() && mNonGameForegroundUids.contains(uid)) {
+                    mNonGameForegroundUids.remove(uid);
+                    if (mNonGameForegroundUids.isEmpty() && !mGameForegroundUids.isEmpty()) {
+                        Slog.v(TAG, "Game power mode ON (only games in foreground)");
+                        mPowerManagerInternal.setPowerMode(Mode.GAME, true);
+                    }
                 }
-                mForegroundGameUids.remove(uid);
-                if (!mForegroundGameUids.isEmpty()) {
-                    return;
-                }
-                Slog.v(TAG,
-                        "Game power mode OFF (process remomved or state changed to background)");
-                mPowerManagerInternal.setPowerMode(Mode.GAME, false);
             }
         }
     }
diff --git a/services/core/java/com/android/server/app/flags.aconfig b/services/core/java/com/android/server/app/flags.aconfig
index f2e4783..0673013 100644
--- a/services/core/java/com/android/server/app/flags.aconfig
+++ b/services/core/java/com/android/server/app/flags.aconfig
@@ -6,4 +6,11 @@
     description: "This flag guards the new behavior with the addition of Game Default Frame Rate feature."
     bug: "286084594"
     is_fixed_read_only: true
-}
\ No newline at end of file
+}
+
+flag {
+    name: "disable_game_mode_when_app_top"
+    namespace: "game"
+    description: "Disable game power mode when a non-game app is also top and visible"
+    bug: "299295925"
+}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 8091753..dada72e 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -56,6 +56,7 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Pair;
 import android.util.PrintWriterPrinter;
 
 import com.android.internal.annotations.GuardedBy;
@@ -1640,7 +1641,7 @@
         return mBtHelper.getLeAudioDeviceGroupId(device);
     }
 
-    /*package*/ List<String> getLeAudioGroupAddresses(int groupId) {
+    /*package*/ List<Pair<String, String>> getLeAudioGroupAddresses(int groupId) {
         return mBtHelper.getLeAudioGroupAddresses(groupId);
     }
 
@@ -2651,9 +2652,9 @@
         }
     }
 
-    List<String> getDeviceAddresses(AudioDeviceAttributes device) {
+    List<String> getDeviceIdentityAddresses(AudioDeviceAttributes device) {
         synchronized (mDeviceStateLock) {
-            return mDeviceInventory.getDeviceAddresses(device);
+            return mDeviceInventory.getDeviceIdentityAddresses(device);
         }
     }
 
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index d138f24..e05824a 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -566,23 +566,40 @@
         final int mDeviceType;
         final @NonNull String mDeviceName;
         final @NonNull String mDeviceAddress;
+        @NonNull String mDeviceIdentityAddress;
         int mDeviceCodecFormat;
-        @NonNull String mPeerDeviceAddress;
         final int mGroupId;
+        @NonNull String mPeerDeviceAddress;
+        @NonNull String mPeerIdentityDeviceAddress;
 
         /** Disabled operating modes for this device. Use a negative logic so that by default
          * an empty list means all modes are allowed.
          * See BluetoothAdapter.AUDIO_MODE_DUPLEX and BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY */
         @NonNull ArraySet<String> mDisabledModes = new ArraySet(0);
 
-        DeviceInfo(int deviceType, String deviceName, String deviceAddress,
-                   int deviceCodecFormat, String peerDeviceAddress, int groupId) {
+        DeviceInfo(int deviceType, String deviceName, String address,
+                   String identityAddress, int codecFormat,
+                   int groupId, String peerAddress, String peerIdentityAddress) {
             mDeviceType = deviceType;
-            mDeviceName = deviceName == null ? "" : deviceName;
-            mDeviceAddress = deviceAddress == null ? "" : deviceAddress;
-            mDeviceCodecFormat = deviceCodecFormat;
-            mPeerDeviceAddress = peerDeviceAddress == null ? "" : peerDeviceAddress;
+            mDeviceName = TextUtils.emptyIfNull(deviceName);
+            mDeviceAddress = TextUtils.emptyIfNull(address);
+            mDeviceIdentityAddress = TextUtils.emptyIfNull(identityAddress);
+            mDeviceCodecFormat = codecFormat;
             mGroupId = groupId;
+            mPeerDeviceAddress = TextUtils.emptyIfNull(peerAddress);
+            mPeerIdentityDeviceAddress = TextUtils.emptyIfNull(peerIdentityAddress);
+        }
+
+        /** Constructor for all devices except A2DP sink and LE Audio */
+        DeviceInfo(int deviceType, String deviceName, String address) {
+            this(deviceType, deviceName, address, null, AudioSystem.AUDIO_FORMAT_DEFAULT);
+        }
+
+        /** Constructor for A2DP sink devices */
+        DeviceInfo(int deviceType, String deviceName, String address,
+                   String identityAddress, int codecFormat) {
+            this(deviceType, deviceName, address, identityAddress, codecFormat,
+                    BluetoothLeAudio.GROUP_ID_INVALID, null, null);
         }
 
         void setModeDisabled(String mode) {
@@ -601,25 +618,20 @@
             return isModeEnabled(BluetoothAdapter.AUDIO_MODE_DUPLEX);
         }
 
-        DeviceInfo(int deviceType, String deviceName, String deviceAddress,
-                   int deviceCodecFormat) {
-            this(deviceType, deviceName, deviceAddress, deviceCodecFormat,
-                    null, BluetoothLeAudio.GROUP_ID_INVALID);
-        }
-
-        DeviceInfo(int deviceType, String deviceName, String deviceAddress) {
-            this(deviceType, deviceName, deviceAddress, AudioSystem.AUDIO_FORMAT_DEFAULT);
-        }
-
         @Override
         public String toString() {
             return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType)
                     + " (" + AudioSystem.getDeviceName(mDeviceType)
                     + ") name:" + mDeviceName
                     + " addr:" + Utils.anonymizeBluetoothAddress(mDeviceType, mDeviceAddress)
+                    + " identity addr:"
+                    + Utils.anonymizeBluetoothAddress(mDeviceType, mDeviceIdentityAddress)
                     + " codec: " + Integer.toHexString(mDeviceCodecFormat)
-                    + " peer addr:" + mPeerDeviceAddress
                     + " group:" + mGroupId
+                    + " peer addr:"
+                    + Utils.anonymizeBluetoothAddress(mDeviceType, mPeerDeviceAddress)
+                    + " peer identity addr:"
+                    + Utils.anonymizeBluetoothAddress(mDeviceType, mPeerIdentityDeviceAddress)
                     + " disabled modes: " + mDisabledModes + "]";
         }
 
@@ -947,20 +959,44 @@
     }
 
 
+    /**
+     * Goes over all connected LE Audio devices in the provided group ID and
+     * update:
+     * - the peer address according to the addres of other device in the same
+     * group (can also clear the peer address is not anymore in the group)
+     * - The dentity address if not yet set.
+     * LE Audio buds in a pair are in the same group.
+     * @param groupId the LE Audio group to update
+     */
     /*package*/ void onUpdateLeAudioGroupAddresses(int groupId) {
         synchronized (mDevicesLock) {
+            // <address, identy address>
+            List<Pair<String, String>> addresses = new ArrayList<>();
             for (DeviceInfo di : mConnectedDevices.values()) {
                 if (di.mGroupId == groupId) {
-                    List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId);
+                    if (addresses.isEmpty()) {
+                        addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId);
+                    }
                     if (di.mPeerDeviceAddress.equals("")) {
-                        for (String addr : addresses) {
-                            if (!addr.equals(di.mDeviceAddress)) {
-                                di.mPeerDeviceAddress = addr;
+                        for (Pair<String, String> addr : addresses) {
+                            if (!addr.first.equals(di.mDeviceAddress)) {
+                                di.mPeerDeviceAddress = addr.first;
+                                di.mPeerIdentityDeviceAddress = addr.second;
                                 break;
                             }
                         }
-                    } else if (!addresses.contains(di.mPeerDeviceAddress)) {
+                    } else if (!addresses.contains(
+                            new Pair(di.mPeerDeviceAddress, di.mPeerIdentityDeviceAddress))) {
                         di.mPeerDeviceAddress = "";
+                        di.mPeerIdentityDeviceAddress = "";
+                    }
+                    if (di.mDeviceIdentityAddress.equals("")) {
+                        for (Pair<String, String> addr : addresses) {
+                            if (addr.first.equals(di.mDeviceAddress)) {
+                                di.mDeviceIdentityAddress = addr.second;
+                                break;
+                            }
+                        }
                     }
                 }
             }
@@ -1964,7 +2000,7 @@
         mDeviceBroker.clearA2dpSuspended(true /* internalOnly */);
 
         final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name,
-                address, codec);
+                address, btInfo.mDevice.getIdentityAddress(), codec);
         final String diKey = di.getKey();
         mConnectedDevices.put(diKey, di);
         // on a connection always overwrite the device seen by AudioPolicy, see comment above when
@@ -2381,12 +2417,15 @@
             // Find LE Group ID and peer headset address if available
             final int groupId = mDeviceBroker.getLeAudioDeviceGroupId(btInfo.mDevice);
             String peerAddress = "";
+            String peerIdentityAddress = "";
             if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
-                List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId);
+                List<Pair<String, String>> addresses =
+                        mDeviceBroker.getLeAudioGroupAddresses(groupId);
                 if (addresses.size() > 1) {
-                    for (String addr : addresses) {
-                        if (!addr.equals(address)) {
-                            peerAddress = addr;
+                    for (Pair<String, String> addr : addresses) {
+                        if (!addr.first.equals(address)) {
+                            peerAddress = addr.first;
+                            peerIdentityAddress = addr.second;
                             break;
                         }
                     }
@@ -2420,8 +2459,9 @@
             // Reset LEA suspend state each time a new sink is connected
             mDeviceBroker.clearLeAudioSuspended(true /* internalOnly */);
             mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address),
-                    new DeviceInfo(device, name, address, codec,
-                            peerAddress, groupId));
+                    new DeviceInfo(device, name, address,
+                            btInfo.mDevice.getIdentityAddress(), codec,
+                            groupId, peerAddress, peerIdentityAddress));
             mDeviceBroker.postAccessoryPlugMediaUnmute(device);
             setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false);
             addAudioDeviceInInventoryIfNeeded(device, address, peerAddress,
@@ -2806,18 +2846,19 @@
         mDevRoleCapturePresetDispatchers.finishBroadcast();
     }
 
-    List<String> getDeviceAddresses(AudioDeviceAttributes device) {
+    List<String> getDeviceIdentityAddresses(AudioDeviceAttributes device) {
         List<String> addresses = new ArrayList<String>();
         final String key = DeviceInfo.makeDeviceListKey(device.getInternalType(),
                 device.getAddress());
         synchronized (mDevicesLock) {
             DeviceInfo di = mConnectedDevices.get(key);
             if (di != null) {
-                if (!di.mDeviceAddress.isEmpty()) {
-                    addresses.add(di.mDeviceAddress);
+                if (!di.mDeviceIdentityAddress.isEmpty()) {
+                    addresses.add(di.mDeviceIdentityAddress);
                 }
-                if (!di.mPeerDeviceAddress.isEmpty()) {
-                    addresses.add(di.mPeerDeviceAddress);
+                if (!di.mPeerIdentityDeviceAddress.isEmpty()
+                        && !di.mPeerIdentityDeviceAddress.equals(di.mDeviceIdentityAddress)) {
+                    addresses.add(di.mPeerIdentityDeviceAddress);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 0c78231..ea791b7 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -33,6 +33,7 @@
 import static android.media.AudioManager.RINGER_MODE_SILENT;
 import static android.media.AudioManager.RINGER_MODE_VIBRATE;
 import static android.media.AudioManager.STREAM_SYSTEM;
+import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration;
 import static android.os.Process.FIRST_APPLICATION_UID;
 import static android.os.Process.INVALID_UID;
 import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE;
@@ -116,6 +117,7 @@
 import android.media.AudioSystem;
 import android.media.AudioTrack;
 import android.media.BluetoothProfileConnectionInfo;
+import android.media.FadeManagerConfiguration;
 import android.media.IAudioDeviceVolumeDispatcher;
 import android.media.IAudioFocusDispatcher;
 import android.media.IAudioModeDispatcher;
@@ -4513,6 +4515,8 @@
                 + bluetoothMacAddressAnonymization());
         pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:"
                 + disablePrescaleAbsoluteVolume());
+        pw.println("\tandroid.media.audiopolicy.enableFadeManagerConfiguration:"
+                + enableFadeManagerConfiguration());
     }
 
     private void dumpAudioMode(PrintWriter pw) {
@@ -12614,6 +12618,47 @@
     }
 
     /**
+     * see {@link AudioPolicy#setFadeManagerConfigurationForFocusLoss(FadeManagerConfiguration)}
+     */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int setFadeManagerConfigurationForFocusLoss(
+            @NonNull FadeManagerConfiguration fmcForFocusLoss) {
+        super.setFadeManagerConfigurationForFocusLoss_enforcePermission();
+        ensureFadeManagerConfigIsEnabled();
+        Objects.requireNonNull(fmcForFocusLoss,
+                "Fade manager config for focus loss cannot be null");
+        validateFadeManagerConfiguration(fmcForFocusLoss);
+
+        return mPlaybackMonitor.setFadeManagerConfiguration(AudioManager.AUDIOFOCUS_LOSS,
+                fmcForFocusLoss);
+    }
+
+    /**
+     * see {@link AudioPolicy#clearFadeManagerConfigurationForFocusLoss()}
+     */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int clearFadeManagerConfigurationForFocusLoss() {
+        super.clearFadeManagerConfigurationForFocusLoss_enforcePermission();
+        ensureFadeManagerConfigIsEnabled();
+
+        return mPlaybackMonitor.clearFadeManagerConfiguration(AudioManager.AUDIOFOCUS_LOSS);
+    }
+
+    /**
+     * see {@link AudioPolicy#getFadeManagerConfigurationForFocusLoss()}
+     */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss() {
+        super.getFadeManagerConfigurationForFocusLoss_enforcePermission();
+        ensureFadeManagerConfigIsEnabled();
+
+        return mPlaybackMonitor.getFadeManagerConfiguration(AudioManager.AUDIOFOCUS_LOSS);
+    }
+
+    /**
      * @see AudioManager#getHalVersion
      */
     public @Nullable AudioHalVersionInfo getHalVersion() {
@@ -12814,6 +12859,19 @@
         }
     }
 
+    private void ensureFadeManagerConfigIsEnabled() {
+        Preconditions.checkState(enableFadeManagerConfiguration(),
+                "Fade manager configuration not supported");
+    }
+
+    private void validateFadeManagerConfiguration(FadeManagerConfiguration fmc) {
+        // validate permission of audio attributes
+        List<AudioAttributes> attrs = fmc.getAudioAttributesWithVolumeShaperConfigs();
+        for (int index = 0; index < attrs.size(); index++) {
+            validateAudioAttributesUsage(attrs.get(index));
+        }
+    }
+
     //======================
     // Audio policy callbacks from AudioSystem for dynamic policies
     //======================
@@ -13114,6 +13172,7 @@
                             + "could not link to " + projection + " binder death", e);
                 }
             }
+
             int status = connectMixes();
             if (status != AudioSystem.SUCCESS) {
                 release();
@@ -13471,6 +13530,43 @@
         }
     }
 
+    /**
+     * see {@link AudioManager#dispatchAudioFocusChangeWithFade(AudioFocusInfo, int, AudioPolicy,
+     * List, FadeManagerConfiguration)}
+     */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int dispatchFocusChangeWithFade(AudioFocusInfo afi, int focusChange,
+            IAudioPolicyCallback pcb, List<AudioFocusInfo> otherActiveAfis,
+            FadeManagerConfiguration transientFadeMgrConfig) {
+        super.dispatchFocusChangeWithFade_enforcePermission();
+        ensureFadeManagerConfigIsEnabled();
+        Objects.requireNonNull(afi, "AudioFocusInfo cannot be null");
+        Objects.requireNonNull(pcb, "AudioPolicy callback cannot be null");
+        Objects.requireNonNull(otherActiveAfis,
+                "Other active AudioFocusInfo list cannot be null");
+        if (transientFadeMgrConfig != null) {
+            validateFadeManagerConfiguration(transientFadeMgrConfig);
+        }
+
+        synchronized (mAudioPolicies) {
+            Preconditions.checkState(mAudioPolicies.containsKey(pcb.asBinder()),
+                    "Unregistered AudioPolicy for focus dispatch with fade");
+
+            // set the transient fade manager config to be used for handling this focus change
+            if (transientFadeMgrConfig != null) {
+                mPlaybackMonitor.setTransientFadeManagerConfiguration(focusChange,
+                        transientFadeMgrConfig);
+            }
+            int status = mMediaFocusControl.dispatchFocusChangeWithFade(afi, focusChange,
+                    otherActiveAfis);
+
+            if (transientFadeMgrConfig != null) {
+                mPlaybackMonitor.clearTransientFadeManagerConfiguration(focusChange);
+            }
+            return status;
+        }
+    }
 
     //======================
     // Audioserver state dispatch
@@ -13775,8 +13871,8 @@
         return activeAssistantUids;
     }
 
-    List<String> getDeviceAddresses(AudioDeviceAttributes device) {
-        return mDeviceBroker.getDeviceAddresses(device);
+    List<String> getDeviceIdentityAddresses(AudioDeviceAttributes device) {
+        return mDeviceBroker.getDeviceIdentityAddresses(device);
     }
 
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 8075618..a818c30 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -58,6 +58,7 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Pair;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.utils.EventLogger;
@@ -1077,8 +1078,8 @@
         return mLeAudio.getGroupId(device);
     }
 
-    /*package*/ List<String> getLeAudioGroupAddresses(int groupId) {
-        List<String> addresses = new ArrayList<String>();
+    /*package*/ List<Pair<String, String>> getLeAudioGroupAddresses(int groupId) {
+        List<Pair<String, String>> addresses = new ArrayList<>();
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
         if (adapter == null || mLeAudio == null) {
             return addresses;
@@ -1086,7 +1087,7 @@
         List<BluetoothDevice> activeDevices = adapter.getActiveDevices(BluetoothProfile.LE_AUDIO);
         for (BluetoothDevice device : activeDevices) {
             if (device != null && mLeAudio.getGroupId(device) == groupId) {
-                addresses.add(device.getAddress());
+                addresses.add(new Pair(device.getAddress(), device.getIdentityAddress()));
             }
         }
         return addresses;
diff --git a/services/core/java/com/android/server/audio/FadeConfigurations.java b/services/core/java/com/android/server/audio/FadeConfigurations.java
index 2e27c76..37ecf0b 100644
--- a/services/core/java/com/android/server/audio/FadeConfigurations.java
+++ b/services/core/java/com/android/server/audio/FadeConfigurations.java
@@ -16,13 +16,22 @@
 
 package com.android.server.audio;
 
+import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration;
+
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.media.AudioAttributes;
+import android.media.AudioManager;
 import android.media.AudioPlaybackConfiguration;
+import android.media.FadeManagerConfiguration;
 import android.media.VolumeShaper;
 import android.util.Slog;
 
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Class to encapsulate configurations used for fading players
@@ -69,51 +78,229 @@
 
     private static final int INVALID_UID = -1;
 
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration mDefaultFadeManagerConfig;
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration mUpdatedFadeManagerConfig;
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration mTransientFadeManagerConfig;
+    /** active fade manager is one of: transient > updated > default */
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration mActiveFadeManagerConfig;
+
+    /**
+     * Sets the custom fade manager configuration
+     *
+     * @param fadeManagerConfig custom fade manager configuration
+     * @return {@link AudioManager#SUCCESS}  if setting custom fade manager configuration succeeds
+     *     or {@link AudioManager#ERROR} otherwise (example - when fade manager configuration
+     *     feature is disabled)
+     */
+    public int setFadeManagerConfiguration(
+            @NonNull FadeManagerConfiguration fadeManagerConfig) {
+        if (!enableFadeManagerConfiguration()) {
+            return AudioManager.ERROR;
+        }
+
+        synchronized (mLock) {
+            mUpdatedFadeManagerConfig = Objects.requireNonNull(fadeManagerConfig,
+                    "Fade manager configuration cannot be null");
+            mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked();
+        }
+        return AudioManager.SUCCESS;
+    }
+
+    /**
+     * Clears the fade manager configuration that was previously set with
+     * {@link #setFadeManagerConfiguration(FadeManagerConfiguration)}
+     *
+     * @return {@link AudioManager#SUCCESS}  if previously set fade manager configuration is cleared
+     *     or {@link AudioManager#ERROR} otherwise (example, when fade manager configuration feature
+     *     is disabled)
+     */
+    public int clearFadeManagerConfiguration() {
+        if (!enableFadeManagerConfiguration()) {
+            return AudioManager.ERROR;
+        }
+
+        synchronized (mLock) {
+            mUpdatedFadeManagerConfig = null;
+            mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked();
+        }
+        return AudioManager.SUCCESS;
+    }
+
+    /**
+     * Returns the active fade manager configuration
+     *
+     * @return {@code null} if feature is disabled, or the custom fade manager configuration if set,
+     *     or default fade manager configuration if not set.
+     */
+    @Nullable
+    public FadeManagerConfiguration getFadeManagerConfiguration() {
+        if (!enableFadeManagerConfiguration()) {
+            return null;
+        }
+
+        synchronized (mLock) {
+            return mActiveFadeManagerConfig;
+        }
+    }
+
+    /**
+     * Sets the transient fade manager configuration
+     *
+     * @param fadeManagerConfig custom fade manager configuration
+     * @return {@link AudioManager#SUCCESS} if setting custom fade manager configuration succeeds
+     *     or {@link AudioManager#ERROR} otherwise (example - when fade manager configuration is
+     *     disabled)
+     */
+    public int setTransientFadeManagerConfiguration(
+            @NonNull FadeManagerConfiguration fadeManagerConfig) {
+        if (!enableFadeManagerConfiguration()) {
+            return AudioManager.ERROR;
+        }
+
+        synchronized (mLock) {
+            mTransientFadeManagerConfig = Objects.requireNonNull(fadeManagerConfig,
+                    "Transient FadeManagerConfiguration cannot be null");
+            mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked();
+        }
+        return AudioManager.SUCCESS;
+    }
+
+    /**
+     * Clears the transient fade manager configuration that was previously set with
+     * {@link #setTransientFadeManagerConfiguration(FadeManagerConfiguration)}
+     *
+     * @return {@link AudioManager#SUCCESS} if previously set transient fade manager configuration
+     *     is cleared or {@link AudioManager#ERROR} otherwise (example - when fade manager
+     *     configuration is disabled)
+     */
+    public int clearTransientFadeManagerConfiguration() {
+        if (!enableFadeManagerConfiguration()) {
+            return AudioManager.ERROR;
+        }
+        synchronized (mLock) {
+            mTransientFadeManagerConfig = null;
+            mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked();
+        }
+        return AudioManager.SUCCESS;
+    }
+
+    /**
+     * Query if fade should be enforecd on players
+     *
+     * @return {@code true} if fade is enabled or using default configurations, {@code false}
+     * otherwise.
+     */
+    public boolean isFadeEnabled() {
+        if (!enableFadeManagerConfiguration()) {
+            return true;
+        }
+
+        synchronized (mLock) {
+            return getUpdatedFadeManagerConfigLocked().isFadeEnabled();
+        }
+    }
+
     /**
      * Query {@link android.media.AudioAttributes.AttributeUsage usages} that are allowed to
      * fade
+     *
      * @return list of {@link android.media.AudioAttributes.AttributeUsage}
      */
     @NonNull
     public List<Integer> getFadeableUsages() {
-        return DEFAULT_FADEABLE_USAGES;
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_FADEABLE_USAGES;
+        }
+
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // when fade is not enabled, return an empty list instead
+            return fadeManagerConfig.isFadeEnabled() ? fadeManagerConfig.getFadeableUsages()
+                    : Collections.EMPTY_LIST;
+        }
     }
 
     /**
      * Query {@link android.media.AudioAttributes.AttributeContentType content types} that are
      * exempted from fade enforcement
+     *
      * @return list of {@link android.media.AudioAttributes.AttributeContentType}
      */
     @NonNull
     public List<Integer> getUnfadeableContentTypes() {
-        return DEFAULT_UNFADEABLE_CONTENT_TYPES;
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_UNFADEABLE_CONTENT_TYPES;
+        }
+
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // when fade is not enabled, return an empty list instead
+            return fadeManagerConfig.isFadeEnabled() ? fadeManagerConfig.getUnfadeableContentTypes()
+                    : Collections.EMPTY_LIST;
+        }
     }
 
     /**
      * Query {@link android.media.AudioPlaybackConfiguration.PlayerType player types} that are
      * exempted from fade enforcement
+     *
      * @return list of {@link android.media.AudioPlaybackConfiguration.PlayerType}
      */
     @NonNull
     public List<Integer> getUnfadeablePlayerTypes() {
-        return DEFAULT_UNFADEABLE_PLAYER_TYPES;
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_UNFADEABLE_PLAYER_TYPES;
+        }
+
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // when fade is not enabled, return an empty list instead
+            return fadeManagerConfig.isFadeEnabled() ? fadeManagerConfig.getUnfadeablePlayerTypes()
+                    : Collections.EMPTY_LIST;
+        }
     }
 
     /**
      * Get the {@link android.media.VolumeShaper.Configuration} configuration to be applied
      * for the fade-out
+     *
      * @param aa The {@link android.media.AudioAttributes}
      * @return {@link android.media.VolumeShaper.Configuration} for the
-     * {@link android.media.AudioAttributes.AttributeUsage} or default volume shaper if not
-     * configured
+     * {@link android.media.AudioAttributes} or default volume shaper if not configured
      */
     @NonNull
     public VolumeShaper.Configuration getFadeOutVolumeShaperConfig(@NonNull AudioAttributes aa) {
-        return DEFAULT_FADEOUT_VSHAPE;
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_FADEOUT_VSHAPE;
+        }
+        return getOptimalFadeOutVolShaperConfig(aa);
     }
 
     /**
+     * Get the {@link android.media.VolumeShaper.Configuration} configuration to be applied for the
+     * fade in
+     *
+     * @param aa The {@link android.media.AudioAttributes}
+     * @return {@link android.media.VolumeShaper.Configuration} for the
+     * {@link android.media.AudioAttributes} or {@code null} otherwise
+     */
+    @Nullable
+    public VolumeShaper.Configuration getFadeInVolumeShaperConfig(@NonNull AudioAttributes aa) {
+        if (!enableFadeManagerConfiguration()) {
+            return null;
+        }
+        return getOptimalFadeInVolShaperConfig(aa);
+    }
+
+
+    /**
      * Get the duration to fade out a player of type usage
+     *
      * @param aa The {@link android.media.AudioAttributes}
      * @return duration in milliseconds for the
      * {@link android.media.AudioAttributes} or default duration if not configured
@@ -122,22 +309,73 @@
         if (!isFadeable(aa, INVALID_UID, AudioPlaybackConfiguration.PLAYER_TYPE_UNKNOWN)) {
             return 0;
         }
-        return DEFAULT_FADE_OUT_DURATION_MS;
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_FADE_OUT_DURATION_MS;
+        }
+        return getOptimalFadeOutDuration(aa);
     }
 
     /**
-     * Get the delay to fade in offending players that do not stop after losing audio focus.
+     * Get the delay to fade in offending players that do not stop after losing audio focus
+     *
      * @param aa The {@link android.media.AudioAttributes}
      * @return delay in milliseconds for the
      * {@link android.media.AudioAttributes.Attribute} or default delay if not configured
      */
     public long getDelayFadeInOffenders(@NonNull AudioAttributes aa) {
-        return DEFAULT_DELAY_FADE_IN_OFFENDERS_MS;
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_DELAY_FADE_IN_OFFENDERS_MS;
+        }
+
+        synchronized (mLock) {
+            return getUpdatedFadeManagerConfigLocked().getFadeInDelayForOffenders();
+        }
+    }
+
+    /**
+     * Query {@link android.media.AudioAttributes} that are exempted from fade enforcement
+     *
+     * @return list of {@link android.media.AudioAttributes}
+     */
+    @NonNull
+    public List<AudioAttributes> getUnfadeableAudioAttributes() {
+        // unfadeable audio attributes is only supported with fade manager configurations
+        if (!enableFadeManagerConfiguration()) {
+            return Collections.EMPTY_LIST;
+        }
+
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // when fade is not enabled, return empty list
+            return fadeManagerConfig.isFadeEnabled()
+                    ? fadeManagerConfig.getUnfadeableAudioAttributes() : Collections.EMPTY_LIST;
+        }
+    }
+
+    /**
+     * Query uids that are exempted from fade enforcement
+     *
+     * @return list of uids
+     */
+    @NonNull
+    public List<Integer> getUnfadeableUids() {
+        // unfadeable uids is only supported with fade manager configurations
+        if (!enableFadeManagerConfiguration()) {
+            return Collections.EMPTY_LIST;
+        }
+
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // when fade is not enabled, return empty list
+            return fadeManagerConfig.isFadeEnabled() ? fadeManagerConfig.getUnfadeableUids()
+                    : Collections.EMPTY_LIST;
+        }
     }
 
     /**
      * Check if it is allowed to fade for the given {@link android.media.AudioAttributes},
-     * client uid and {@link android.media.AudioPlaybackConfiguration.PlayerType} config.
+     * client uid and {@link android.media.AudioPlaybackConfiguration.PlayerType} config
+     *
      * @param aa The {@link android.media.AudioAttributes}
      * @param uid The uid of the client owning the player
      * @param playerType The {@link android.media.AudioPlaybackConfiguration.PlayerType}
@@ -145,36 +383,173 @@
      */
     public boolean isFadeable(@NonNull AudioAttributes aa, int uid,
             @AudioPlaybackConfiguration.PlayerType int playerType) {
-        if (isPlayerTypeUnfadeable(playerType)) {
-            if (DEBUG) {
-                Slog.i(TAG, "not fadeable: player type:" + playerType);
+        synchronized (mLock) {
+            if (isPlayerTypeUnfadeableLocked(playerType)) {
+                if (DEBUG) {
+                    Slog.i(TAG, "not fadeable: player type:" + playerType);
+                }
+                return false;
             }
-            return false;
-        }
-        if (isContentTypeUnfadeable(aa.getContentType())) {
-            if (DEBUG) {
-                Slog.i(TAG, "not fadeable: content type:" + aa.getContentType());
+            if (isContentTypeUnfadeableLocked(aa.getContentType())) {
+                if (DEBUG) {
+                    Slog.i(TAG, "not fadeable: content type:" + aa.getContentType());
+                }
+                return false;
             }
-            return false;
-        }
-        if (!isUsageFadeable(aa.getUsage())) {
-            if (DEBUG) {
-                Slog.i(TAG, "not fadeable: usage:" + aa.getUsage());
+            if (!isUsageFadeableLocked(aa.getSystemUsage())) {
+                if (DEBUG) {
+                    Slog.i(TAG, "not fadeable: usage:" + aa.getUsage());
+                }
+                return false;
             }
-            return false;
+            // new configs using fade manager configuration
+            if (isUnfadeableForFadeMgrConfigLocked(aa, uid)) {
+                return false;
+            }
+            return true;
         }
-        return true;
     }
 
-    private boolean isUsageFadeable(int usage) {
-        return getFadeableUsages().contains(usage);
+    /** Tries to get the fade out volume shaper config closest to the audio attributes */
+    private VolumeShaper.Configuration getOptimalFadeOutVolShaperConfig(AudioAttributes aa) {
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // check if the specific audio attributes has a volume shaper config defined
+            VolumeShaper.Configuration volShaperConfig =
+                    fadeManagerConfig.getFadeOutVolumeShaperConfigForAudioAttributes(aa);
+            if (volShaperConfig != null) {
+                return volShaperConfig;
+            }
+
+            // get the volume shaper config for usage
+            // for fadeable usages, this should never return null
+            return fadeManagerConfig.getFadeOutVolumeShaperConfigForUsage(
+                    aa.getSystemUsage());
+        }
     }
 
-    private boolean isContentTypeUnfadeable(int contentType) {
-        return getUnfadeableContentTypes().contains(contentType);
+    /** Tries to get the fade in volume shaper config closest to the audio attributes */
+    private VolumeShaper.Configuration getOptimalFadeInVolShaperConfig(AudioAttributes aa) {
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // check if the specific audio attributes has a volume shaper config defined
+            VolumeShaper.Configuration volShaperConfig =
+                    fadeManagerConfig.getFadeInVolumeShaperConfigForAudioAttributes(aa);
+            if (volShaperConfig != null) {
+                return volShaperConfig;
+            }
+
+            // get the volume shaper config for usage
+            // for fadeable usages, this should never return null
+            return fadeManagerConfig.getFadeInVolumeShaperConfigForUsage(aa.getSystemUsage());
+        }
     }
 
-    private boolean isPlayerTypeUnfadeable(int playerType) {
-        return getUnfadeablePlayerTypes().contains(playerType);
+    /** Tries to get the duration closest to the audio attributes */
+    private long getOptimalFadeOutDuration(AudioAttributes aa) {
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // check if specific audio attributes has a duration defined
+            long duration = fadeManagerConfig.getFadeOutDurationForAudioAttributes(aa);
+            if (duration != FadeManagerConfiguration.DURATION_NOT_SET) {
+                return duration;
+            }
+
+            // get the duration for usage
+            // for fadeable usages, this should never return DURATION_NOT_SET
+            return fadeManagerConfig.getFadeOutDurationForUsage(aa.getSystemUsage());
+        }
+    }
+
+    @GuardedBy("mLock")
+    private boolean isUnfadeableForFadeMgrConfigLocked(AudioAttributes aa, int uid) {
+        if (isAudioAttributesUnfadeableLocked(aa)) {
+            if (DEBUG) {
+                Slog.i(TAG, "not fadeable: aa:" + aa);
+            }
+            return true;
+        }
+        if (isUidUnfadeableLocked(uid)) {
+            if (DEBUG) {
+                Slog.i(TAG, "not fadeable: uid:" + uid);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @GuardedBy("mLock")
+    private boolean isUsageFadeableLocked(int usage) {
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_FADEABLE_USAGES.contains(usage);
+        }
+        return getUpdatedFadeManagerConfigLocked().isUsageFadeable(usage);
+    }
+
+    @GuardedBy("mLock")
+    private boolean isContentTypeUnfadeableLocked(int contentType) {
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_UNFADEABLE_CONTENT_TYPES.contains(contentType);
+        }
+        return getUpdatedFadeManagerConfigLocked().isContentTypeUnfadeable(contentType);
+    }
+
+    @GuardedBy("mLock")
+    private boolean isPlayerTypeUnfadeableLocked(int playerType) {
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_UNFADEABLE_PLAYER_TYPES.contains(playerType);
+        }
+        return getUpdatedFadeManagerConfigLocked().isPlayerTypeUnfadeable(playerType);
+    }
+
+    @GuardedBy("mLock")
+    private boolean isAudioAttributesUnfadeableLocked(AudioAttributes aa) {
+        if (!enableFadeManagerConfiguration()) {
+            // default fade configs do not support unfadeable audio attributes, hence return false
+            return false;
+        }
+        return getUpdatedFadeManagerConfigLocked().isAudioAttributesUnfadeable(aa);
+    }
+
+    @GuardedBy("mLock")
+    private boolean isUidUnfadeableLocked(int uid) {
+        if (!enableFadeManagerConfiguration()) {
+            // default fade configs do not support unfadeable uids, hence return false
+            return false;
+        }
+        return getUpdatedFadeManagerConfigLocked().isUidUnfadeable(uid);
+    }
+
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration getUpdatedFadeManagerConfigLocked() {
+        if (mActiveFadeManagerConfig == null) {
+            mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked();
+        }
+        return mActiveFadeManagerConfig;
+    }
+
+    /** Priority between fade manager configs: Transient > Updated > Default */
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration getActiveFadeMgrConfigLocked() {
+        // below configs are arranged in the order of priority
+        // configs placed higher have higher priority
+        if (mTransientFadeManagerConfig != null) {
+            return mTransientFadeManagerConfig;
+        }
+
+        if (mUpdatedFadeManagerConfig != null) {
+            return mUpdatedFadeManagerConfig;
+        }
+
+        // default - must be the lowest priority
+        return getDefaultFadeManagerConfigLocked();
+    }
+
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration getDefaultFadeManagerConfigLocked() {
+        if (mDefaultFadeManagerConfig == null) {
+            mDefaultFadeManagerConfig = new FadeManagerConfiguration.Builder().build();
+        }
+        return mDefaultFadeManagerConfig;
     }
 }
diff --git a/services/core/java/com/android/server/audio/FadeOutManager.java b/services/core/java/com/android/server/audio/FadeOutManager.java
index 1171f97..2cceb5a 100644
--- a/services/core/java/com/android/server/audio/FadeOutManager.java
+++ b/services/core/java/com/android/server/audio/FadeOutManager.java
@@ -16,21 +16,24 @@
 
 package com.android.server.audio;
 
+import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration;
+
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.AudioPlaybackConfiguration;
+import android.media.FadeManagerConfiguration;
 import android.media.VolumeShaper;
 import android.util.Slog;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.server.utils.EventLogger;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Objects;
+import java.util.List;
+import java.util.Map;
 
 /**
  * Class to handle fading out players
@@ -40,14 +43,6 @@
     public static final String TAG = "AS.FadeOutManager";
 
     private static final boolean DEBUG = PlaybackActivityMonitor.DEBUG;
-    private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED =
-            new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY)
-                    .createIfNeeded()
-                    .build();
-
-    // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp
-    private static final VolumeShaper.Operation PLAY_SKIP_RAMP =
-            new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build();
 
     private final Object mLock = new Object();
 
@@ -57,16 +52,81 @@
     @GuardedBy("mLock")
     private final SparseArray<FadedOutApp> mUidToFadedAppsMap = new SparseArray<>();
 
-    @GuardedBy("mLock")
-    private FadeConfigurations mFadeConfigurations;
+    private final FadeConfigurations mFadeConfigurations = new FadeConfigurations();
 
-    public FadeOutManager() {
-        mFadeConfigurations = new FadeConfigurations();
+    /**
+     * Sets the custom fade manager configuration to be used for player fade out and in
+     *
+     * @param fadeManagerConfig custom fade manager configuration
+     * @return {@link AudioManager#SUCCESS} if setting fade manager config succeeded,
+     *     {@link AudioManager#ERROR} otherwise
+     */
+    int setFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig) {
+        // locked to ensure the fade configs are not updated while faded app state is being updated
+        synchronized (mLock) {
+            return mFadeConfigurations.setFadeManagerConfiguration(fadeManagerConfig);
+        }
     }
 
-    public FadeOutManager(FadeConfigurations fadeConfigurations) {
-        mFadeConfigurations = Objects.requireNonNull(fadeConfigurations,
-                "Fade configurations can not be null");
+    /**
+     * Clears the fade manager configuration that was previously set with
+     * {@link #setFadeManagerConfiguration(FadeManagerConfiguration)}
+     *
+     * @return {@link AudioManager#SUCCESS}  if clearing fade manager config succeeded,
+     *     {@link AudioManager#ERROR} otherwise
+     */
+    int clearFadeManagerConfiguration() {
+        // locked to ensure the fade configs are not updated while faded app state is being updated
+        synchronized (mLock) {
+            return mFadeConfigurations.clearFadeManagerConfiguration();
+        }
+    }
+
+    /**
+     * Returns the active fade manager configuration
+     *
+     * @return the {@link FadeManagerConfiguration}
+     */
+    FadeManagerConfiguration getFadeManagerConfiguration() {
+        return mFadeConfigurations.getFadeManagerConfiguration();
+    }
+
+    /**
+     * Sets the transient fade manager configuration to be used for player fade out and in
+     *
+     * @param fadeManagerConfig fade manager config that has higher priority than the existing
+     *     fade manager configuration. This is expected to be transient.
+     * @return {@link AudioManager#SUCCESS}  if setting fade manager config succeeded,
+     *     {@link AudioManager#ERROR} otherwise
+     */
+    int setTransientFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig) {
+        // locked to ensure the fade configs are not updated while faded app state is being updated
+        synchronized (mLock) {
+            return mFadeConfigurations.setTransientFadeManagerConfiguration(fadeManagerConfig);
+        }
+    }
+
+    /**
+     * Clears the transient fade manager configuration that was previously set with
+     * {@link #setTransientFadeManagerConfiguration(FadeManagerConfiguration)}
+     *
+     * @return {@link AudioManager#SUCCESS}  if clearing fade manager config succeeded,
+     *      {@link AudioManager#ERROR} otherwise
+     */
+    int clearTransientFadeManagerConfiguration() {
+        // locked to ensure the fade configs are not updated while faded app state is being updated
+        synchronized (mLock) {
+            return mFadeConfigurations.clearTransientFadeManagerConfiguration();
+        }
+    }
+
+    /**
+     * Query if fade is enblead and can be enforced on players
+     *
+     * @return {@code true} if fade is enabled, {@code false} otherwise.
+     */
+    boolean isFadeEnabled() {
+        return mFadeConfigurations.isFadeEnabled();
     }
 
     // TODO explore whether a shorter fade out would be a better UX instead of not fading out at all
@@ -128,7 +188,7 @@
         }
     }
 
-    void fadeOutUid(int uid, ArrayList<AudioPlaybackConfiguration> players) {
+    void fadeOutUid(int uid, List<AudioPlaybackConfiguration> players) {
         Slog.i(TAG, "fadeOutUid() uid:" + uid);
         synchronized (mLock) {
             if (!mUidToFadedAppsMap.contains(uid)) {
@@ -148,15 +208,31 @@
      * @param uid the uid for the app to unfade out
      * @param players map of current available players (so we can get an APC from piid)
      */
-    void unfadeOutUid(int uid, HashMap<Integer, AudioPlaybackConfiguration> players) {
+    void unfadeOutUid(int uid, Map<Integer, AudioPlaybackConfiguration> players) {
         Slog.i(TAG, "unfadeOutUid() uid:" + uid);
         synchronized (mLock) {
-            final FadedOutApp fa = mUidToFadedAppsMap.get(uid);
+            FadedOutApp fa = mUidToFadedAppsMap.get(uid);
             if (fa == null) {
                 return;
             }
             mUidToFadedAppsMap.remove(uid);
-            fa.removeUnfadeAll(players);
+
+            if (!enableFadeManagerConfiguration()) {
+                fa.removeUnfadeAll(players);
+                return;
+            }
+
+            // since fade manager configs may have volume-shaper config per audio attributes,
+            // iterate through each palyer and gather respective configs  for fade in
+            ArrayList<AudioPlaybackConfiguration> apcs = new ArrayList<>(players.values());
+            for (int index = 0; index < apcs.size(); index++) {
+                AudioPlaybackConfiguration apc = apcs.get(index);
+                VolumeShaper.Configuration config = mFadeConfigurations
+                        .getFadeInVolumeShaperConfig(apc.getAudioAttributes());
+                fa.fadeInPlayer(apc, config);
+            }
+            // ideal case all players should be faded in
+            fa.clear();
         }
     }
 
@@ -209,16 +285,6 @@
         }
     }
 
-    /**
-     * Update fade configurations used for player fade operations
-     * @param fadeConfigurations set of configs that define fade properties
-     */
-    void setFadeConfigurations(@NonNull FadeConfigurations fadeConfigurations) {
-        synchronized (mLock) {
-            mFadeConfigurations = fadeConfigurations;
-        }
-    }
-
     void dump(PrintWriter pw) {
         synchronized (mLock) {
             for (int index = 0; index < mUidToFadedAppsMap.size(); index++) {
@@ -232,6 +298,15 @@
      * Class to group players from a common app, that are faded out.
      */
     private static final class FadedOutApp {
+        private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED =
+                new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY)
+                        .createIfNeeded()
+                        .build();
+
+        // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp
+        private static final VolumeShaper.Operation PLAY_SKIP_RAMP =
+                new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build();
+
         private final int mUid;
         // key -> piid; value -> volume shaper config applied
         private final SparseArray<VolumeShaper.Configuration> mFadedPlayers = new SparseArray<>();
@@ -269,17 +344,8 @@
                 return;
             }
             if (apc.getPlayerProxy() != null) {
-                try {
-                    PlaybackActivityMonitor.sEventLogger.enqueue(
-                            (new PlaybackActivityMonitor.FadeOutEvent(apc, skipRamp)).printLog(
-                                    TAG));
-                    apc.getPlayerProxy().applyVolumeShaper(volShaper,
-                            skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED);
-                    mFadedPlayers.put(piid, volShaper);
-                } catch (Exception e) {
-                    Slog.e(TAG, "Error fading out player piid:" + piid
-                            + " uid:" + apc.getClientUid(), e);
-                }
+                applyVolumeShaperInternal(apc, piid, volShaper,
+                        skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED);
             } else {
                 if (DEBUG) {
                     Slog.v(TAG, "Error fading out player piid:" + piid
@@ -288,21 +354,13 @@
             }
         }
 
-        void removeUnfadeAll(HashMap<Integer, AudioPlaybackConfiguration> players) {
+        void removeUnfadeAll(Map<Integer, AudioPlaybackConfiguration> players) {
             for (int index = 0; index < mFadedPlayers.size(); index++) {
                 int piid = mFadedPlayers.keyAt(index);
                 final AudioPlaybackConfiguration apc = players.get(piid);
                 if ((apc != null) && (apc.getPlayerProxy() != null)) {
-                    final VolumeShaper.Configuration volShaper = mFadedPlayers.valueAt(index);
-                    try {
-                        PlaybackActivityMonitor.sEventLogger.enqueue(
-                                (new EventLogger.StringEvent("unfading out piid:"
-                                        + piid)).printLog(TAG));
-                        apc.getPlayerProxy().applyVolumeShaper(volShaper,
-                                VolumeShaper.Operation.REVERSE);
-                    } catch (Exception e) {
-                        Slog.e(TAG, "Error unfading out player piid:" + piid + " uid:" + mUid, e);
-                    }
+                    applyVolumeShaperInternal(apc, piid, /* volShaperConfig= */ null,
+                            VolumeShaper.Operation.REVERSE);
                 } else {
                     // this piid was in the list of faded players, but wasn't found
                     if (DEBUG) {
@@ -314,8 +372,61 @@
             mFadedPlayers.clear();
         }
 
+        void fadeInPlayer(@NonNull AudioPlaybackConfiguration apc,
+                @Nullable VolumeShaper.Configuration config) {
+            int piid = Integer.valueOf(apc.getPlayerInterfaceId());
+            // if not found, no need to fade in since it was never faded out
+            if (!mFadedPlayers.contains(piid)) {
+                if (DEBUG) {
+                    Slog.v(TAG, "Player (piid: " + piid + ") for uid (" + mUid
+                            + ") is not faded out, no need to fade in");
+                }
+                return;
+            }
+
+            mFadedPlayers.remove(piid);
+            if (apc.getPlayerProxy() != null) {
+                applyVolumeShaperInternal(apc, piid, config,
+                        config != null ? PLAY_CREATE_IF_NEEDED : VolumeShaper.Operation.REVERSE);
+            } else {
+                if (DEBUG) {
+                    Slog.v(TAG, "Error fading in player piid:" + piid
+                            + ", player not found for uid " + mUid);
+                }
+            }
+        }
+
+        void clear() {
+            if (mFadedPlayers.size() > 0) {
+                if (DEBUG) {
+                    Slog.v(TAG, "Non empty faded players list being cleared! Faded out players:"
+                            + mFadedPlayers);
+                }
+            }
+            // should the players be faded in irrespective?
+            mFadedPlayers.clear();
+        }
+
         void removeReleased(@NonNull AudioPlaybackConfiguration apc) {
             mFadedPlayers.delete(Integer.valueOf(apc.getPlayerInterfaceId()));
         }
+
+        private void applyVolumeShaperInternal(AudioPlaybackConfiguration apc, int piid,
+                VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation) {
+            VolumeShaper.Configuration config = volShaperConfig;
+            // when operation is reverse, use the fade out volume shaper config instead
+            if (operation.equals(VolumeShaper.Operation.REVERSE)) {
+                config = mFadedPlayers.get(piid);
+            }
+            try {
+                PlaybackActivityMonitor.sEventLogger.enqueue(
+                        (new PlaybackActivityMonitor.FadeEvent(apc, config, operation))
+                                .printLog(TAG));
+                apc.getPlayerProxy().applyVolumeShaper(config, operation);
+            } catch (Exception e) {
+                Slog.e(TAG, "Error fading player piid:" + piid + " uid:" + mUid
+                        + " operation:" + operation, e);
+            }
+        }
     }
 }
diff --git a/services/core/java/com/android/server/audio/FocusRequester.java b/services/core/java/com/android/server/audio/FocusRequester.java
index 00c04ff..f462539 100644
--- a/services/core/java/com/android/server/audio/FocusRequester.java
+++ b/services/core/java/com/android/server/audio/FocusRequester.java
@@ -33,6 +33,7 @@
 import com.android.server.pm.UserManagerInternal;
 
 import java.io.PrintWriter;
+import java.util.List;
 
 /**
  * @hide
@@ -534,6 +535,33 @@
         return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
     }
 
+    @GuardedBy("MediaFocusControl.mAudioFocusLock")
+    int dispatchFocusChangeWithFadeLocked(int focusChange, List<FocusRequester> otherActiveFrs) {
+        if (focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+                || focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
+                || focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+                || focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+            mFocusLossFadeLimbo = false;
+            mFocusController.restoreVShapedPlayers(this);
+        } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS
+                && mFocusController.shouldEnforceFade()) {
+            for (int index = 0; index < otherActiveFrs.size(); index++) {
+                // candidate for fade-out before a receiving a loss
+                if (mFocusController.fadeOutPlayers(otherActiveFrs.get(index), /* loser= */ this)) {
+                    // active players are being faded out, delay the dispatch of focus loss
+                    // mark this instance as being faded so it's not released yet as the focus loss
+                    // will be dispatched later, it is now in limbo mode
+                    mFocusLossFadeLimbo = true;
+                    mFocusController.postDelayedLossAfterFade(this,
+                            mFocusController.getFadeOutDurationOnFocusLossMillis(
+                                    this.getAudioAttributes()));
+                    return AudioManager.AUDIOFOCUS_REQUEST_DELAYED;
+                }
+            }
+        }
+        return dispatchFocusChange(focusChange);
+    }
+
     void dispatchFocusResultFromExtPolicy(int requestResult) {
         final IAudioFocusDispatcher fd = mFocusDispatcher;
         if (fd == null) {
diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java
index 58f5d5e..0df0006 100644
--- a/services/core/java/com/android/server/audio/MediaFocusControl.java
+++ b/services/core/java/com/android/server/audio/MediaFocusControl.java
@@ -16,6 +16,8 @@
 
 package com.android.server.audio;
 
+import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
@@ -195,6 +197,15 @@
         }
         return mFocusEnforcer.getFadeInDelayForOffendersMillis(aa);
     }
+
+    @Override
+    public boolean shouldEnforceFade() {
+        if (!enableFadeManagerConfiguration()) {
+            return ENFORCE_FADEOUT_FOR_FOCUS_LOSS;
+        }
+
+        return mFocusEnforcer.shouldEnforceFade();
+    }
     //==========================================================================================
     // AudioFocus
     //==========================================================================================
@@ -861,14 +872,17 @@
                 return;
             }
         }
-        final FocusRequester fr;
-        if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
-            fr = mFocusOwnersForFocusPolicy.remove(afi.getClientId());
-        } else {
-            fr = mFocusOwnersForFocusPolicy.get(afi.getClientId());
-        }
-        if (fr != null) {
-            fr.dispatchFocusResultFromExtPolicy(requestResult);
+        synchronized (mAudioFocusLock) {
+            FocusRequester fr = getFocusRequesterLocked(afi.getClientId(),
+                    /* shouldRemove= */ requestResult == AudioManager.AUDIOFOCUS_REQUEST_FAILED);
+            if (fr != null) {
+                fr.dispatchFocusResultFromExtPolicy(requestResult);
+                // if fade is enabled for external focus policies, apply it when setting
+                // focus result as well
+                if (enableFadeManagerConfiguration()) {
+                    fr.handleFocusGainFromRequest(requestResult);
+                }
+            }
         }
     }
 
@@ -902,24 +916,80 @@
                     + afi.getClientId());
         }
         synchronized (mAudioFocusLock) {
-            if (mFocusPolicy == null) {
-                if (DEBUG) { Log.v(TAG, "> failed: no focus policy" ); }
-                return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
-            }
-            final FocusRequester fr;
-            if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
-                fr = mFocusOwnersForFocusPolicy.remove(afi.getClientId());
-            } else {
-                fr = mFocusOwnersForFocusPolicy.get(afi.getClientId());
-            }
+            FocusRequester fr = getFocusRequesterLocked(afi.getClientId(),
+                    /* shouldRemove= */ focusChange == AudioManager.AUDIOFOCUS_LOSS);
             if (fr == null) {
-                if (DEBUG) { Log.v(TAG, "> failed: no such focus requester known" ); }
+                if (DEBUG) {
+                    Log.v(TAG, "> failed: no such focus requester known");
+                }
                 return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
             }
             return fr.dispatchFocusChange(focusChange);
         }
     }
 
+    int dispatchFocusChangeWithFade(AudioFocusInfo afi, int focusChange,
+            List<AudioFocusInfo> otherActiveAfis) {
+        if (DEBUG) {
+            Log.v(TAG, "dispatchFocusChangeWithFade " + AudioManager.audioFocusToString(focusChange)
+                    + " to afi client=" + afi.getClientId()
+                    + " other active afis=" + otherActiveAfis);
+        }
+
+        synchronized (mAudioFocusLock) {
+            String clientId = afi.getClientId();
+            // do not remove the entry since it can be posted for fade
+            FocusRequester fr = getFocusRequesterLocked(clientId, /* shouldRemove= */ false);
+            if (fr == null) {
+                if (DEBUG) {
+                    Log.v(TAG, "> failed: no such focus requester known");
+                }
+                return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+            }
+
+            // convert other AudioFocusInfo to corresponding FocusRequester
+            ArrayList<FocusRequester> otherActiveFrs = new ArrayList<>();
+            for (int index = 0; index < otherActiveAfis.size(); index++) {
+                FocusRequester otherFr = getFocusRequesterLocked(
+                        otherActiveAfis.get(index).getClientId(), /* shouldRemove= */ false);
+                if (otherFr == null) {
+                    continue;
+                }
+                otherActiveFrs.add(otherFr);
+            }
+
+            int status = fr.dispatchFocusChangeWithFadeLocked(focusChange, otherActiveFrs);
+            if (status != AudioManager.AUDIOFOCUS_REQUEST_DELAYED
+                    && focusChange == AudioManager.AUDIOFOCUS_LOSS) {
+                mFocusOwnersForFocusPolicy.remove(clientId);
+            }
+
+            return status;
+        }
+    }
+
+    @GuardedBy("mAudioFocusLock")
+    private FocusRequester getFocusRequesterLocked(String clientId, boolean shouldRemove) {
+        if (mFocusPolicy == null) {
+            if (DEBUG) {
+                Log.v(TAG, "> failed: no focus policy");
+            }
+            return null;
+        }
+
+        FocusRequester fr;
+        if (shouldRemove) {
+            fr = mFocusOwnersForFocusPolicy.remove(clientId);
+        } else {
+            fr = mFocusOwnersForFocusPolicy.get(clientId);
+        }
+
+        if (fr == null && DEBUG) {
+            Log.v(TAG, "> failed: no such focus requester known");
+        }
+        return fr;
+    }
+
     private void dumpExtFocusPolicyFocusOwners(PrintWriter pw) {
         final Set<Entry<String, FocusRequester>> owners = mFocusOwnersForFocusPolicy.entrySet();
         final Iterator<Entry<String, FocusRequester>> ownerIterator = owners.iterator();
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index bc9b9b4..e69fbbd 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -38,6 +38,7 @@
 import android.media.AudioPlaybackConfiguration.FormatInfo;
 import android.media.AudioPlaybackConfiguration.PlayerMuteEvent;
 import android.media.AudioSystem;
+import android.media.FadeManagerConfiguration;
 import android.media.IPlaybackConfigDispatcher;
 import android.media.PlayerBase;
 import android.media.VolumeShaper;
@@ -156,8 +157,7 @@
     private final int mMaxAlarmVolume;
     private int mPrivilegedAlarmActiveCount = 0;
     private final Consumer<AudioDeviceAttributes> mMuteAwaitConnectionTimeoutCb;
-    private final FadeOutManager mFadeOutManager;
-
+    private final FadeOutManager mFadeOutManager = new FadeOutManager();
 
     PlaybackActivityMonitor(Context context, int maxAlarmVolume,
             Consumer<AudioDeviceAttributes> muteTimeoutCallback) {
@@ -167,7 +167,6 @@
         AudioPlaybackConfiguration.sPlayerDeathMonitor = this;
         mMuteAwaitConnectionTimeoutCb = muteTimeoutCallback;
         initEventHandler();
-        mFadeOutManager = new FadeOutManager(new FadeConfigurations());
     }
 
     //=================================================================
@@ -971,6 +970,12 @@
         return mFadeOutManager.getFadeInDelayForOffendersMillis(aa);
     }
 
+    @Override
+    public boolean shouldEnforceFade() {
+        return mFadeOutManager.isFadeEnabled();
+    }
+
+
     //=================================================================
     // Track playback activity listeners
 
@@ -1010,6 +1015,27 @@
         }
     }
 
+    int setFadeManagerConfiguration(int focusType, FadeManagerConfiguration fadeMgrConfig) {
+        return mFadeOutManager.setFadeManagerConfiguration(fadeMgrConfig);
+    }
+
+    int clearFadeManagerConfiguration(int focusType) {
+        return mFadeOutManager.clearFadeManagerConfiguration();
+    }
+
+    FadeManagerConfiguration getFadeManagerConfiguration(int focusType) {
+        return mFadeOutManager.getFadeManagerConfiguration();
+    }
+
+    int setTransientFadeManagerConfiguration(int focusType,
+            FadeManagerConfiguration fadeMgrConfig) {
+        return mFadeOutManager.setTransientFadeManagerConfiguration(fadeMgrConfig);
+    }
+
+    int clearTransientFadeManagerConfiguration(int focusType) {
+        return mFadeOutManager.clearTransientFadeManagerConfiguration();
+    }
+
     /**
      * Inner class to track clients that want to be notified of playback updates
      */
@@ -1337,6 +1363,38 @@
         }
     }
 
+    static final class FadeEvent extends EventLogger.Event {
+        private final int mPlayerIId;
+        private final int mPlayerType;
+        private final int mClientUid;
+        private final int mClientPid;
+        private final AudioAttributes mPlayerAttr;
+        private final VolumeShaper.Configuration mVShaper;
+        private final VolumeShaper.Operation mVOperation;
+
+        FadeEvent(AudioPlaybackConfiguration apc, VolumeShaper.Configuration vShaper,
+                VolumeShaper.Operation vOperation) {
+            mPlayerIId = apc.getPlayerInterfaceId();
+            mClientUid = apc.getClientUid();
+            mClientPid = apc.getClientPid();
+            mPlayerAttr = apc.getAudioAttributes();
+            mPlayerType = apc.getPlayerType();
+            mVShaper = vShaper;
+            mVOperation = vOperation;
+        }
+
+        @Override
+        public String eventToString() {
+            return "Fade Event:" + " player piid:" + mPlayerIId
+                    + " uid/pid:" + mClientUid + "/" + mClientPid
+                    + " player type:"
+                    + AudioPlaybackConfiguration.toLogFriendlyPlayerType(mPlayerType)
+                    + " attr:" + mPlayerAttr
+                    + " volume shaper:" + mVShaper
+                    + " volume operation:" + mVOperation;
+        }
+    }
+
     private abstract static class VolumeShaperEvent extends EventLogger.Event {
         private final int mPlayerIId;
         private final boolean mSkipRamp;
diff --git a/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java b/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java
index f1d42f3..4a29eca 100644
--- a/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java
+++ b/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java
@@ -79,4 +79,11 @@
      * @return delay in milliseconds
      */
     long getFadeInDelayForOffendersMillis(@NonNull AudioAttributes aa);
+
+    /**
+     * Check if the fade should be enforced
+     *
+     * @return {@code true} if fade should be enforced, {@code false} otherwise
+     */
+    boolean shouldEnforceFade();
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 4f7f31d..8428f12 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -1639,8 +1639,7 @@
             return  -1;
         }
         final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);
-        List<String> deviceAddresses = mAudioService.getDeviceAddresses(currentDevice);
-
+        List<String> deviceAddresses = mAudioService.getDeviceIdentityAddresses(currentDevice);
         // We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion
         // with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR
         // and Sensor.TYPE_GAME_ROTATION_VECTOR are supported internally by
diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java
index dafea9a..d5d8fd2 100644
--- a/services/core/java/com/android/server/biometrics/AuthService.java
+++ b/services/core/java/com/android/server/biometrics/AuthService.java
@@ -51,9 +51,13 @@
 import android.hardware.biometrics.PromptInfo;
 import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.biometrics.SensorPropertiesInternal;
+import android.hardware.biometrics.face.IFace;
+import android.hardware.biometrics.fingerprint.IFingerprint;
+import android.hardware.face.FaceSensorConfigurations;
 import android.hardware.face.FaceSensorProperties;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.face.IFaceService;
+import android.hardware.fingerprint.FingerprintSensorConfigurations;
 import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintService;
@@ -122,8 +126,6 @@
 
         /**
          * Allows to test with various device sensor configurations.
-         * @param context
-         * @return
          */
         @VisibleForTesting
         public String[] getConfiguration(Context context) {
@@ -131,6 +133,30 @@
         }
 
         /**
+         * Allows to test with various device sensor configurations.
+         */
+        @VisibleForTesting
+        public String[] getFingerprintConfiguration(Context context) {
+            return getConfiguration(context);
+        }
+
+        /**
+         * Allows to test with various device sensor configurations.
+         */
+        @VisibleForTesting
+        public String[] getFaceConfiguration(Context context) {
+            return getConfiguration(context);
+        }
+
+        /**
+         * Allows to test with various device sensor configurations.
+         */
+        @VisibleForTesting
+        public String[] getIrisConfiguration(Context context) {
+            return getConfiguration(context);
+        }
+
+        /**
          * Allows us to mock FingerprintService for testing
          */
         @VisibleForTesting
@@ -173,6 +199,22 @@
             }
             return false;
         }
+
+        /**
+         * Allows to test with various fingerprint aidl instances.
+         */
+        @VisibleForTesting
+        public String[] getFingerprintAidlInstances() {
+            return ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR);
+        }
+
+        /**
+         * Allows to test with various face aidl instances.
+         */
+        @VisibleForTesting
+        public String[] getFaceAidlInstances() {
+            return ServiceManager.getDeclaredInstances(IFace.DESCRIPTOR);
+        }
     }
 
     private final class AuthServiceImpl extends IAuthService.Stub {
@@ -717,12 +759,129 @@
             hidlConfigs = null;
         }
 
-        // Registers HIDL and AIDL authenticators, but only HIDL configs need to be provided.
-        registerAuthenticators(hidlConfigs);
+        if (com.android.server.biometrics.Flags.deHidl()) {
+            Slog.d(TAG, "deHidl flag is on.");
+            registerAuthenticators();
+        } else {
+            // Registers HIDL and AIDL authenticators, but only HIDL configs need to be provided.
+            registerAuthenticators(hidlConfigs);
+        }
 
         mInjector.publishBinderService(this, mImpl);
     }
 
+    private void registerAuthenticators() {
+        registerFingerprintSensors(mInjector.getFingerprintAidlInstances(),
+                mInjector.getFingerprintConfiguration(getContext()));
+        registerFaceSensors(mInjector.getFaceAidlInstances(),
+                mInjector.getFaceConfiguration(getContext()));
+        registerIrisSensors(mInjector.getIrisConfiguration(getContext()));
+    }
+
+    private void registerIrisSensors(String[] hidlConfigStrings) {
+        final SensorConfig[] hidlConfigs;
+        if (!mInjector.isHidlDisabled(getContext())) {
+            final int firstApiLevel = SystemProperties.getInt(SYSPROP_FIRST_API_LEVEL, 0);
+            final int apiLevel = SystemProperties.getInt(SYSPROP_API_LEVEL, firstApiLevel);
+            if (hidlConfigStrings.length == 0 && apiLevel == Build.VERSION_CODES.R) {
+                // For backwards compatibility with R where biometrics could work without being
+                // configured in config_biometric_sensors. In the absence of a vendor provided
+                // configuration, we assume the weakest biometric strength (i.e. convenience).
+                Slog.w(TAG, "Found R vendor partition without config_biometric_sensors");
+                hidlConfigStrings = generateRSdkCompatibleConfiguration();
+            }
+            hidlConfigs = new SensorConfig[hidlConfigStrings.length];
+            for (int i = 0; i < hidlConfigStrings.length; ++i) {
+                hidlConfigs[i] = new SensorConfig(hidlConfigStrings[i]);
+            }
+        } else {
+            hidlConfigs = null;
+        }
+
+        final List<SensorPropertiesInternal> hidlIrisSensors = new ArrayList<>();
+
+        if (hidlConfigs != null) {
+            for (SensorConfig sensor : hidlConfigs) {
+                switch (sensor.modality) {
+                    case TYPE_IRIS:
+                        hidlIrisSensors.add(getHidlIrisSensorProps(sensor.id, sensor.strength));
+                        break;
+
+                    default:
+                        Slog.e(TAG, "Unknown modality: " + sensor.modality);
+                }
+            }
+        }
+
+        final IIrisService irisService = mInjector.getIrisService();
+        if (irisService != null) {
+            try {
+                irisService.registerAuthenticators(hidlIrisSensors);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "RemoteException when registering iris authenticators", e);
+            }
+        } else if (hidlIrisSensors.size() > 0) {
+            Slog.e(TAG, "HIDL iris configuration exists, but IrisService is null.");
+        }
+    }
+
+    private void registerFaceSensors(final String[] faceAidlInstances,
+            final String[] hidlConfigStrings) {
+        final FaceSensorConfigurations mFaceSensorConfigurations =
+                new FaceSensorConfigurations(hidlConfigStrings != null
+                        && hidlConfigStrings.length > 0);
+
+        if (hidlConfigStrings != null && hidlConfigStrings.length > 0) {
+            mFaceSensorConfigurations.addHidlConfigs(
+                    hidlConfigStrings, getContext());
+        }
+
+        if (faceAidlInstances != null && faceAidlInstances.length > 0) {
+            mFaceSensorConfigurations.addAidlConfigs(faceAidlInstances,
+                    name -> IFace.Stub.asInterface(Binder.allowBlocking(
+                            ServiceManager.waitForDeclaredService(name))));
+        }
+
+        final IFaceService faceService = mInjector.getFaceService();
+        if (faceService != null) {
+            try {
+                faceService.registerAuthenticatorsLegacy(mFaceSensorConfigurations);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "RemoteException when registering face authenticators", e);
+            }
+        }  else if (mFaceSensorConfigurations.hasSensorConfigurations()) {
+            Slog.e(TAG, "Face configuration exists, but FaceService is null.");
+        }
+    }
+
+    private void registerFingerprintSensors(final String[] fingerprintAidlInstances,
+            final String[] hidlConfigStrings) {
+        final FingerprintSensorConfigurations mFingerprintSensorConfigurations =
+                new FingerprintSensorConfigurations(!(hidlConfigStrings != null
+                        && hidlConfigStrings.length > 0));
+
+        if (hidlConfigStrings != null && hidlConfigStrings.length > 0) {
+            mFingerprintSensorConfigurations.addHidlSensors(hidlConfigStrings, getContext());
+        }
+
+        if (fingerprintAidlInstances != null && fingerprintAidlInstances.length > 0) {
+            mFingerprintSensorConfigurations.addAidlSensors(fingerprintAidlInstances,
+                    name -> IFingerprint.Stub.asInterface(Binder.allowBlocking(
+                            ServiceManager.waitForDeclaredService(name))));
+        }
+
+        final IFingerprintService fingerprintService = mInjector.getFingerprintService();
+        if (fingerprintService != null) {
+            try {
+                fingerprintService.registerAuthenticatorsLegacy(mFingerprintSensorConfigurations);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "RemoteException when registering fingerprint authenticators", e);
+            }
+        }  else if (mFingerprintSensorConfigurations.hasSensorConfigurations()) {
+            Slog.e(TAG, "Fingerprint configuration exists, but FingerprintService is null.");
+        }
+    }
+
     /**
      * Generates an array of string configs with entries that correspond to the biometric features
      * declared on the device. Returns an empty array if no biometric features are declared.
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 037ea38a..89b638b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
@@ -202,7 +202,7 @@
     };
 
     @VisibleForTesting
-    BiometricScheduler(@NonNull String tag,
+    public BiometricScheduler(@NonNull String tag,
             @NonNull Handler handler,
             @SensorType int sensorType,
             @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
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 57feedc..0c3dfa7 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
@@ -387,6 +387,11 @@
         return isAuthentication || isDetection;
     }
 
+    /** If this operation is {@link StartUserClient}. */
+    public boolean isStartUserOperation() {
+        return mClientMonitor instanceof StartUserClient<?, ?>;
+    }
+
     /** If this operation performs acquisition {@link AcquisitionClient}. */
     public boolean isAcquisitionOperation() {
         return mClientMonitor instanceof AcquisitionClient;
diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
index 7f8f38f..6daaad1 100644
--- a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
@@ -54,8 +54,6 @@
         // is all done internally.
         super(context, lazyDaemon, token, null /* ClientMonitorCallbackConverter */, userId, owner,
                 0 /* cookie */, sensorId, logger, biometricContext);
-        //, BiometricsProtoEnums.ACTION_ENUMERATE,
-          //      BiometricsProtoEnums.CLIENT_UNKNOWN);
         mEnrolledList = enrolledList;
         mUtils = utils;
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
index 8075470..3753bbd 100644
--- a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
@@ -135,7 +135,7 @@
         final int currentUserId = mCurrentUserRetriever.getCurrentUserId();
         final int nextUserId = mPendingOperations.getFirst().getTargetUserId();
 
-        if (nextUserId == currentUserId) {
+        if (nextUserId == currentUserId || mPendingOperations.getFirst().isStartUserOperation()) {
             super.startNextOperationIfIdle();
         } else if (currentUserId == UserHandle.USER_NULL) {
             final BaseClientMonitor startClient =
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 578d9dc..6af223b 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
@@ -36,6 +36,7 @@
 import android.hardware.biometrics.face.SensorProps;
 import android.hardware.face.Face;
 import android.hardware.face.FaceAuthenticateOptions;
+import android.hardware.face.FaceSensorConfigurations;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.face.FaceServiceReceiver;
 import android.hardware.face.IFaceAuthenticatorsRegisteredCallback;
@@ -55,6 +56,7 @@
 import android.util.proto.ProtoOutputStream;
 import android.view.Surface;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.SystemService;
@@ -76,6 +78,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Supplier;
 
 /**
  * A service to manage multiple clients that want to access the face HAL API.
@@ -86,7 +89,7 @@
 
     protected static final String TAG = "FaceService";
 
-    private final FaceServiceWrapper mServiceWrapper;
+    @VisibleForTesting final FaceServiceWrapper mServiceWrapper;
     private final LockoutResetDispatcher mLockoutResetDispatcher;
     private final LockPatternUtils mLockPatternUtils;
     @NonNull
@@ -94,11 +97,18 @@
     @NonNull
     private final BiometricStateCallback<ServiceProvider, FaceSensorPropertiesInternal>
             mBiometricStateCallback;
+    @NonNull
+    private final FaceProviderFunction mFaceProviderFunction;
+
+    interface FaceProviderFunction {
+        FaceProvider getFaceProvider(Pair<String, SensorProps[]> filteredSensorProps,
+                boolean resetLockoutRequiresChallenge);
+    }
 
     /**
      * Receives the incoming binder calls from FaceManager.
      */
-    private final class FaceServiceWrapper extends IFaceService.Stub {
+    @VisibleForTesting final class FaceServiceWrapper extends IFaceService.Stub {
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
@@ -672,7 +682,8 @@
                     final SensorProps[] props = face.getSensorProps();
                     final FaceProvider provider = new FaceProvider(getContext(),
                             mBiometricStateCallback, props, instance, mLockoutResetDispatcher,
-                            BiometricContext.getInstance(getContext()));
+                            BiometricContext.getInstance(getContext()),
+                            false /* resetLockoutRequiresChallenge */);
                     providers.add(provider);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
@@ -704,6 +715,55 @@
             });
         }
 
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        public void registerAuthenticatorsLegacy(
+                FaceSensorConfigurations faceSensorConfigurations) {
+            super.registerAuthenticatorsLegacy_enforcePermission();
+
+            if (!faceSensorConfigurations.hasSensorConfigurations()) {
+                Slog.d(TAG, "No face sensors to register.");
+                return;
+            }
+            mRegistry.registerAll(() -> getProviders(faceSensorConfigurations));
+        }
+
+        private List<ServiceProvider> getProviders(
+                FaceSensorConfigurations faceSensorConfigurations) {
+            final List<ServiceProvider> providers = new ArrayList<>();
+            final Pair<String, SensorProps[]> filteredSensorProps =
+                    filterAvailableHalInstances(faceSensorConfigurations);
+            providers.add(mFaceProviderFunction.getFaceProvider(filteredSensorProps,
+                    faceSensorConfigurations.getResetLockoutRequiresChallenge()));
+            return providers;
+        }
+
+        @NonNull
+        private Pair<String, SensorProps[]> filterAvailableHalInstances(
+                FaceSensorConfigurations faceSensorConfigurations) {
+            Pair<String, SensorProps[]> finalSensorPair = faceSensorConfigurations.getSensorPair();
+
+            if (faceSensorConfigurations.isSingleSensorConfigurationPresent()) {
+                return finalSensorPair;
+            }
+
+            final Pair<String, SensorProps[]> virtualSensorProps = faceSensorConfigurations
+                    .getSensorPairForInstance("virtual");
+
+            if (Utils.isVirtualEnabled(getContext())) {
+                if (virtualSensorProps != null) {
+                    return virtualSensorProps;
+                } else {
+                    Slog.e(TAG, "Could not find virtual interface while it is enabled");
+                    return finalSensorPair;
+                }
+            } else {
+                if (virtualSensorProps != null) {
+                    return faceSensorConfigurations.getSensorPairNotForInstance("virtual");
+                }
+            }
+            return finalSensorPair;
+        }
+
         private Pair<List<FaceSensorPropertiesInternal>, List<String>>
                 filterAvailableHalInstances(
                 @NonNull List<FaceSensorPropertiesInternal> hidlInstances,
@@ -752,20 +812,36 @@
     }
 
     public FaceService(Context context) {
+        this(context, null /* faceProviderFunction */, () -> IBiometricService.Stub.asInterface(
+                ServiceManager.getService(Context.BIOMETRIC_SERVICE)));
+    }
+
+    @VisibleForTesting FaceService(Context context, FaceProviderFunction faceProviderFunction,
+            Supplier<IBiometricService> biometricServiceSupplier) {
         super(context);
         mServiceWrapper = new FaceServiceWrapper();
         mLockoutResetDispatcher = new LockoutResetDispatcher(context);
         mLockPatternUtils = new LockPatternUtils(context);
         mBiometricStateCallback = new BiometricStateCallback<>(UserManager.get(context));
-        mRegistry = new FaceServiceRegistry(mServiceWrapper,
-                () -> IBiometricService.Stub.asInterface(
-                        ServiceManager.getService(Context.BIOMETRIC_SERVICE)));
+        mRegistry = new FaceServiceRegistry(mServiceWrapper, biometricServiceSupplier);
         mRegistry.addAllRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() {
             @Override
             public void onAllAuthenticatorsRegistered(List<FaceSensorPropertiesInternal> sensors) {
                 mBiometricStateCallback.start(mRegistry.getProviders());
             }
         });
+
+        if (Flags.deHidl()) {
+            mFaceProviderFunction = faceProviderFunction != null ? faceProviderFunction :
+                    ((filteredSensorProps, resetLockoutRequiresChallenge) -> new FaceProvider(
+                            getContext(), mBiometricStateCallback,
+                            filteredSensorProps.second,
+                            filteredSensorProps.first, mLockoutResetDispatcher,
+                            BiometricContext.getInstance(getContext()),
+                            resetLockoutRequiresChallenge));
+        } else {
+            mFaceProviderFunction = ((filteredSensorProps, resetLockoutRequiresChallenge) -> null);
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/LockoutHalImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/LockoutHalImpl.java
index e5d4a63..ef2be79 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/LockoutHalImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/LockoutHalImpl.java
@@ -24,7 +24,8 @@
  * the user changes.
  */
 public class LockoutHalImpl implements LockoutTracker {
-    private @LockoutMode int mCurrentUserLockoutMode;
+    @LockoutMode
+    private int mCurrentUserLockoutMode;
 
     @Override
     public int getLockoutModeForUser(int userId) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java
index 57f5b34..098be21 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java
@@ -27,6 +27,7 @@
 import android.hardware.keymaster.HardwareAuthToken;
 import android.util.Slog;
 
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.HardwareAuthTokenUtils;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.sensors.AcquisitionClient;
@@ -59,6 +60,20 @@
         void onHardwareUnavailable();
     }
 
+    /**
+     * Interface to send results to the AidlResponseHandler's owner.
+     */
+    public interface AidlResponseHandlerCallback {
+        /**
+         * Invoked when enrollment is successful.
+         */
+        void onEnrollSuccess();
+        /**
+         * Invoked when the HAL sends ERROR_HW_UNAVAILABLE.
+         */
+        void onHardwareUnavailable();
+    }
+
     private static final String TAG = "AidlResponseHandler";
 
     @NonNull
@@ -68,7 +83,7 @@
     private final int mSensorId;
     private final int mUserId;
     @NonNull
-    private final LockoutTracker mLockoutCache;
+    private final LockoutTracker mLockoutTracker;
     @NonNull
     private final LockoutResetDispatcher mLockoutResetDispatcher;
 
@@ -76,6 +91,8 @@
     private final AuthSessionCoordinator mAuthSessionCoordinator;
     @NonNull
     private final HardwareUnavailableCallback mHardwareUnavailableCallback;
+    @NonNull
+    private final AidlResponseHandlerCallback mAidlResponseHandlerCallback;
 
     public AidlResponseHandler(@NonNull Context context,
             @NonNull BiometricScheduler scheduler, int sensorId, int userId,
@@ -83,14 +100,33 @@
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull AuthSessionCoordinator authSessionCoordinator,
             @NonNull HardwareUnavailableCallback hardwareUnavailableCallback) {
+        this(context, scheduler, sensorId, userId, lockoutTracker, lockoutResetDispatcher,
+                authSessionCoordinator, hardwareUnavailableCallback,
+                new AidlResponseHandlerCallback() {
+                    @Override
+                    public void onEnrollSuccess() {}
+
+                    @Override
+                    public void onHardwareUnavailable() {}
+                });
+    }
+
+    public AidlResponseHandler(@NonNull Context context,
+            @NonNull BiometricScheduler scheduler, int sensorId, int userId,
+            @NonNull LockoutTracker lockoutTracker,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull AuthSessionCoordinator authSessionCoordinator,
+            @NonNull HardwareUnavailableCallback hardwareUnavailableCallback,
+            @NonNull AidlResponseHandlerCallback aidlResponseHandlerCallback) {
         mContext = context;
         mScheduler = scheduler;
         mSensorId = sensorId;
         mUserId = userId;
-        mLockoutCache = lockoutTracker;
+        mLockoutTracker = lockoutTracker;
         mLockoutResetDispatcher = lockoutResetDispatcher;
         mAuthSessionCoordinator = authSessionCoordinator;
         mHardwareUnavailableCallback = hardwareUnavailableCallback;
+        mAidlResponseHandlerCallback = aidlResponseHandlerCallback;
     }
 
     @Override
@@ -106,13 +142,13 @@
     @Override
     public void onChallengeGenerated(long challenge) {
         handleResponse(FaceGenerateChallengeClient.class, (c) -> c.onChallengeGenerated(mSensorId,
-                mUserId, challenge), null);
+                mUserId, challenge));
     }
 
     @Override
     public void onChallengeRevoked(long challenge) {
         handleResponse(FaceRevokeChallengeClient.class, (c) -> c.onChallengeRevoked(mSensorId,
-                mUserId, challenge), null);
+                mUserId, challenge));
     }
 
     @Override
@@ -123,7 +159,7 @@
                 return;
             }
             c.onAuthenticationFrame(AidlConversionUtils.toFrameworkAuthenticationFrame(frame));
-        }, null);
+        });
     }
 
     @Override
@@ -134,7 +170,7 @@
                 return;
             }
             c.onEnrollmentFrame(AidlConversionUtils.toFrameworkEnrollmentFrame(frame));
-        }, null);
+        });
     }
 
     @Override
@@ -149,9 +185,13 @@
         handleResponse(ErrorConsumer.class, (c) -> {
             c.onError(error, vendorCode);
             if (error == Error.HW_UNAVAILABLE) {
-                mHardwareUnavailableCallback.onHardwareUnavailable();
+                if (Flags.deHidl()) {
+                    mAidlResponseHandlerCallback.onHardwareUnavailable();
+                } else {
+                    mHardwareUnavailableCallback.onHardwareUnavailable();
+                }
             }
-        }, null);
+        });
     }
 
     @Override
@@ -167,7 +207,12 @@
                 .getUniqueName(mContext, currentUserId);
         final Face face = new Face(name, enrollmentId, mSensorId);
 
-        handleResponse(FaceEnrollClient.class, (c) -> c.onEnrollResult(face, remaining), null);
+        handleResponse(FaceEnrollClient.class, (c) -> {
+            c.onEnrollResult(face, remaining);
+            if (remaining == 0) {
+                mAidlResponseHandlerCallback.onEnrollSuccess();
+            }
+        });
     }
 
     @Override
@@ -179,37 +224,37 @@
             byteList.add(b);
         }
         handleResponse(AuthenticationConsumer.class, (c) -> c.onAuthenticated(face,
-                true /* authenticated */, byteList), null);
+                true /* authenticated */, byteList));
     }
 
     @Override
     public void onAuthenticationFailed() {
         final Face face = new Face("" /* name */, 0 /* faceId */, mSensorId);
         handleResponse(AuthenticationConsumer.class, (c) -> c.onAuthenticated(face,
-                false /* authenticated */, null /* hat */), null);
+                false /* authenticated */, null /* hat */));
     }
 
     @Override
     public void onLockoutTimed(long durationMillis) {
-        handleResponse(LockoutConsumer.class, (c) -> c.onLockoutTimed(durationMillis), null);
+        handleResponse(LockoutConsumer.class, (c) -> c.onLockoutTimed(durationMillis));
     }
 
     @Override
     public void onLockoutPermanent() {
-        handleResponse(LockoutConsumer.class, LockoutConsumer::onLockoutPermanent, null);
+        handleResponse(LockoutConsumer.class, LockoutConsumer::onLockoutPermanent);
     }
 
     @Override
     public void onLockoutCleared() {
         handleResponse(FaceResetLockoutClient.class, FaceResetLockoutClient::onLockoutCleared,
                 (c) -> FaceResetLockoutClient.resetLocalLockoutStateToNone(mSensorId, mUserId,
-                mLockoutCache, mLockoutResetDispatcher, mAuthSessionCoordinator,
-                Utils.getCurrentStrength(mSensorId), -1 /* requestId */));
+                        mLockoutTracker, mLockoutResetDispatcher, mAuthSessionCoordinator,
+                        Utils.getCurrentStrength(mSensorId), -1 /* requestId */));
     }
 
     @Override
     public void onInteractionDetected() {
-        handleResponse(FaceDetectClient.class, FaceDetectClient::onInteractionDetected, null);
+        handleResponse(FaceDetectClient.class, FaceDetectClient::onInteractionDetected);
     }
 
     @Override
@@ -219,23 +264,23 @@
                 final Face face = new Face("" /* name */, enrollmentIds[i], mSensorId);
                 final int finalI = i;
                 handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(face,
-                        enrollmentIds.length - finalI - 1), null);
+                        enrollmentIds.length - finalI - 1 /* remaining */));
             }
         } else {
             handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(
-                    null /* identifier */, 0 /* remaining */), null);
+                    null /* identifier */, 0 /* remaining */));
         }
     }
 
     @Override
     public void onFeaturesRetrieved(byte[] features) {
         handleResponse(FaceGetFeatureClient.class, (c) -> c.onFeatureGet(true /* success */,
-                features), null);
+                features));
     }
 
     @Override
     public void onFeatureSet(byte feature) {
-        handleResponse(FaceSetFeatureClient.class, (c) -> c.onFeatureSet(true /* success */), null);
+        handleResponse(FaceSetFeatureClient.class, (c) -> c.onFeatureSet(true /* success */));
     }
 
     @Override
@@ -245,33 +290,32 @@
                 final Face face = new Face("" /* name */, enrollmentIds[i], mSensorId);
                 final int finalI = i;
                 handleResponse(RemovalConsumer.class,
-                        (c) -> c.onRemoved(face, enrollmentIds.length - finalI - 1),
-                        null);
+                        (c) -> c.onRemoved(face,
+                                enrollmentIds.length - finalI - 1 /* remaining */));
             }
         } else {
             handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(null /* identifier */,
-                    0 /* remaining */), null);
+                    0 /* remaining */));
         }
     }
 
     @Override
     public void onAuthenticatorIdRetrieved(long authenticatorId) {
         handleResponse(FaceGetAuthenticatorIdClient.class, (c) -> c.onAuthenticatorIdRetrieved(
-                authenticatorId), null);
+                authenticatorId));
     }
 
     @Override
     public void onAuthenticatorIdInvalidated(long newAuthenticatorId) {
         handleResponse(FaceInvalidationClient.class, (c) -> c.onAuthenticatorIdInvalidated(
-                newAuthenticatorId), null);
+                newAuthenticatorId));
     }
 
     /**
      * Handles acquired messages sent by the HAL (specifically for HIDL HAL).
      */
     public void onAcquired(int acquiredInfo, int vendorCode) {
-        handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(acquiredInfo, vendorCode),
-                null);
+        handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(acquiredInfo, vendorCode));
     }
 
     /**
@@ -288,7 +332,7 @@
                 lockoutMode = LockoutTracker.LOCKOUT_TIMED;
             }
 
-            mLockoutCache.setLockoutModeForUser(mUserId, lockoutMode);
+            mLockoutTracker.setLockoutModeForUser(mUserId, lockoutMode);
 
             if (duration == 0) {
                 mLockoutResetDispatcher.notifyLockoutResetCallbacks(mSensorId);
@@ -296,6 +340,20 @@
         });
     }
 
+    /**
+     * Handle clients which are not supported in HIDL HAL. For face, FaceInvalidationClient
+     * is the only AIDL client which is not supported in HIDL.
+     */
+    public void onUnsupportedClientScheduled() {
+        Slog.e(TAG, "FaceInvalidationClient is not supported in the HAL.");
+        handleResponse(FaceInvalidationClient.class, BaseClientMonitor::cancel);
+    }
+
+    private <T> void handleResponse(@NonNull Class<T> className,
+            @NonNull Consumer<T> action) {
+        handleResponse(className, action, null /* alternateAction */);
+    }
+
     private <T> void handleResponse(@NonNull Class<T> className,
             @NonNull Consumer<T> actionIfClassMatchesClient,
             @Nullable Consumer<BaseClientMonitor> alternateAction) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java
index e70e25a..af46f44 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java
@@ -21,7 +21,7 @@
 import android.hardware.biometrics.face.ISession;
 import android.hardware.biometrics.face.V1_0.IBiometricsFace;
 
-import com.android.server.biometrics.sensors.face.hidl.AidlToHidlAdapter;
+import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter;
 
 import java.util.function.Supplier;
 
@@ -47,7 +47,7 @@
 
     public AidlSession(Context context, Supplier<IBiometricsFace> session, int userId,
             AidlResponseHandler aidlResponseHandler) {
-        mSession = new AidlToHidlAdapter(context, session, userId, aidlResponseHandler);
+        mSession = new HidlToAidlSessionAdapter(context, session, userId, aidlResponseHandler);
         mHalInterfaceVersion = 0;
         mUserId = userId;
         mAidlResponseHandler = aidlResponseHandler;
@@ -64,7 +64,7 @@
     }
 
     /** The HAL callback, which should only be used in tests {@See BiometricTestSessionImpl}. */
-    AidlResponseHandler getHalSessionCallback() {
+    public AidlResponseHandler getHalSessionCallback() {
         return mAidlResponseHandler;
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java
index c15049b..c41b706 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java
@@ -34,7 +34,7 @@
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.ErrorConsumer;
 import com.android.server.biometrics.sensors.HalClientMonitor;
-import com.android.server.biometrics.sensors.face.hidl.AidlToHidlAdapter;
+import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -75,8 +75,8 @@
     protected void startHalOperation() {
         try {
             ISession session = getFreshDaemon().getSession();
-            if (session instanceof AidlToHidlAdapter) {
-                ((AidlToHidlAdapter) session).setFeature(mFeature);
+            if (session instanceof HidlToAidlSessionAdapter) {
+                ((HidlToAidlSessionAdapter) session).setFeature(mFeature);
             }
             session.getFeatures();
         } catch (RemoteException e) {
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 dd9c6d5..9fa15b8 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
@@ -24,6 +24,7 @@
 import android.app.TaskStackListener;
 import android.content.Context;
 import android.content.pm.UserInfo;
+import android.hardware.biometrics.BiometricFaceConstants;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.IInvalidationCallback;
@@ -51,6 +52,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver;
 import com.android.server.biometrics.AuthenticationStatsCollector;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
@@ -64,11 +66,13 @@
 import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
 import com.android.server.biometrics.sensors.InvalidationRequesterClient;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.PerformanceTracker;
 import com.android.server.biometrics.sensors.SensorList;
 import com.android.server.biometrics.sensors.face.FaceUtils;
 import com.android.server.biometrics.sensors.face.ServiceProvider;
 import com.android.server.biometrics.sensors.face.UsageStats;
+import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSensorAdapter;
 
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -152,9 +156,11 @@
             @NonNull SensorProps[] props,
             @NonNull String halInstanceName,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-            @NonNull BiometricContext biometricContext) {
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresChallenge) {
         this(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
-                biometricContext, null /* daemon */);
+                biometricContext, null /* daemon */, resetLockoutRequiresChallenge,
+                false /* testHalEnabled */);
     }
 
     @VisibleForTesting FaceProvider(@NonNull Context context,
@@ -163,7 +169,8 @@
             @NonNull String halInstanceName,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull BiometricContext biometricContext,
-            IFace daemon) {
+            @Nullable IFace daemon, boolean resetLockoutRequiresChallenge,
+            boolean testHalEnabled) {
         mContext = context;
         mBiometricStateCallback = biometricStateCallback;
         mHalInstanceName = halInstanceName;
@@ -176,48 +183,118 @@
         mBiometricContext = biometricContext;
         mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator();
         mDaemon = daemon;
+        mTestHalEnabled = testHalEnabled;
 
-        AuthenticationStatsBroadcastReceiver mBroadcastReceiver =
-                new AuthenticationStatsBroadcastReceiver(
-                        mContext,
-                        BiometricsProtoEnums.MODALITY_FACE,
-                        (AuthenticationStatsCollector collector) -> {
-                            Slog.d(getTag(), "Initializing AuthenticationStatsCollector");
-                            mAuthenticationStatsCollector = collector;
-                        });
+        initAuthenticationBroadcastReceiver();
+        initSensors(resetLockoutRequiresChallenge, props);
+    }
 
-        for (SensorProps prop : props) {
-            final int sensorId = prop.commonProps.sensorId;
+    private void initAuthenticationBroadcastReceiver() {
+        new AuthenticationStatsBroadcastReceiver(
+                mContext,
+                BiometricsProtoEnums.MODALITY_FACE,
+                (AuthenticationStatsCollector collector) -> {
+                    Slog.d(getTag(), "Initializing AuthenticationStatsCollector");
+                    mAuthenticationStatsCollector = collector;
+                });
+    }
 
-            final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
-            if (prop.commonProps.componentInfo != null) {
-                for (ComponentInfo info : prop.commonProps.componentInfo) {
-                    componentInfo.add(new ComponentInfoInternal(info.componentId,
-                            info.hardwareVersion, info.firmwareVersion, info.serialNumber,
-                            info.softwareVersion));
+    private void initSensors(boolean resetLockoutRequiresChallenge, SensorProps[] props) {
+        if (Flags.deHidl()) {
+            if (resetLockoutRequiresChallenge) {
+                Slog.d(getTag(), "Adding HIDL configs");
+                for (SensorProps prop : props) {
+                    addHidlSensors(prop, resetLockoutRequiresChallenge);
+                }
+            } else {
+                Slog.d(getTag(), "Adding AIDL configs");
+                for (SensorProps prop : props) {
+                    addAidlSensors(prop, resetLockoutRequiresChallenge);
                 }
             }
+        } else {
+            for (SensorProps prop : props) {
+                final int sensorId = prop.commonProps.sensorId;
 
-            final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal(
-                    prop.commonProps.sensorId, prop.commonProps.sensorStrength,
-                    prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType,
-                    prop.supportsDetectInteraction, prop.halControlsPreview,
-                    false /* resetLockoutRequiresChallenge */);
-            final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, mHandler,
-                    internalProp, lockoutResetDispatcher, mBiometricContext);
-            final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
-                    sensor.getLazySession().get().getUserId();
-            mFaceSensors.addSensor(sensorId, sensor, userId,
-                    new SynchronousUserSwitchObserver() {
-                        @Override
-                        public void onUserSwitching(int newUserId) {
-                            scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
-                        }
-                    });
-            Slog.d(getTag(), "Added: " + internalProp);
+                final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
+                if (prop.commonProps.componentInfo != null) {
+                    for (ComponentInfo info : prop.commonProps.componentInfo) {
+                        componentInfo.add(new ComponentInfoInternal(info.componentId,
+                                info.hardwareVersion, info.firmwareVersion, info.serialNumber,
+                                info.softwareVersion));
+                    }
+                }
+
+                final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal(
+                        prop.commonProps.sensorId, prop.commonProps.sensorStrength,
+                        prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType,
+                        prop.supportsDetectInteraction, prop.halControlsPreview,
+                        false /* resetLockoutRequiresChallenge */);
+                final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this,
+                        mContext, mHandler, internalProp, mLockoutResetDispatcher,
+                        mBiometricContext);
+                sensor.init(mLockoutResetDispatcher, this);
+                final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
+                        sensor.getLazySession().get().getUserId();
+                mFaceSensors.addSensor(sensorId, sensor, userId,
+                        new SynchronousUserSwitchObserver() {
+                            @Override
+                            public void onUserSwitching(int newUserId) {
+                                scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
+                            }
+                        });
+                Slog.d(getTag(), "Added: " + internalProp);
+            }
         }
     }
 
+    private void addHidlSensors(SensorProps prop, boolean resetLockoutRequiresChallenge) {
+        final int sensorId = prop.commonProps.sensorId;
+        final Sensor sensor = new HidlToAidlSensorAdapter(getTag() + "/" + sensorId, this,
+                mContext, mHandler, prop, mLockoutResetDispatcher,
+                mBiometricContext, resetLockoutRequiresChallenge,
+                () -> {
+                    //TODO: update to make this testable
+                    scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser(),
+                            null /* callback */);
+                    scheduleGetFeature(sensorId, new Binder(), ActivityManager.getCurrentUser(),
+                            BiometricFaceConstants.FEATURE_REQUIRE_ATTENTION, null,
+                            mContext.getOpPackageName());
+                });
+        sensor.init(mLockoutResetDispatcher, this);
+        final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
+                sensor.getLazySession().get().getUserId();
+        mFaceSensors.addSensor(sensorId, sensor, userId,
+                new SynchronousUserSwitchObserver() {
+                    @Override
+                    public void onUserSwitching(int newUserId) {
+                        scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
+                        scheduleGetFeature(sensorId, new Binder(), newUserId,
+                                BiometricFaceConstants.FEATURE_REQUIRE_ATTENTION,
+                                null, mContext.getOpPackageName());
+                    }
+                });
+        Slog.d(getTag(), "Added: " + mFaceSensors.get(sensorId));
+    }
+
+    private void addAidlSensors(SensorProps prop, boolean resetLockoutRequiresChallenge) {
+        final int sensorId = prop.commonProps.sensorId;
+        final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext,
+                mHandler, prop, mLockoutResetDispatcher, mBiometricContext,
+                resetLockoutRequiresChallenge);
+        sensor.init(mLockoutResetDispatcher, this);
+        final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
+                sensor.getLazySession().get().getUserId();
+        mFaceSensors.addSensor(sensorId, sensor, userId,
+                new SynchronousUserSwitchObserver() {
+                    @Override
+                    public void onUserSwitching(int newUserId) {
+                        scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
+                    }
+                });
+        Slog.d(getTag(), "Added: " + mFaceSensors.get(sensorId));
+    }
+
     private String getTag() {
         return "FaceProvider/" + mHalInstanceName;
     }
@@ -290,7 +367,10 @@
         }
     }
 
-    private void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) {
+    /**
+     * Schedules FaceGetAuthenticatorIdClient for specific sensor and user.
+     */
+    protected void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) {
         mHandler.post(() -> {
             final FaceGetAuthenticatorIdClient client = new FaceGetAuthenticatorIdClient(
                     mContext, mFaceSensors.get(sensorId).getLazySession(), userId,
@@ -365,8 +445,12 @@
 
     @Override
     public int getLockoutModeForUser(int sensorId, int userId) {
-        return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
-                Utils.getCurrentStrength(sensorId));
+        if (Flags.deHidl()) {
+            return mFaceSensors.get(sensorId).getLockoutModeForUser(userId);
+        } else {
+            return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
+                    Utils.getCurrentStrength(sensorId));
+        }
     }
 
     @Override
@@ -376,13 +460,18 @@
 
     @Override
     public boolean isHardwareDetected(int sensorId) {
-        return hasHalInstance();
+        if (Flags.deHidl()) {
+            return mFaceSensors.get(sensorId).isHardwareDetected(mHalInstanceName);
+        } else {
+            return hasHalInstance();
+        }
     }
 
     @Override
     public void scheduleGenerateChallenge(int sensorId, int userId, @NonNull IBinder token,
             @NonNull IFaceServiceReceiver receiver, String opPackageName) {
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final FaceGenerateChallengeClient client = new FaceGenerateChallengeClient(mContext,
                     mFaceSensors.get(sensorId).getLazySession(), token,
                     new ClientMonitorCallbackConverter(receiver), userId, opPackageName, sensorId,
@@ -416,6 +505,7 @@
             @Nullable Surface previewSurface, boolean debugConsent) {
         final long id = mRequestCounter.incrementAndGet();
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final int maxTemplatesPerUser = mFaceSensors.get(
                     sensorId).getSensorProperties().maxEnrollmentsPerUser;
             final FaceEnrollClient client = new FaceEnrollClient(mContext,
@@ -427,18 +517,23 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN,
                             mAuthenticationStatsCollector),
                     mBiometricContext, maxTemplatesPerUser, debugConsent);
-            scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
-                            mBiometricStateCallback, new ClientMonitorCallback() {
-                        @Override
-                        public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
-                                boolean success) {
-                            ClientMonitorCallback.super.onClientFinished(clientMonitor, success);
-                            if (success) {
-                                scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
-                                scheduleInvalidationRequest(sensorId, userId);
+            if (Flags.deHidl()) {
+                scheduleForSensor(sensorId, client, mBiometricStateCallback);
+            } else {
+                scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
+                        mBiometricStateCallback, new ClientMonitorCallback() {
+                            @Override
+                            public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
+                                    boolean success) {
+                                ClientMonitorCallback.super.onClientFinished(clientMonitor,
+                                        success);
+                                if (success) {
+                                    scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
+                                    scheduleInvalidationRequest(sensorId, userId);
+                                }
                             }
-                        }
-                    }));
+                        }));
+            }
         });
         return id;
     }
@@ -486,6 +581,13 @@
             final int userId = options.getUserId();
             final int sensorId = options.getSensorId();
             final boolean isStrongBiometric = Utils.isStrongBiometric(sensorId);
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
+            final LockoutTracker lockoutTracker;
+            if (Flags.deHidl()) {
+                lockoutTracker = mFaceSensors.get(sensorId).getLockoutTracker(true /* forAuth */);
+            } else {
+                lockoutTracker = null;
+            }
             final FaceAuthenticationClient client = new FaceAuthenticationClient(
                     mContext, mFaceSensors.get(sensorId).getLazySession(), token, requestId,
                     callback, operationId, restricted, options, cookie,
@@ -493,7 +595,7 @@
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient,
                             mAuthenticationStatsCollector),
                     mBiometricContext, isStrongBiometric,
-                    mUsageStats, null /* lockoutTracker */,
+                    mUsageStats, lockoutTracker,
                     allowBackgroundAuthentication, Utils.getCurrentStrength(sensorId));
             scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
                 @Override
@@ -555,6 +657,7 @@
     private void scheduleRemoveSpecifiedIds(int sensorId, @NonNull IBinder token, int[] faceIds,
             int userId, @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) {
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final FaceRemovalClient client = new FaceRemovalClient(mContext,
                     mFaceSensors.get(sensorId).getLazySession(), token,
                     new ClientMonitorCallbackConverter(receiver), faceIds, userId,
@@ -571,6 +674,7 @@
     @Override
     public void scheduleResetLockout(int sensorId, int userId, @NonNull byte[] hardwareAuthToken) {
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final FaceResetLockoutClient client = new FaceResetLockoutClient(
                     mContext, mFaceSensors.get(sensorId).getLazySession(), userId,
                     mContext.getOpPackageName(), sensorId,
@@ -578,8 +682,8 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN,
                             mAuthenticationStatsCollector),
                     mBiometricContext, hardwareAuthToken,
-                    mFaceSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher,
-                    Utils.getCurrentStrength(sensorId));
+                    mFaceSensors.get(sensorId).getLockoutTracker(false/* forAuth */),
+                    mLockoutResetDispatcher, Utils.getCurrentStrength(sensorId));
 
             scheduleForSensor(sensorId, client);
         });
@@ -590,6 +694,7 @@
             boolean enabled, @NonNull byte[] hardwareAuthToken,
             @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) {
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final List<Face> faces = FaceUtils.getInstance(sensorId)
                     .getBiometricsForUser(mContext, userId);
             if (faces.isEmpty()) {
@@ -610,6 +715,7 @@
     public void scheduleGetFeature(int sensorId, @NonNull IBinder token, int userId, int feature,
             @NonNull ClientMonitorCallbackConverter callback, @NonNull String opPackageName) {
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final List<Face> faces = FaceUtils.getInstance(sensorId)
                     .getBiometricsForUser(mContext, userId);
             if (faces.isEmpty()) {
@@ -641,6 +747,7 @@
     public void scheduleInternalCleanup(int sensorId, int userId,
             @Nullable ClientMonitorCallback callback, boolean favorHalEnrollments) {
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final FaceInternalCleanupClient client =
                     new FaceInternalCleanupClient(mContext,
                             mFaceSensors.get(sensorId).getLazySession(), userId,
@@ -760,4 +867,8 @@
         }
         biometricScheduler.startWatchdog();
     }
+
+    public boolean getTestHalEnabled() {
+        return mTestHalEnabled;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java
index 77b5592..d02eefa 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.hardware.biometrics.BiometricManager.Authenticators;
 import android.hardware.biometrics.face.IFace;
+import android.hardware.biometrics.face.ISession;
 import android.hardware.keymaster.HardwareAuthToken;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -34,6 +35,7 @@
 import com.android.server.biometrics.sensors.HalClientMonitor;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter;
 
 import java.util.function.Supplier;
 
@@ -47,7 +49,7 @@
     private static final String TAG = "FaceResetLockoutClient";
 
     private final HardwareAuthToken mHardwareAuthToken;
-    private final LockoutTracker mLockoutCache;
+    private final LockoutTracker mLockoutTracker;
     private final LockoutResetDispatcher mLockoutResetDispatcher;
     private final int mBiometricStrength;
 
@@ -60,7 +62,7 @@
         super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner,
                 0 /* cookie */, sensorId, logger, biometricContext);
         mHardwareAuthToken = HardwareAuthTokenUtils.toHardwareAuthToken(hardwareAuthToken);
-        mLockoutCache = lockoutTracker;
+        mLockoutTracker = lockoutTracker;
         mLockoutResetDispatcher = lockoutResetDispatcher;
         mBiometricStrength = biometricStrength;
     }
@@ -79,7 +81,11 @@
     @Override
     protected void startHalOperation() {
         try {
-            getFreshDaemon().getSession().resetLockout(mHardwareAuthToken);
+            final ISession session = getFreshDaemon().getSession();
+            session.resetLockout(mHardwareAuthToken);
+            if (session instanceof HidlToAidlSessionAdapter) {
+                mCallback.onClientFinished(this, true /* success */);
+            }
         } catch (RemoteException e) {
             Slog.e(TAG, "Unable to reset lockout", e);
             mCallback.onClientFinished(this, false /* success */);
@@ -87,7 +93,7 @@
     }
 
     void onLockoutCleared() {
-        resetLocalLockoutStateToNone(getSensorId(), getTargetUserId(), mLockoutCache,
+        resetLocalLockoutStateToNone(getSensorId(), getTargetUserId(), mLockoutTracker,
                 mLockoutResetDispatcher, getBiometricContext().getAuthSessionCoordinator(),
                 mBiometricStrength, getRequestId());
         mCallback.onClientFinished(this, true /* success */);
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
index 54e66eb..3e5c599 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
@@ -21,16 +21,20 @@
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.hardware.biometrics.BiometricsProtoEnums;
+import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.ITestSession;
 import android.hardware.biometrics.ITestSessionCallback;
+import android.hardware.biometrics.common.ComponentInfo;
 import android.hardware.biometrics.face.IFace;
 import android.hardware.biometrics.face.ISession;
+import android.hardware.biometrics.face.SensorProps;
 import android.hardware.face.FaceManager;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Slog;
@@ -38,6 +42,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.SensorServiceStateProto;
 import com.android.server.biometrics.SensorStateProto;
 import com.android.server.biometrics.UserStateProto;
@@ -49,12 +54,15 @@
 import com.android.server.biometrics.sensors.ErrorConsumer;
 import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.StartUserClient;
 import com.android.server.biometrics.sensors.StopUserClient;
 import com.android.server.biometrics.sensors.UserAwareBiometricScheduler;
 import com.android.server.biometrics.sensors.face.FaceUtils;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.function.Supplier;
 
@@ -71,25 +79,53 @@
     @NonNull private final IBinder mToken;
     @NonNull private final Handler mHandler;
     @NonNull private final FaceSensorPropertiesInternal mSensorProperties;
-    @NonNull private final UserAwareBiometricScheduler mScheduler;
-    @NonNull private final LockoutCache mLockoutCache;
+    @NonNull private BiometricScheduler mScheduler;
+    @Nullable private LockoutTracker mLockoutTracker;
     @NonNull private final Map<Integer, Long> mAuthenticatorIds;
 
-    @NonNull private final Supplier<AidlSession> mLazySession;
+    @NonNull private Supplier<AidlSession> mLazySession;
     @Nullable AidlSession mCurrentSession;
+    @NonNull BiometricContext mBiometricContext;
 
 
     Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
             @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-            @NonNull BiometricContext biometricContext, AidlSession session) {
+            @NonNull BiometricContext biometricContext, @Nullable AidlSession session) {
         mTag = tag;
         mProvider = provider;
         mContext = context;
         mToken = new Binder();
         mHandler = handler;
         mSensorProperties = sensorProperties;
-        mScheduler = new UserAwareBiometricScheduler(tag,
+        mBiometricContext = biometricContext;
+        mAuthenticatorIds = new HashMap<>();
+    }
+
+    Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
+            @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull BiometricContext biometricContext) {
+        this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher,
+                biometricContext, null);
+    }
+
+    public Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
+            @NonNull Handler handler, @NonNull SensorProps prop,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresChallenge) {
+        this(tag, provider, context, handler,
+                getFaceSensorPropertiesInternal(prop, resetLockoutRequiresChallenge),
+                lockoutResetDispatcher, biometricContext, null);
+    }
+
+    /**
+     * Initialize biometric scheduler, lockout tracker and session for the sensor.
+     */
+    public void init(LockoutResetDispatcher lockoutResetDispatcher,
+            FaceProvider provider) {
+        mScheduler = new UserAwareBiometricScheduler(mTag,
                 BiometricScheduler.SENSOR_TYPE_FACE, null /* gestureAvailabilityDispatcher */,
                 () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL,
                 new UserAwareBiometricScheduler.UserSwitchCallback() {
@@ -98,7 +134,7 @@
                     public StopUserClient<?> getStopUserClient(int userId) {
                         return new FaceStopUserClient(mContext, mLazySession, mToken, userId,
                                 mSensorProperties.sensorId,
-                                BiometricLogger.ofUnknown(mContext), biometricContext,
+                                BiometricLogger.ofUnknown(mContext), mBiometricContext,
                                 () -> mCurrentSession = null);
                     }
 
@@ -107,13 +143,36 @@
                     public StartUserClient<?, ?> getStartUserClient(int newUserId) {
                         final int sensorId = mSensorProperties.sensorId;
 
-                        final AidlResponseHandler resultController = new AidlResponseHandler(
-                                mContext, mScheduler, sensorId, newUserId,
-                                mLockoutCache, lockoutResetDispatcher,
-                                biometricContext.getAuthSessionCoordinator(), () -> {
-                            Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE");
-                            mCurrentSession = null;
-                        });
+                        final AidlResponseHandler resultController;
+                        if (Flags.deHidl()) {
+                            resultController = new AidlResponseHandler(
+                                    mContext, mScheduler, sensorId, newUserId,
+                                    mLockoutTracker, lockoutResetDispatcher,
+                                    mBiometricContext.getAuthSessionCoordinator(), () -> {},
+                                    new AidlResponseHandler.AidlResponseHandlerCallback() {
+                                        @Override
+                                        public void onEnrollSuccess() {
+                                            mProvider.scheduleLoadAuthenticatorIdsForUser(sensorId,
+                                                    newUserId);
+                                            mProvider.scheduleInvalidationRequest(sensorId,
+                                                    newUserId);
+                                        }
+
+                                        @Override
+                                        public void onHardwareUnavailable() {
+                                            Slog.e(mTag, "Face sensor hardware unavailable.");
+                                            mCurrentSession = null;
+                                        }
+                                    });
+                        } else {
+                            resultController = new AidlResponseHandler(
+                                    mContext, mScheduler, sensorId, newUserId,
+                                    mLockoutTracker, lockoutResetDispatcher,
+                                    mBiometricContext.getAuthSessionCoordinator(), () -> {
+                                Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE");
+                                mCurrentSession = null;
+                            });
+                        }
 
                         final StartUserClient.UserStartedCallback<ISession> userStartedCallback =
                                 (userIdStarted, newSession, halInterfaceVersion) -> {
@@ -136,32 +195,42 @@
 
                         return new FaceStartUserClient(mContext, provider::getHalInstance,
                                 mToken, newUserId, mSensorProperties.sensorId,
-                                BiometricLogger.ofUnknown(mContext), biometricContext,
+                                BiometricLogger.ofUnknown(mContext), mBiometricContext,
                                 resultController, userStartedCallback);
                     }
                 });
-        mLockoutCache = new LockoutCache();
-        mAuthenticatorIds = new HashMap<>();
         mLazySession = () -> mCurrentSession != null ? mCurrentSession : null;
+        mLockoutTracker = new LockoutCache();
     }
 
-    Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
-            @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties,
-            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-            @NonNull BiometricContext biometricContext) {
-        this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher,
-                biometricContext, null);
+    private static FaceSensorPropertiesInternal getFaceSensorPropertiesInternal(SensorProps prop,
+            boolean resetLockoutRequiresChallenge) {
+        final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
+        if (prop.commonProps.componentInfo != null) {
+            for (ComponentInfo info : prop.commonProps.componentInfo) {
+                componentInfo.add(new ComponentInfoInternal(info.componentId,
+                        info.hardwareVersion, info.firmwareVersion, info.serialNumber,
+                        info.softwareVersion));
+            }
+        }
+        final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal(
+                prop.commonProps.sensorId, prop.commonProps.sensorStrength,
+                prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType,
+                prop.supportsDetectInteraction, prop.halControlsPreview,
+                resetLockoutRequiresChallenge);
+
+        return internalProp;
     }
 
-    @NonNull Supplier<AidlSession> getLazySession() {
+    @NonNull public Supplier<AidlSession> getLazySession() {
         return mLazySession;
     }
 
-    @NonNull FaceSensorPropertiesInternal getSensorProperties() {
+    @NonNull protected FaceSensorPropertiesInternal getSensorProperties() {
         return mSensorProperties;
     }
 
-    @VisibleForTesting @Nullable AidlSession getSessionForUser(int userId) {
+    @VisibleForTesting @Nullable protected AidlSession getSessionForUser(int userId) {
         if (mCurrentSession != null && mCurrentSession.getUserId() == userId) {
             return mCurrentSession;
         } else {
@@ -174,15 +243,18 @@
                 mProvider, this);
     }
 
-    @NonNull BiometricScheduler getScheduler() {
+    @NonNull public BiometricScheduler getScheduler() {
         return mScheduler;
     }
 
-    @NonNull LockoutCache getLockoutCache() {
-        return mLockoutCache;
+    @NonNull protected LockoutTracker getLockoutTracker(boolean forAuth) {
+        if (forAuth) {
+            return null;
+        }
+        return mLockoutTracker;
     }
 
-    @NonNull Map<Integer, Long> getAuthenticatorIds() {
+    @NonNull protected Map<Integer, Long> getAuthenticatorIds() {
         return mAuthenticatorIds;
     }
 
@@ -253,4 +325,49 @@
         mScheduler.reset();
         mCurrentSession = null;
     }
+
+    protected BiometricContext getBiometricContext() {
+        return mBiometricContext;
+    }
+
+    protected Handler getHandler() {
+        return mHandler;
+    }
+
+    protected Context getContext() {
+        return mContext;
+    }
+
+    /**
+     * Schedules FaceUpdateActiveUserClient for user id.
+     */
+    public void scheduleFaceUpdateActiveUserClient(int userId) {}
+
+    /**
+     * Returns true if the sensor hardware is detected.
+     */
+    public boolean isHardwareDetected(String halInstanceName) {
+        if (mTestHalEnabled) {
+            return true;
+        }
+        return ServiceManager.checkService(IFace.DESCRIPTOR + "/" + halInstanceName) != null;
+    }
+
+    /**
+     * Returns lockout mode of this sensor.
+     */
+    @LockoutTracker.LockoutMode
+    public int getLockoutModeForUser(int userId) {
+        return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
+                Utils.getCurrentStrength(mSensorProperties.sensorId));
+    }
+
+    public void setScheduler(BiometricScheduler scheduler) {
+        mScheduler = scheduler;
+    }
+
+    public void setLazySession(
+            Supplier<AidlSession> lazySession) {
+        mLazySession = lazySession;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceUpdateActiveUserClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceUpdateActiveUserClient.java
index 8385c3f..0c34d70 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceUpdateActiveUserClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceUpdateActiveUserClient.java
@@ -27,13 +27,14 @@
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
-import com.android.server.biometrics.sensors.HalClientMonitor;
+import com.android.server.biometrics.sensors.StartUserClient;
+import com.android.server.biometrics.sensors.face.aidl.AidlSession;
 
 import java.io.File;
 import java.util.Map;
 import java.util.function.Supplier;
 
-public class FaceUpdateActiveUserClient extends HalClientMonitor<IBiometricsFace> {
+public class FaceUpdateActiveUserClient extends StartUserClient<IBiometricsFace, AidlSession> {
     private static final String TAG = "FaceUpdateActiveUserClient";
     private static final String FACE_DATA_DIR = "facedata";
 
@@ -45,8 +46,18 @@
             int sensorId, @NonNull BiometricLogger logger,
             @NonNull BiometricContext biometricContext, boolean hasEnrolledBiometrics,
             @NonNull Map<Integer, Long> authenticatorIds) {
-        super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner,
-                0 /* cookie */, sensorId, logger, biometricContext);
+        this(context, lazyDaemon, (newUserId, newUser, halInterfaceVersion) -> {},
+                userId, owner, sensorId, logger, biometricContext, hasEnrolledBiometrics,
+                authenticatorIds);
+    }
+
+    FaceUpdateActiveUserClient(@NonNull Context context,
+            @NonNull Supplier<IBiometricsFace> lazyDaemon, UserStartedCallback userStartedCallback,
+            int userId, @NonNull String owner, int sensorId, @NonNull BiometricLogger logger,
+            @NonNull BiometricContext biometricContext, boolean hasEnrolledBiometrics,
+            @NonNull Map<Integer, Long> authenticatorIds) {
+        super(context, lazyDaemon, null /* token */, userId, sensorId, logger, biometricContext,
+                userStartedCallback);
         mHasEnrolledBiometrics = hasEnrolledBiometrics;
         mAuthenticatorIds = authenticatorIds;
     }
@@ -77,6 +88,7 @@
             daemon.setActiveUser(getTargetUserId(), storePath.getAbsolutePath());
             mAuthenticatorIds.put(getTargetUserId(),
                     mHasEnrolledBiometrics ? daemon.getAuthenticatorId().value : 0L);
+            mUserStartedCallback.onUserStarted(getTargetUserId(), null, 0);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
             Slog.e(TAG, "Failed to setActiveUser: " + e);
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlCallbackConverter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlCallbackConverter.java
index 36a9790..7a574ce 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlCallbackConverter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlCallbackConverter.java
@@ -112,4 +112,8 @@
     void onAuthenticatorIdRetrieved(long authenticatorId) {
         mAidlResponseHandler.onAuthenticatorIdRetrieved(authenticatorId);
     }
+
+    void onUnsupportedClientScheduled() {
+        mAidlResponseHandler.onUnsupportedClientScheduled();
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java
new file mode 100644
index 0000000..6355cb5
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.biometrics.sensors.face.hidl;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.hardware.biometrics.face.SensorProps;
+import android.hardware.biometrics.face.V1_0.IBiometricsFace;
+import android.os.Handler;
+import android.os.IHwBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.StartUserClient;
+import com.android.server.biometrics.sensors.face.FaceUtils;
+import com.android.server.biometrics.sensors.face.LockoutHalImpl;
+import com.android.server.biometrics.sensors.face.aidl.AidlResponseHandler;
+import com.android.server.biometrics.sensors.face.aidl.AidlSession;
+import com.android.server.biometrics.sensors.face.aidl.FaceProvider;
+import com.android.server.biometrics.sensors.face.aidl.Sensor;
+
+/**
+ * Convert HIDL sensor configurations to an AIDL Sensor.
+ */
+public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRecipient{
+
+    private static final String TAG = "HidlToAidlSensorAdapter";
+
+    private IBiometricsFace mDaemon;
+    private AidlSession mSession;
+    private int mCurrentUserId = UserHandle.USER_NULL;
+    private final Runnable mInternalCleanupAndGetFeatureRunnable;
+    private final FaceProvider mFaceProvider;
+    private final LockoutResetDispatcher mLockoutResetDispatcher;
+    private final AuthSessionCoordinator mAuthSessionCoordinator;
+    private final AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback;
+    private final StartUserClient.UserStartedCallback<AidlSession> mUserStartedCallback =
+            (newUserId, newUser, halInterfaceVersion) -> {
+                if (newUserId != mCurrentUserId) {
+                    handleUserChanged(newUserId);
+                }
+            };
+    private LockoutHalImpl mLockoutTracker;
+
+    public HidlToAidlSensorAdapter(@NonNull String tag,
+            @NonNull FaceProvider provider,
+            @NonNull Context context,
+            @NonNull Handler handler,
+            @NonNull SensorProps prop,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresChallenge,
+            @NonNull Runnable internalCleanupAndGetFeatureRunnable) {
+        this(tag, provider, context, handler, prop, lockoutResetDispatcher, biometricContext,
+                resetLockoutRequiresChallenge, internalCleanupAndGetFeatureRunnable,
+                new AuthSessionCoordinator(), null /* daemon */,
+                null /* onEnrollSuccessCallback */);
+    }
+
+    @VisibleForTesting
+    HidlToAidlSensorAdapter(@NonNull String tag,
+            @NonNull FaceProvider provider,
+            @NonNull Context context,
+            @NonNull Handler handler,
+            @NonNull SensorProps prop,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresChallenge,
+            @NonNull Runnable internalCleanupAndGetFeatureRunnable,
+            @NonNull AuthSessionCoordinator authSessionCoordinator,
+            @Nullable IBiometricsFace daemon,
+            @Nullable AidlResponseHandler.AidlResponseHandlerCallback aidlResponseHandlerCallback) {
+        super(tag, provider, context, handler, prop, lockoutResetDispatcher, biometricContext,
+                resetLockoutRequiresChallenge);
+        mInternalCleanupAndGetFeatureRunnable = internalCleanupAndGetFeatureRunnable;
+        mFaceProvider = provider;
+        mLockoutResetDispatcher = lockoutResetDispatcher;
+        mAuthSessionCoordinator = authSessionCoordinator;
+        mDaemon = daemon;
+        mAidlResponseHandlerCallback = aidlResponseHandlerCallback == null
+                ? new AidlResponseHandler.AidlResponseHandlerCallback() {
+                    @Override
+                    public void onEnrollSuccess() {
+                        scheduleFaceUpdateActiveUserClient(mCurrentUserId);
+                    }
+
+                    @Override
+                    public void onHardwareUnavailable() {
+                        mDaemon = null;
+                        mCurrentUserId = UserHandle.USER_NULL;
+                    }
+                } : aidlResponseHandlerCallback;
+    }
+
+    @Override
+    public void scheduleFaceUpdateActiveUserClient(int userId) {
+        getScheduler().scheduleClientMonitor(getFaceUpdateActiveUserClient(userId));
+    }
+
+    @Override
+    public void serviceDied(long cookie) {
+        Slog.d(TAG, "HAL died.");
+        mDaemon = null;
+    }
+
+    @Override
+    public boolean isHardwareDetected(String halInstanceName) {
+        return getIBiometricsFace() != null;
+    }
+
+    @Override
+    @LockoutTracker.LockoutMode
+    public int getLockoutModeForUser(int userId) {
+        return mLockoutTracker.getLockoutModeForUser(userId);
+    }
+
+    @Override
+    public void init(LockoutResetDispatcher lockoutResetDispatcher,
+            FaceProvider provider) {
+        setScheduler(new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
+                null /* gestureAvailabilityTracker */));
+        setLazySession(this::getSession);
+        mLockoutTracker = new LockoutHalImpl();
+    }
+
+    @Override
+    @VisibleForTesting @Nullable protected AidlSession getSessionForUser(int userId) {
+        if (mSession != null && mSession.getUserId() == userId) {
+            return mSession;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    protected LockoutTracker getLockoutTracker(boolean forAuth) {
+        return mLockoutTracker;
+    }
+
+    @NonNull AidlSession getSession() {
+        if (mDaemon != null && mSession != null) {
+            return mSession;
+        } else {
+            return mSession = new AidlSession(getContext(), this::getIBiometricsFace,
+                    mCurrentUserId, getAidlResponseHandler());
+        }
+    }
+
+    private AidlResponseHandler getAidlResponseHandler() {
+        return new AidlResponseHandler(getContext(), getScheduler(), getSensorProperties().sensorId,
+                mCurrentUserId, mLockoutTracker, mLockoutResetDispatcher,
+                mAuthSessionCoordinator, () -> {}, mAidlResponseHandlerCallback);
+    }
+
+    private IBiometricsFace getIBiometricsFace() {
+        if (mFaceProvider.getTestHalEnabled()) {
+            final TestHal testHal = new TestHal(getContext(), getSensorProperties().sensorId);
+            testHal.setCallback(new HidlToAidlCallbackConverter(getAidlResponseHandler()));
+            return testHal;
+        }
+
+        if (mDaemon != null) {
+            return mDaemon;
+        }
+
+        Slog.d(TAG, "Daemon was null, reconnecting, current operation: "
+                + getScheduler().getCurrentClient());
+
+        try {
+            mDaemon = IBiometricsFace.getService();
+        } catch (java.util.NoSuchElementException e) {
+            // Service doesn't exist or cannot be opened.
+            Slog.w(TAG, "NoSuchElementException", e);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to get face HAL", e);
+        }
+
+        if (mDaemon == null) {
+            Slog.w(TAG, "Face HAL not available");
+            return null;
+        }
+
+        mDaemon.asBinder().linkToDeath(this, 0 /* flags */);
+
+        scheduleLoadAuthenticatorIds();
+        mInternalCleanupAndGetFeatureRunnable.run();
+        return mDaemon;
+    }
+
+    @VisibleForTesting void handleUserChanged(int newUserId) {
+        Slog.d(TAG, "User changed. Current user is " + newUserId);
+        mSession = null;
+        mCurrentUserId = newUserId;
+    }
+
+    private void scheduleLoadAuthenticatorIds() {
+        // Note that this can be performed on the scheduler (as opposed to being done immediately
+        // when the HAL is (re)loaded, since
+        // 1) If this is truly the first time it's being performed (e.g. system has just started),
+        //    this will be run very early and way before any applications need to generate keys.
+        // 2) If this is being performed to refresh the authenticatorIds (e.g. HAL crashed and has
+        //    just been reloaded), the framework already has a cache of the authenticatorIds. This
+        //    is safe because authenticatorIds only change when A) new template has been enrolled,
+        //    or B) all templates are removed.
+        getHandler().post(() -> {
+            for (UserInfo user : UserManager.get(getContext()).getAliveUsers()) {
+                final int targetUserId = user.id;
+                if (!getAuthenticatorIds().containsKey(targetUserId)) {
+                    scheduleFaceUpdateActiveUserClient(targetUserId);
+                }
+            }
+        });
+    }
+
+    private FaceUpdateActiveUserClient getFaceUpdateActiveUserClient(int userId) {
+        return new FaceUpdateActiveUserClient(getContext(), this::getIBiometricsFace,
+                mUserStartedCallback, userId, TAG, getSensorProperties().sensorId,
+                BiometricLogger.ofUnknown(getContext()), getBiometricContext(),
+                !FaceUtils.getInstance(getSensorProperties().sensorId).getBiometricsForUser(
+                        getContext(), userId).isEmpty(),
+                getAuthenticatorIds());
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java
similarity index 88%
rename from services/core/java/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapter.java
rename to services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java
index 489b213..5daf2d4 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java
@@ -47,34 +47,37 @@
 import java.util.function.Supplier;
 
 /**
- * Adapter to convert AIDL-specific interface {@link ISession} methods to HIDL implementation.
+ * Adapter to convert HIDL methods into AIDL interface {@link ISession}.
  */
-public class AidlToHidlAdapter implements ISession {
+public class HidlToAidlSessionAdapter implements ISession {
 
-    private final String TAG = "AidlToHidlAdapter";
+    private static final String TAG = "HidlToAidlSessionAdapter";
+
     private static final int CHALLENGE_TIMEOUT_SEC = 600;
     @DurationMillisLong
     private static final int GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS = 60 * 1000;
     @DurationMillisLong
     private static final int GENERATE_CHALLENGE_COUNTER_TTL_MILLIS = CHALLENGE_TIMEOUT_SEC * 1000;
     private static final int INVALID_VALUE = -1;
+    @VisibleForTesting static final int ENROLL_TIMEOUT_SEC = 75;
+
     private final Clock mClock;
     private final List<Long> mGeneratedChallengeCount = new ArrayList<>();
-    @VisibleForTesting static final int ENROLL_TIMEOUT_SEC = 75;
+    private final int mUserId;
+    private final Context mContext;
+
     private long mGenerateChallengeCreatedAt = INVALID_VALUE;
     private long mGenerateChallengeResult = INVALID_VALUE;
     @NonNull private Supplier<IBiometricsFace> mSession;
-    private final int mUserId;
     private HidlToAidlCallbackConverter mHidlToAidlCallbackConverter;
-    private final Context mContext;
     private int mFeature = INVALID_VALUE;
 
-    public AidlToHidlAdapter(Context context, Supplier<IBiometricsFace> session,
+    public HidlToAidlSessionAdapter(Context context, Supplier<IBiometricsFace> session,
             int userId, AidlResponseHandler aidlResponseHandler) {
         this(context, session, userId, aidlResponseHandler, Clock.systemUTC());
     }
 
-    AidlToHidlAdapter(Context context, Supplier<IBiometricsFace> session, int userId,
+    HidlToAidlSessionAdapter(Context context, Supplier<IBiometricsFace> session, int userId,
             AidlResponseHandler aidlResponseHandler, Clock clock) {
         mSession = session;
         mUserId = userId;
@@ -83,42 +86,11 @@
         setCallback(aidlResponseHandler);
     }
 
-    private void setCallback(AidlResponseHandler aidlResponseHandler) {
-        mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler);
-        try {
-            mSession.get().setCallback(mHidlToAidlCallbackConverter);
-        } catch (RemoteException e) {
-            Slog.d(TAG, "Failed to set callback");
-        }
-    }
-
     @Override
     public IBinder asBinder() {
         return null;
     }
 
-    private boolean isGeneratedChallengeCacheValid() {
-        return mGenerateChallengeCreatedAt != INVALID_VALUE
-                && mGenerateChallengeResult != INVALID_VALUE
-                && mClock.millis() - mGenerateChallengeCreatedAt
-                < GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS;
-    }
-
-    private void incrementChallengeCount() {
-        mGeneratedChallengeCount.add(0, mClock.millis());
-    }
-
-    private int decrementChallengeCount() {
-        final long now = mClock.millis();
-        // ignore values that are old in case generate/revoke calls are not matched
-        // this doesn't ensure revoke if calls are mismatched but it keeps the list from growing
-        mGeneratedChallengeCount.removeIf(x -> now - x > GENERATE_CHALLENGE_COUNTER_TTL_MILLIS);
-        if (!mGeneratedChallengeCount.isEmpty()) {
-            mGeneratedChallengeCount.remove(0);
-        }
-        return mGeneratedChallengeCount.size();
-    }
-
     @Override
     public void generateChallenge() throws RemoteException {
         incrementChallengeCount();
@@ -150,7 +122,7 @@
 
     @Override
     public EnrollmentStageConfig[] getEnrollmentConfig(byte enrollmentType) throws RemoteException {
-        //unsupported in HIDL
+        Slog.e(TAG, "getEnrollmentConfig unsupported in HIDL");
         return null;
     }
 
@@ -244,19 +216,6 @@
         }
     }
 
-    private int getFaceId() {
-        FaceManager faceManager = mContext.getSystemService(FaceManager.class);
-        List<Face> faces = faceManager.getEnrolledFaces(mUserId);
-        if (faces.isEmpty()) {
-            Slog.d(TAG, "No faces to get feature from.");
-            mHidlToAidlCallbackConverter.onError(0 /* deviceId */, mUserId,
-                    BiometricFaceConstants.FACE_ERROR_NOT_ENROLLED, 0 /* vendorCode */);
-            return INVALID_VALUE;
-        }
-
-        return faces.get(0).getBiometricId();
-    }
-
     @Override
     public void getAuthenticatorId() throws RemoteException {
         long authenticatorId = mSession.get().getAuthenticatorId().value;
@@ -265,7 +224,8 @@
 
     @Override
     public void invalidateAuthenticatorId() throws RemoteException {
-        //unsupported in HIDL
+        Slog.e(TAG, "invalidateAuthenticatorId unsupported in HIDL");
+        mHidlToAidlCallbackConverter.onUnsupportedClientScheduled();
     }
 
     @Override
@@ -279,47 +239,105 @@
 
     @Override
     public void close() throws RemoteException {
-        //Unsupported in HIDL
+        Slog.e(TAG, "close unsupported in HIDL");
     }
 
     @Override
     public ICancellationSignal authenticateWithContext(long operationId, OperationContext context)
             throws RemoteException {
-        //Unsupported in HIDL
-        return null;
+        Slog.e(TAG, "authenticateWithContext unsupported in HIDL");
+        return authenticate(operationId);
     }
 
     @Override
     public ICancellationSignal enrollWithContext(HardwareAuthToken hat, byte type, byte[] features,
             NativeHandle previewSurface, OperationContext context) throws RemoteException {
-        //Unsupported in HIDL
-        return null;
+        Slog.e(TAG, "enrollWithContext unsupported in HIDL");
+        return enroll(hat, type, features, previewSurface);
     }
 
     @Override
     public ICancellationSignal detectInteractionWithContext(OperationContext context)
             throws RemoteException {
-        //Unsupported in HIDL
-        return null;
+        Slog.e(TAG, "detectInteractionWithContext unsupported in HIDL");
+        return detectInteraction();
     }
 
     @Override
     public void onContextChanged(OperationContext context) throws RemoteException {
-        //Unsupported in HIDL
+        Slog.e(TAG, "onContextChanged unsupported in HIDL");
     }
 
     @Override
     public int getInterfaceVersion() throws RemoteException {
-        //Unsupported in HIDL
+        Slog.e(TAG, "getInterfaceVersion unsupported in HIDL");
         return 0;
     }
 
     @Override
     public String getInterfaceHash() throws RemoteException {
+        Slog.e(TAG, "getInterfaceHash unsupported in HIDL");
+        return null;
+    }
+
+    @Override
+    public ICancellationSignal enrollWithOptions(FaceEnrollOptions options) {
         //Unsupported in HIDL
         return null;
     }
 
+    private boolean isGeneratedChallengeCacheValid() {
+        return mGenerateChallengeCreatedAt != INVALID_VALUE
+                && mGenerateChallengeResult != INVALID_VALUE
+                && mClock.millis() - mGenerateChallengeCreatedAt
+                < GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS;
+    }
+
+    private void incrementChallengeCount() {
+        mGeneratedChallengeCount.add(0, mClock.millis());
+    }
+
+    private int decrementChallengeCount() {
+        final long now = mClock.millis();
+        // ignore values that are old in case generate/revoke calls are not matched
+        // this doesn't ensure revoke if calls are mismatched but it keeps the list from growing
+        mGeneratedChallengeCount.removeIf(x -> now - x > GENERATE_CHALLENGE_COUNTER_TTL_MILLIS);
+        if (!mGeneratedChallengeCount.isEmpty()) {
+            mGeneratedChallengeCount.remove(0);
+        }
+        return mGeneratedChallengeCount.size();
+    }
+
+    private void setCallback(AidlResponseHandler aidlResponseHandler) {
+        mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler);
+        try {
+            if (mSession.get() != null) {
+                long halId = mSession.get().setCallback(mHidlToAidlCallbackConverter).value;
+                Slog.d(TAG, "Face HAL ready, HAL ID: " + halId);
+                if (halId == 0) {
+                    Slog.d(TAG, "Unable to set HIDL callback.");
+                }
+            } else {
+                Slog.e(TAG, "Unable to set HIDL callback. HIDL daemon is null.");
+            }
+        } catch (RemoteException e) {
+            Slog.d(TAG, "Failed to set callback");
+        }
+    }
+
+    private int getFaceId() {
+        FaceManager faceManager = mContext.getSystemService(FaceManager.class);
+        List<Face> faces = faceManager.getEnrolledFaces(mUserId);
+        if (faces.isEmpty()) {
+            Slog.d(TAG, "No faces to get feature from.");
+            mHidlToAidlCallbackConverter.onError(0 /* deviceId */, mUserId,
+                    BiometricFaceConstants.FACE_ERROR_NOT_ENROLLED, 0 /* vendorCode */);
+            return INVALID_VALUE;
+        }
+
+        return faces.get(0).getBiometricId();
+    }
+
     /**
      * Cancellation in HIDL occurs for the entire session, instead of a specific client.
      */
@@ -345,10 +363,4 @@
             return null;
         }
     }
-
-    @Override
-    public ICancellationSignal enrollWithOptions(FaceEnrollOptions options) {
-        //Unsupported in HIDL
-        return null;
-    }
 }
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 83b306b..e01d672 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
@@ -45,9 +45,11 @@
 import android.hardware.biometrics.ITestSessionCallback;
 import android.hardware.biometrics.fingerprint.IFingerprint;
 import android.hardware.biometrics.fingerprint.PointerContext;
+import android.hardware.biometrics.fingerprint.SensorProps;
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
 import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorConfigurations;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.FingerprintServiceReceiver;
 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
@@ -80,6 +82,7 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.SystemService;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.AuthenticationStateListeners;
@@ -127,6 +130,8 @@
     @NonNull
     private final Function<String, FingerprintProvider> mFingerprintProvider;
     @NonNull
+    private final FingerprintProviderFunction mFingerprintProviderFunction;
+    @NonNull
     private final BiometricStateCallback<ServiceProvider, FingerprintSensorPropertiesInternal>
             mBiometricStateCallback;
     @NonNull
@@ -136,6 +141,11 @@
     @NonNull
     private final FingerprintServiceRegistry mRegistry;
 
+    interface FingerprintProviderFunction {
+        FingerprintProvider getFingerprintProvider(Pair<String, SensorProps[]> filteredSensorProp,
+                boolean resetLockoutRequiresHardwareAuthToken);
+    }
+
     /** Receives the incoming binder calls from FingerprintManager. */
     @VisibleForTesting
     final IFingerprintService.Stub mServiceWrapper = new IFingerprintService.Stub() {
@@ -874,6 +884,18 @@
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
+        public void registerAuthenticatorsLegacy(
+                @NonNull FingerprintSensorConfigurations fingerprintSensorConfigurations) {
+            super.registerAuthenticatorsLegacy_enforcePermission();
+            if (!fingerprintSensorConfigurations.hasSensorConfigurations()) {
+                Slog.d(TAG, "No fingerprint sensors available.");
+                return;
+            }
+            mRegistry.registerAll(() -> getProviders(fingerprintSensorConfigurations));
+        }
+
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override // Binder call
         public void registerAuthenticators(
                 @NonNull List<FingerprintSensorPropertiesInternal> hidlSensors) {
             super.registerAuthenticators_enforcePermission();
@@ -1021,7 +1043,8 @@
                 () -> IBiometricService.Stub.asInterface(
                         ServiceManager.getService(Context.BIOMETRIC_SERVICE)),
                 () -> ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR),
-                null /* fingerprintProvider */);
+                null /* fingerprintProvider */,
+                null /* fingerprintProviderFunction */);
     }
 
     @VisibleForTesting
@@ -1029,7 +1052,8 @@
             BiometricContext biometricContext,
             Supplier<IBiometricService> biometricServiceSupplier,
             Supplier<String[]> aidlInstanceNameSupplier,
-            Function<String, FingerprintProvider> fingerprintProvider) {
+            Function<String, FingerprintProvider> fingerprintProvider,
+            FingerprintProviderFunction fingerprintProviderFunction) {
         super(context);
         mBiometricContext = biometricContext;
         mAidlInstanceNameSupplier = aidlInstanceNameSupplier;
@@ -1049,7 +1073,8 @@
                             return new FingerprintProvider(getContext(),
                                     mBiometricStateCallback, mAuthenticationStateListeners,
                                     fp.getSensorProps(), name, mLockoutResetDispatcher,
-                                    mGestureAvailabilityDispatcher, mBiometricContext);
+                                    mGestureAvailabilityDispatcher, mBiometricContext,
+                                    true /* resetLockoutRequiresHardwareAuthToken */);
                         } catch (RemoteException e) {
                             Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
                         }
@@ -1059,6 +1084,22 @@
 
                     return null;
                 };
+        if (Flags.deHidl()) {
+            mFingerprintProviderFunction = fingerprintProviderFunction == null
+                    ? (filteredSensorProps, resetLockoutRequiresHardwareAuthToken) ->
+                            new FingerprintProvider(
+                                    getContext(), mBiometricStateCallback,
+                                    mAuthenticationStateListeners,
+                                    filteredSensorProps.second,
+                                    filteredSensorProps.first, mLockoutResetDispatcher,
+                                    mGestureAvailabilityDispatcher,
+                                    mBiometricContext,
+                                    resetLockoutRequiresHardwareAuthToken)
+                    : fingerprintProviderFunction;
+        } else {
+            mFingerprintProviderFunction =
+                    (filteredSensorProps, resetLockoutRequiresHardwareAuthToken) -> null;
+        }
         mHandler = new Handler(Looper.getMainLooper());
         mRegistry = new FingerprintServiceRegistry(mServiceWrapper, biometricServiceSupplier);
         mRegistry.addAllRegisteredCallback(new IFingerprintAuthenticatorsRegisteredCallback.Stub() {
@@ -1070,6 +1111,44 @@
         });
     }
 
+    @NonNull
+    private List<ServiceProvider> getProviders(@NonNull FingerprintSensorConfigurations
+            fingerprintSensorConfigurations) {
+        final List<ServiceProvider> providers = new ArrayList<>();
+        final Pair<String, SensorProps[]> filteredSensorProps = filterAvailableHalInstances(
+                fingerprintSensorConfigurations);
+        providers.add(mFingerprintProviderFunction.getFingerprintProvider(filteredSensorProps,
+                fingerprintSensorConfigurations.getResetLockoutRequiresHardwareAuthToken()));
+
+        return providers;
+    }
+
+    @NonNull
+    private Pair<String, SensorProps[]> filterAvailableHalInstances(
+            FingerprintSensorConfigurations fingerprintSensorConfigurations) {
+        Pair<String, SensorProps[]> finalSensorPair =
+                fingerprintSensorConfigurations.getSensorPair();
+        if (fingerprintSensorConfigurations.isSingleSensorConfigurationPresent()) {
+            return finalSensorPair;
+        }
+
+        final Pair<String, SensorProps[]> virtualSensorPropsPair = fingerprintSensorConfigurations
+                .getSensorPairForInstance("virtual");
+        if (Utils.isVirtualEnabled(getContext())) {
+            if (virtualSensorPropsPair != null) {
+                return virtualSensorPropsPair;
+            } else {
+                Slog.e(TAG, "Could not find virtual interface while it is enabled");
+                return finalSensorPair;
+            }
+        } else {
+            if (virtualSensorPropsPair != null) {
+                return fingerprintSensorConfigurations.getSensorPairNotForInstance("virtual");
+            }
+        }
+        return finalSensorPair;
+    }
+
     private Pair<List<FingerprintSensorPropertiesInternal>, List<String>>
                 filterAvailableHalInstances(
             @NonNull List<FingerprintSensorPropertiesInternal> hidlInstances,
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java
index 4a01943..bd21cf4 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java
@@ -25,6 +25,7 @@
 import android.hardware.keymaster.HardwareAuthToken;
 import android.util.Slog;
 
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.HardwareAuthTokenUtils;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.sensors.AcquisitionClient;
@@ -34,9 +35,9 @@
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.EnumerateConsumer;
 import com.android.server.biometrics.sensors.ErrorConsumer;
-import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutConsumer;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.RemovalConsumer;
 import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils;
 
@@ -59,6 +60,21 @@
         void onHardwareUnavailable();
     }
 
+    /**
+     * Interface to send results to the AidlResponseHandler's owner.
+     */
+    public interface AidlResponseHandlerCallback {
+        /**
+         * Invoked when enrollment is successful.
+         */
+        void onEnrollSuccess();
+
+        /**
+         * Invoked when the HAL sends ERROR_HW_UNAVAILABLE.
+         */
+        void onHardwareUnavailable();
+    }
+
     private static final String TAG = "AidlResponseHandler";
 
     @NonNull
@@ -68,28 +84,49 @@
     private final int mSensorId;
     private final int mUserId;
     @NonNull
-    private final LockoutCache mLockoutCache;
+    private final LockoutTracker mLockoutTracker;
     @NonNull
     private final LockoutResetDispatcher mLockoutResetDispatcher;
     @NonNull
     private final AuthSessionCoordinator mAuthSessionCoordinator;
     @NonNull
     private final HardwareUnavailableCallback mHardwareUnavailableCallback;
+    @NonNull
+    private final AidlResponseHandlerCallback mAidlResponseHandlerCallback;
 
     public AidlResponseHandler(@NonNull Context context,
             @NonNull BiometricScheduler scheduler, int sensorId, int userId,
-            @NonNull LockoutCache lockoutTracker,
+            @NonNull LockoutTracker lockoutTracker,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull AuthSessionCoordinator authSessionCoordinator,
             @NonNull HardwareUnavailableCallback hardwareUnavailableCallback) {
+        this(context, scheduler, sensorId, userId, lockoutTracker, lockoutResetDispatcher,
+                authSessionCoordinator, hardwareUnavailableCallback,
+                new AidlResponseHandlerCallback() {
+                    @Override
+                    public void onEnrollSuccess() {}
+
+                    @Override
+                    public void onHardwareUnavailable() {}
+                });
+    }
+
+    public AidlResponseHandler(@NonNull Context context,
+            @NonNull BiometricScheduler scheduler, int sensorId, int userId,
+            @NonNull LockoutTracker lockoutTracker,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull AuthSessionCoordinator authSessionCoordinator,
+            @NonNull HardwareUnavailableCallback hardwareUnavailableCallback,
+            @NonNull AidlResponseHandlerCallback aidlResponseHandlerCallback) {
         mContext = context;
         mScheduler = scheduler;
         mSensorId = sensorId;
         mUserId = userId;
-        mLockoutCache = lockoutTracker;
+        mLockoutTracker = lockoutTracker;
         mLockoutResetDispatcher = lockoutResetDispatcher;
         mAuthSessionCoordinator = authSessionCoordinator;
         mHardwareUnavailableCallback = hardwareUnavailableCallback;
+        mAidlResponseHandlerCallback = aidlResponseHandlerCallback;
     }
 
     @Override
@@ -105,27 +142,26 @@
     @Override
     public void onChallengeGenerated(long challenge) {
         handleResponse(FingerprintGenerateChallengeClient.class, (c) -> c.onChallengeGenerated(
-                mSensorId, mUserId, challenge), null);
+                mSensorId, mUserId, challenge));
     }
 
     @Override
     public void onChallengeRevoked(long challenge) {
         handleResponse(FingerprintRevokeChallengeClient.class, (c) -> c.onChallengeRevoked(
-                challenge), null);
+                challenge));
     }
 
     /**
      * Handles acquired messages sent by the HAL (specifically for HIDL HAL).
      */
     public void onAcquired(int acquiredInfo, int vendorCode) {
-        handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(acquiredInfo, vendorCode),
-                null);
+        handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(acquiredInfo, vendorCode));
     }
 
     @Override
     public void onAcquired(byte info, int vendorCode) {
         handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(
-                AidlConversionUtils.toFrameworkAcquiredInfo(info), vendorCode), null);
+                AidlConversionUtils.toFrameworkAcquiredInfo(info), vendorCode));
     }
 
     /**
@@ -135,9 +171,13 @@
         handleResponse(ErrorConsumer.class, (c) -> {
             c.onError(error, vendorCode);
             if (error == Error.HW_UNAVAILABLE) {
-                mHardwareUnavailableCallback.onHardwareUnavailable();
+                if (Flags.deHidl()) {
+                    mAidlResponseHandlerCallback.onHardwareUnavailable();
+                } else {
+                    mHardwareUnavailableCallback.onHardwareUnavailable();
+                }
             }
-        }, null);
+        });
     }
 
     @Override
@@ -158,8 +198,12 @@
                 .getUniqueName(mContext, currentUserId);
         final Fingerprint fingerprint = new Fingerprint(name, currentUserId,
                 enrollmentId, mSensorId);
-        handleResponse(FingerprintEnrollClient.class, (c) -> c.onEnrollResult(fingerprint,
-                remaining), null);
+        handleResponse(FingerprintEnrollClient.class, (c) -> {
+            c.onEnrollResult(fingerprint, remaining);
+            if (remaining == 0) {
+                mAidlResponseHandlerCallback.onEnrollSuccess();
+            }
+        });
     }
 
     @Override
@@ -184,13 +228,12 @@
 
     @Override
     public void onLockoutTimed(long durationMillis) {
-        handleResponse(LockoutConsumer.class, (c) -> c.onLockoutTimed(durationMillis),
-                null);
+        handleResponse(LockoutConsumer.class, (c) -> c.onLockoutTimed(durationMillis));
     }
 
     @Override
     public void onLockoutPermanent() {
-        handleResponse(LockoutConsumer.class, LockoutConsumer::onLockoutPermanent, null);
+        handleResponse(LockoutConsumer.class, LockoutConsumer::onLockoutPermanent);
     }
 
     @Override
@@ -198,7 +241,7 @@
         handleResponse(FingerprintResetLockoutClient.class,
                 FingerprintResetLockoutClient::onLockoutCleared,
                 (c) -> FingerprintResetLockoutClient.resetLocalLockoutStateToNone(
-                        mSensorId, mUserId, mLockoutCache, mLockoutResetDispatcher,
+                        mSensorId, mUserId, mLockoutTracker, mLockoutResetDispatcher,
                         mAuthSessionCoordinator, Utils.getCurrentStrength(mSensorId),
                         -1 /* requestId */));
     }
@@ -206,49 +249,74 @@
     @Override
     public void onInteractionDetected() {
         handleResponse(FingerprintDetectClient.class,
-                FingerprintDetectClient::onInteractionDetected, null);
+                FingerprintDetectClient::onInteractionDetected);
     }
 
     @Override
     public void onEnrollmentsEnumerated(int[] enrollmentIds) {
         if (enrollmentIds.length > 0) {
             for (int i = 0; i < enrollmentIds.length; i++) {
-                final Fingerprint fp = new Fingerprint("", enrollmentIds[i], mSensorId);
-                int finalI = i;
-                handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(fp,
-                        enrollmentIds.length - finalI - 1), null);
+                onEnrollmentEnumerated(enrollmentIds[i],
+                        enrollmentIds.length - i - 1 /* remaining */);
             }
         } else {
-            handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(null,
-                    0), null);
+            handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(
+                    null /* identifier */,
+                    0 /* remaining */));
         }
     }
 
+    /**
+     * Handle enumerated fingerprint.
+     */
+    public void onEnrollmentEnumerated(int enrollmentId, int remaining) {
+        final Fingerprint fp = new Fingerprint("", enrollmentId, mSensorId);
+        handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(fp, remaining));
+    }
+
+    /**
+     * Handle removal of fingerprint.
+     */
+    public void onEnrollmentRemoved(int enrollmentId, int remaining) {
+        final Fingerprint fp = new Fingerprint("", enrollmentId, mSensorId);
+        handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(fp, remaining));
+    }
+
     @Override
     public void onEnrollmentsRemoved(int[] enrollmentIds) {
         if (enrollmentIds.length > 0) {
             for (int i  = 0; i < enrollmentIds.length; i++) {
-                final Fingerprint fp = new Fingerprint("", enrollmentIds[i], mSensorId);
-                int finalI = i;
-                handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(fp,
-                        enrollmentIds.length - finalI - 1), null);
+                onEnrollmentRemoved(enrollmentIds[i], enrollmentIds.length - i - 1 /* remaining */);
             }
         } else {
-            handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(null, 0),
-                    null);
+            handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(null /* identifier */,
+                    0 /* remaining */));
         }
     }
 
     @Override
     public void onAuthenticatorIdRetrieved(long authenticatorId) {
         handleResponse(FingerprintGetAuthenticatorIdClient.class,
-                (c) -> c.onAuthenticatorIdRetrieved(authenticatorId), null);
+                (c) -> c.onAuthenticatorIdRetrieved(authenticatorId));
     }
 
     @Override
     public void onAuthenticatorIdInvalidated(long newAuthenticatorId) {
         handleResponse(FingerprintInvalidationClient.class, (c) -> c.onAuthenticatorIdInvalidated(
-                newAuthenticatorId), null);
+                newAuthenticatorId));
+    }
+
+    /**
+     * Handle clients which are not supported in HIDL HAL.
+     */
+    public <T extends BaseClientMonitor> void onUnsupportedClientScheduled(Class<T> className) {
+        Slog.e(TAG, className + " is not supported in the HAL.");
+        handleResponse(className, (c) -> c.cancel());
+    }
+
+    private <T> void handleResponse(@NonNull Class<T> className,
+            @NonNull Consumer<T> action) {
+        handleResponse(className, action, null /* alternateAction */);
     }
 
     private <T> void handleResponse(@NonNull Class<T> className,
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java
index 299a310..8ff105b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java
@@ -20,7 +20,7 @@
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
 
-import com.android.server.biometrics.sensors.fingerprint.hidl.AidlToHidlAdapter;
+import com.android.server.biometrics.sensors.fingerprint.hidl.HidlToAidlSessionAdapter;
 
 import java.util.function.Supplier;
 
@@ -45,7 +45,7 @@
 
     public AidlSession(@NonNull Supplier<IBiometricsFingerprint> session,
             int userId, AidlResponseHandler aidlResponseHandler) {
-        mSession = new AidlToHidlAdapter(session, userId, aidlResponseHandler);
+        mSession = new HidlToAidlSessionAdapter(session, userId, aidlResponseHandler);
         mHalInterfaceVersion = 0;
         mUserId = userId;
         mAidlResponseHandler = aidlResponseHandler;
@@ -62,7 +62,7 @@
     }
 
     /** The HAL callback, which should only be used in tests {@See BiometricTestSessionImpl}. */
-    AidlResponseHandler getHalSessionCallback() {
+    public AidlResponseHandler getHalSessionCallback() {
         return mAidlResponseHandler;
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java
index ea1a622..0353969 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java
@@ -30,7 +30,7 @@
 import java.util.Map;
 import java.util.function.Supplier;
 
-class FingerprintGetAuthenticatorIdClient extends HalClientMonitor<AidlSession> {
+public class FingerprintGetAuthenticatorIdClient extends HalClientMonitor<AidlSession> {
 
     private static final String TAG = "FingerprintGetAuthenticatorIdClient";
 
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 032ab87..88a11d9 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.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver;
 import com.android.server.biometrics.AuthenticationStatsCollector;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
@@ -73,6 +74,7 @@
 import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
 import com.android.server.biometrics.sensors.InvalidationRequesterClient;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.PerformanceTracker;
 import com.android.server.biometrics.sensors.SensorList;
 import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils;
@@ -80,6 +82,7 @@
 import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler;
 import com.android.server.biometrics.sensors.fingerprint.ServiceProvider;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
+import com.android.server.biometrics.sensors.fingerprint.hidl.HidlToAidlSensorAdapter;
 
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -165,10 +168,12 @@
             @NonNull SensorProps[] props, @NonNull String halInstanceName,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
-            @NonNull BiometricContext biometricContext) {
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresHardwareAuthToken) {
         this(context, biometricStateCallback, authenticationStateListeners, props, halInstanceName,
                 lockoutResetDispatcher, gestureAvailabilityDispatcher, biometricContext,
-                null /* daemon */);
+                null /* daemon */, resetLockoutRequiresHardwareAuthToken,
+                false /* testHalEnabled */);
     }
 
     @VisibleForTesting FingerprintProvider(@NonNull Context context,
@@ -178,7 +183,9 @@
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
             @NonNull BiometricContext biometricContext,
-            IFingerprint daemon) {
+            @Nullable IFingerprint daemon,
+            boolean resetLockoutRequiresHardwareAuthToken,
+            boolean testHalEnabled) {
         mContext = context;
         mBiometricStateCallback = biometricStateCallback;
         mAuthenticationStateListeners = authenticationStateListeners;
@@ -191,62 +198,136 @@
         mBiometricContext = biometricContext;
         mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator();
         mDaemon = daemon;
+        mTestHalEnabled = testHalEnabled;
 
-        AuthenticationStatsBroadcastReceiver mBroadcastReceiver =
-                new AuthenticationStatsBroadcastReceiver(
-                        mContext,
-                        BiometricsProtoEnums.MODALITY_FINGERPRINT,
-                        (AuthenticationStatsCollector collector) -> {
-                            Slog.d(getTag(), "Initializing AuthenticationStatsCollector");
-                            mAuthenticationStatsCollector = collector;
-                        });
+        initAuthenticationBroadcastReceiver();
+        initSensors(resetLockoutRequiresHardwareAuthToken, props, gestureAvailabilityDispatcher);
+    }
 
-        final List<SensorLocationInternal> workaroundLocations = getWorkaroundSensorProps(context);
+    private void initAuthenticationBroadcastReceiver() {
+        new AuthenticationStatsBroadcastReceiver(
+                mContext,
+                BiometricsProtoEnums.MODALITY_FINGERPRINT,
+                (AuthenticationStatsCollector collector) -> {
+                    Slog.d(getTag(), "Initializing AuthenticationStatsCollector");
+                    mAuthenticationStatsCollector = collector;
+                });
+    }
 
-        for (SensorProps prop : props) {
-            final int sensorId = prop.commonProps.sensorId;
-
-            final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
-            if (prop.commonProps.componentInfo != null) {
-                for (ComponentInfo info : prop.commonProps.componentInfo) {
-                    componentInfo.add(new ComponentInfoInternal(info.componentId,
-                            info.hardwareVersion, info.firmwareVersion, info.serialNumber,
-                            info.softwareVersion));
+    private void initSensors(boolean resetLockoutRequiresHardwareAuthToken, SensorProps[] props,
+            GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
+        if (Flags.deHidl()) {
+            if (!resetLockoutRequiresHardwareAuthToken) {
+                Slog.d(getTag(), "Adding HIDL configs");
+                for (SensorProps sensorConfig: props) {
+                    addHidlSensors(sensorConfig, gestureAvailabilityDispatcher,
+                            resetLockoutRequiresHardwareAuthToken);
+                }
+            } else {
+                Slog.d(getTag(), "Adding AIDL configs");
+                final List<SensorLocationInternal> workaroundLocations =
+                        getWorkaroundSensorProps(mContext);
+                for (SensorProps prop : props) {
+                    addAidlSensors(prop, gestureAvailabilityDispatcher, workaroundLocations,
+                            resetLockoutRequiresHardwareAuthToken);
                 }
             }
+        } else {
+            final List<SensorLocationInternal> workaroundLocations =
+                    getWorkaroundSensorProps(mContext);
 
-            final FingerprintSensorPropertiesInternal internalProp =
-                    new FingerprintSensorPropertiesInternal(prop.commonProps.sensorId,
-                            prop.commonProps.sensorStrength,
-                            prop.commonProps.maxEnrollmentsPerUser,
-                            componentInfo,
-                            prop.sensorType,
-                            prop.halControlsIllumination,
-                            true /* resetLockoutRequiresHardwareAuthToken */,
-                            !workaroundLocations.isEmpty() ? workaroundLocations :
-                                    Arrays.stream(prop.sensorLocations).map(location ->
-                                                    new SensorLocationInternal(
-                                                            location.display,
-                                                            location.sensorLocationX,
-                                                            location.sensorLocationY,
-                                                            location.sensorRadius))
-                                            .collect(Collectors.toList()));
-            final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, mHandler,
-                    internalProp, lockoutResetDispatcher, gestureAvailabilityDispatcher,
-                    mBiometricContext);
-            final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
-                    sensor.getLazySession().get().getUserId();
-            mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId,
-                    new SynchronousUserSwitchObserver() {
-                        @Override
-                        public void onUserSwitching(int newUserId) {
-                            scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
-                        }
-                    });
-            Slog.d(getTag(), "Added: " + internalProp);
+            for (SensorProps prop : props) {
+                final int sensorId = prop.commonProps.sensorId;
+                final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
+                if (prop.commonProps.componentInfo != null) {
+                    for (ComponentInfo info : prop.commonProps.componentInfo) {
+                        componentInfo.add(new ComponentInfoInternal(info.componentId,
+                                info.hardwareVersion, info.firmwareVersion, info.serialNumber,
+                                info.softwareVersion));
+                    }
+                }
+                final FingerprintSensorPropertiesInternal internalProp =
+                        new FingerprintSensorPropertiesInternal(prop.commonProps.sensorId,
+                                prop.commonProps.sensorStrength,
+                                prop.commonProps.maxEnrollmentsPerUser,
+                                componentInfo,
+                                prop.sensorType,
+                                prop.halControlsIllumination,
+                                true /* resetLockoutRequiresHardwareAuthToken */,
+                                !workaroundLocations.isEmpty() ? workaroundLocations :
+                                        Arrays.stream(prop.sensorLocations).map(
+                                                        location -> new SensorLocationInternal(
+                                                                location.display,
+                                                                location.sensorLocationX,
+                                                                location.sensorLocationY,
+                                                                location.sensorRadius))
+                                                .collect(Collectors.toList()));
+                final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext,
+                        mHandler, internalProp, mLockoutResetDispatcher,
+                        gestureAvailabilityDispatcher, mBiometricContext);
+                sensor.init(gestureAvailabilityDispatcher,
+                        mLockoutResetDispatcher);
+                final int sessionUserId =
+                        sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
+                                sensor.getLazySession().get().getUserId();
+                mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId,
+                        new SynchronousUserSwitchObserver() {
+                            @Override
+                            public void onUserSwitching(int newUserId) {
+                                scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
+                            }
+                        });
+                Slog.d(getTag(), "Added: " + mFingerprintSensors.get(sensorId).toString());
+            }
         }
     }
 
+    private void addHidlSensors(@NonNull SensorProps prop,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            boolean resetLockoutRequiresHardwareAuthToken) {
+        final int sensorId = prop.commonProps.sensorId;
+        final Sensor sensor = new HidlToAidlSensorAdapter(getTag() + "/"
+                + sensorId, this, mContext, mHandler,
+                prop, mLockoutResetDispatcher, gestureAvailabilityDispatcher,
+                mBiometricContext, resetLockoutRequiresHardwareAuthToken,
+                () -> scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser(),
+                        null /* callback */));
+        sensor.init(gestureAvailabilityDispatcher, mLockoutResetDispatcher);
+        final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
+                sensor.getLazySession().get().getUserId();
+        mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId,
+                new SynchronousUserSwitchObserver() {
+                    @Override
+                    public void onUserSwitching(int newUserId) {
+                        scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
+                    }
+                });
+        Slog.d(getTag(), "Added: " + mFingerprintSensors.get(sensorId).toString());
+    }
+
+    private void addAidlSensors(@NonNull SensorProps prop,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            List<SensorLocationInternal> workaroundLocations,
+            boolean resetLockoutRequiresHardwareAuthToken) {
+        final int sensorId = prop.commonProps.sensorId;
+        final Sensor sensor = new Sensor(getTag() + "/" + sensorId,
+                this, mContext, mHandler,
+                prop, mLockoutResetDispatcher, gestureAvailabilityDispatcher,
+                mBiometricContext, workaroundLocations,
+                resetLockoutRequiresHardwareAuthToken);
+        sensor.init(gestureAvailabilityDispatcher, mLockoutResetDispatcher);
+        final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
+                sensor.getLazySession().get().getUserId();
+        mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId,
+                new SynchronousUserSwitchObserver() {
+                    @Override
+                    public void onUserSwitching(int newUserId) {
+                        scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
+                    }
+                });
+        Slog.d(getTag(), "Added: " + mFingerprintSensors.get(sensorId).toString());
+    }
+
     private String getTag() {
         return "FingerprintProvider/" + mHalInstanceName;
     }
@@ -351,7 +432,10 @@
         }
     }
 
-    private void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) {
+    /**
+     * Schedules FingerprintGetAuthenticatorIdClient for specific sensor and user.
+     */
+    protected void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) {
         mHandler.post(() -> {
             final FingerprintGetAuthenticatorIdClient client =
                     new FingerprintGetAuthenticatorIdClient(mContext,
@@ -387,8 +471,8 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN,
                             mAuthenticationStatsCollector),
                     mBiometricContext, hardwareAuthToken,
-                    mFingerprintSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher,
-                    Utils.getCurrentStrength(sensorId));
+                    mFingerprintSensors.get(sensorId).getLockoutTracker(false /* forAuth */),
+                    mLockoutResetDispatcher, Utils.getCurrentStrength(sensorId));
             scheduleForSensor(sensorId, client);
         });
     }
@@ -443,18 +527,23 @@
                     mFingerprintSensors.get(sensorId).getSensorProperties(),
                     mUdfpsOverlayController, mSidefpsController,
                     mAuthenticationStateListeners, maxTemplatesPerUser, enrollReason);
-            scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
-                    mBiometricStateCallback, new ClientMonitorCallback() {
-                @Override
-                public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
-                        boolean success) {
-                    ClientMonitorCallback.super.onClientFinished(clientMonitor, success);
-                    if (success) {
-                        scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
-                        scheduleInvalidationRequest(sensorId, userId);
-                    }
-                }
-            }));
+            if (Flags.deHidl()) {
+                scheduleForSensor(sensorId, client, mBiometricStateCallback);
+            } else {
+                scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
+                        mBiometricStateCallback, new ClientMonitorCallback() {
+                            @Override
+                            public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
+                                    boolean success) {
+                                ClientMonitorCallback.super.onClientFinished(
+                                        clientMonitor, success);
+                                if (success) {
+                                    scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
+                                    scheduleInvalidationRequest(sensorId, userId);
+                                }
+                            }
+                        }));
+            }
         });
         return id;
     }
@@ -497,6 +586,13 @@
             final int userId = options.getUserId();
             final int sensorId = options.getSensorId();
             final boolean isStrongBiometric = Utils.isStrongBiometric(sensorId);
+            final LockoutTracker lockoutTracker;
+            if (Flags.deHidl()) {
+                lockoutTracker = mFingerprintSensors.get(sensorId)
+                        .getLockoutTracker(true /* forAuth */);
+            } else {
+                lockoutTracker = null;
+            }
             final FingerprintAuthenticationClient client = new FingerprintAuthenticationClient(
                     mContext, mFingerprintSensors.get(sensorId).getLazySession(), token, requestId,
                     callback, operationId, restricted, options, cookie,
@@ -510,7 +606,7 @@
                     mFingerprintSensors.get(sensorId).getSensorProperties(), mHandler,
                     Utils.getCurrentStrength(sensorId),
                     SystemClock.elapsedRealtimeClock(),
-                    null /* lockoutTracker */);
+                    lockoutTracker);
             scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
 
                 @Override
@@ -636,6 +732,9 @@
 
     @Override
     public boolean isHardwareDetected(int sensorId) {
+        if (Flags.deHidl()) {
+            return mFingerprintSensors.get(sensorId).isHardwareDetected(mHalInstanceName);
+        }
         return hasHalInstance();
     }
 
@@ -674,8 +773,12 @@
 
     @Override
     public int getLockoutModeForUser(int sensorId, int userId) {
-        return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
-                Utils.getCurrentStrength(sensorId));
+        if (Flags.deHidl()) {
+            return mFingerprintSensors.get(sensorId).getLockoutModeForUser(userId);
+        } else {
+            return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
+                    Utils.getCurrentStrength(sensorId));
+        }
     }
 
     @Override
@@ -829,6 +932,10 @@
         mTestHalEnabled = enabled;
     }
 
+    public boolean getTestHalEnabled() {
+        return mTestHalEnabled;
+    }
+
     // TODO(b/174868353): workaround for gaps in HAL interface (remove and get directly from HAL)
     // reads values via an overlay instead of querying the HAL
     @NonNull
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java
index ec225a6..387ae12 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java
@@ -60,7 +60,8 @@
             @Authenticators.Types int biometricStrength) {
         super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner,
                 0 /* cookie */, sensorId, biometricLogger, biometricContext);
-        mHardwareAuthToken = HardwareAuthTokenUtils.toHardwareAuthToken(hardwareAuthToken);
+        mHardwareAuthToken = hardwareAuthToken == null ? null :
+                HardwareAuthTokenUtils.toHardwareAuthToken(hardwareAuthToken);
         mLockoutCache = lockoutTracker;
         mLockoutResetDispatcher = lockoutResetDispatcher;
         mBiometricStrength = biometricStrength;
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
index 893cb8f..dd887bb 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
@@ -21,21 +21,28 @@
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.hardware.biometrics.BiometricsProtoEnums;
+import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.ITestSession;
 import android.hardware.biometrics.ITestSessionCallback;
+import android.hardware.biometrics.SensorLocationInternal;
+import android.hardware.biometrics.common.ComponentInfo;
+import android.hardware.biometrics.fingerprint.IFingerprint;
 import android.hardware.biometrics.fingerprint.ISession;
+import android.hardware.biometrics.fingerprint.SensorProps;
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.SensorServiceStateProto;
 import com.android.server.biometrics.SensorStateProto;
 import com.android.server.biometrics.UserStateProto;
@@ -48,15 +55,20 @@
 import com.android.server.biometrics.sensors.ErrorConsumer;
 import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.StartUserClient;
 import com.android.server.biometrics.sensors.StopUserClient;
 import com.android.server.biometrics.sensors.UserAwareBiometricScheduler;
 import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils;
 import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 /**
  * Maintains the state of a single sensor within an instance of the
@@ -73,15 +85,17 @@
     @NonNull private final IBinder mToken;
     @NonNull private final Handler mHandler;
     @NonNull private final FingerprintSensorPropertiesInternal mSensorProperties;
-    @NonNull private final UserAwareBiometricScheduler mScheduler;
-    @NonNull private final LockoutCache mLockoutCache;
+    @NonNull private BiometricScheduler mScheduler;
+    @NonNull private LockoutTracker mLockoutTracker;
     @NonNull private final Map<Integer, Long> mAuthenticatorIds;
+    @NonNull private final BiometricContext mBiometricContext;
 
     @Nullable AidlSession mCurrentSession;
-    @NonNull private final Supplier<AidlSession> mLazySession;
+    @NonNull private Supplier<AidlSession> mLazySession;
 
-    Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context,
-            @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties,
+    public Sensor(@NonNull String tag, @NonNull FingerprintProvider provider,
+            @NonNull Context context, @NonNull Handler handler,
+            @NonNull FingerprintSensorPropertiesInternal sensorProperties,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
             @NonNull BiometricContext biometricContext, AidlSession session) {
@@ -91,8 +105,38 @@
         mToken = new Binder();
         mHandler = handler;
         mSensorProperties = sensorProperties;
-        mLockoutCache = new LockoutCache();
-        mScheduler = new UserAwareBiometricScheduler(tag,
+        mBiometricContext = biometricContext;
+        mAuthenticatorIds = new HashMap<>();
+        mCurrentSession = session;
+    }
+
+    Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context,
+            @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull BiometricContext biometricContext) {
+        this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher,
+                gestureAvailabilityDispatcher, biometricContext, null);
+    }
+
+    Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context,
+            @NonNull Handler handler, @NonNull SensorProps sensorProp,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull BiometricContext biometricContext,
+            @NonNull List<SensorLocationInternal> workaroundLocation,
+            boolean resetLockoutRequiresHardwareAuthToken) {
+        this(tag, provider, context, handler, getFingerprintSensorPropertiesInternal(sensorProp,
+                        workaroundLocation, resetLockoutRequiresHardwareAuthToken),
+                lockoutResetDispatcher, gestureAvailabilityDispatcher, biometricContext, null);
+    }
+
+    /**
+     * Initialize biometric scheduler, lockout tracker and session for the sensor.
+     */
+    public void init(GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            LockoutResetDispatcher lockoutResetDispatcher) {
+        mScheduler = new UserAwareBiometricScheduler(mTag,
                 BiometricScheduler.sensorTypeFromFingerprintProperties(mSensorProperties),
                 gestureAvailabilityDispatcher,
                 () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL,
@@ -102,7 +146,7 @@
                     public StopUserClient<?> getStopUserClient(int userId) {
                         return new FingerprintStopUserClient(mContext, mLazySession, mToken,
                                 userId, mSensorProperties.sensorId,
-                                BiometricLogger.ofUnknown(mContext), biometricContext,
+                                BiometricLogger.ofUnknown(mContext), mBiometricContext,
                                 () -> mCurrentSession = null);
                     }
 
@@ -111,13 +155,38 @@
                     public StartUserClient<?, ?> getStartUserClient(int newUserId) {
                         final int sensorId = mSensorProperties.sensorId;
 
-                        final AidlResponseHandler resultController = new AidlResponseHandler(
-                                mContext, mScheduler, sensorId, newUserId,
-                                mLockoutCache, lockoutResetDispatcher,
-                                biometricContext.getAuthSessionCoordinator(), () -> {
-                            Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE");
-                            mCurrentSession = null;
-                        });
+                        final AidlResponseHandler resultController;
+
+                        if (Flags.deHidl()) {
+                            resultController = new AidlResponseHandler(
+                                    mContext, mScheduler, sensorId, newUserId,
+                                    mLockoutTracker, lockoutResetDispatcher,
+                                    mBiometricContext.getAuthSessionCoordinator(), () -> {},
+                                    new AidlResponseHandler.AidlResponseHandlerCallback() {
+                                        @Override
+                                        public void onEnrollSuccess() {
+                                            mProvider.scheduleLoadAuthenticatorIdsForUser(sensorId,
+                                                    newUserId);
+                                            mProvider.scheduleInvalidationRequest(sensorId,
+                                                    newUserId);
+                                        }
+
+                                        @Override
+                                        public void onHardwareUnavailable() {
+                                            Slog.e(mTag,
+                                                    "Fingerprint sensor hardware unavailable.");
+                                            mCurrentSession = null;
+                                        }
+                                    });
+                        } else {
+                            resultController = new AidlResponseHandler(
+                                    mContext, mScheduler, sensorId, newUserId,
+                                    mLockoutTracker, lockoutResetDispatcher,
+                                    mBiometricContext.getAuthSessionCoordinator(), () -> {
+                                Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE");
+                                mCurrentSession = null;
+                            });
+                        }
 
                         final StartUserClient.UserStartedCallback<ISession> userStartedCallback =
                                 (userIdStarted, newSession, halInterfaceVersion) -> {
@@ -133,40 +202,58 @@
                                                         + "sensor: "
                                                         + sensorId
                                                         + ", user: " + userIdStarted);
-                                        provider.scheduleInvalidationRequest(sensorId,
+                                        mProvider.scheduleInvalidationRequest(sensorId,
                                                 userIdStarted);
                                     }
                                 };
 
-                        return new FingerprintStartUserClient(mContext, provider::getHalInstance,
+                        return new FingerprintStartUserClient(mContext, mProvider::getHalInstance,
                                 mToken, newUserId, mSensorProperties.sensorId,
-                                BiometricLogger.ofUnknown(mContext), biometricContext,
+                                BiometricLogger.ofUnknown(mContext), mBiometricContext,
                                 resultController, userStartedCallback);
                     }
                 });
-        mAuthenticatorIds = new HashMap<>();
+        mLockoutTracker = new LockoutCache();
         mLazySession = () -> mCurrentSession != null ? mCurrentSession : null;
-        mCurrentSession = session;
     }
 
-    Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context,
-            @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties,
-            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
-            @NonNull BiometricContext biometricContext) {
-        this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher,
-                gestureAvailabilityDispatcher, biometricContext, null);
+    protected static FingerprintSensorPropertiesInternal getFingerprintSensorPropertiesInternal(
+            SensorProps prop, List<SensorLocationInternal> workaroundLocations,
+            boolean resetLockoutRequiresHardwareAuthToken) {
+        final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
+        if (prop.commonProps.componentInfo != null) {
+            for (ComponentInfo info : prop.commonProps.componentInfo) {
+                componentInfo.add(new ComponentInfoInternal(info.componentId,
+                        info.hardwareVersion, info.firmwareVersion, info.serialNumber,
+                        info.softwareVersion));
+            }
+        }
+        return new FingerprintSensorPropertiesInternal(prop.commonProps.sensorId,
+                prop.commonProps.sensorStrength,
+                prop.commonProps.maxEnrollmentsPerUser,
+                componentInfo,
+                prop.sensorType,
+                prop.halControlsIllumination,
+                resetLockoutRequiresHardwareAuthToken,
+                !workaroundLocations.isEmpty() ? workaroundLocations :
+                        Arrays.stream(prop.sensorLocations).map(location ->
+                                        new SensorLocationInternal(
+                                                location.display,
+                                                location.sensorLocationX,
+                                                location.sensorLocationY,
+                                                location.sensorRadius))
+                                .collect(Collectors.toList()));
     }
 
-    @NonNull Supplier<AidlSession> getLazySession() {
+    @NonNull public Supplier<AidlSession> getLazySession() {
         return mLazySession;
     }
 
-    @NonNull FingerprintSensorPropertiesInternal getSensorProperties() {
+    @NonNull public FingerprintSensorPropertiesInternal getSensorProperties() {
         return mSensorProperties;
     }
 
-    @Nullable AidlSession getSessionForUser(int userId) {
+    @Nullable protected AidlSession getSessionForUser(int userId) {
         if (mCurrentSession != null && mCurrentSession.getUserId() == userId) {
             return mCurrentSession;
         } else {
@@ -180,15 +267,18 @@
                 biometricStateCallback, mProvider, this);
     }
 
-    @NonNull BiometricScheduler getScheduler() {
+    @NonNull public BiometricScheduler getScheduler() {
         return mScheduler;
     }
 
-    @NonNull LockoutCache getLockoutCache() {
-        return mLockoutCache;
+    @NonNull protected LockoutTracker getLockoutTracker(boolean forAuth) {
+        if (forAuth) {
+            return null;
+        }
+        return mLockoutTracker;
     }
 
-    @NonNull Map<Integer, Long> getAuthenticatorIds() {
+    @NonNull public Map<Integer, Long> getAuthenticatorIds() {
         return mAuthenticatorIds;
     }
 
@@ -262,4 +352,49 @@
         mScheduler.reset();
         mCurrentSession = null;
     }
+
+    @NonNull protected Handler getHandler() {
+        return mHandler;
+    }
+
+    @NonNull protected Context getContext() {
+        return mContext;
+    }
+
+    /**
+     * Returns true if the sensor hardware is detected.
+     */
+    protected boolean isHardwareDetected(String halInstance) {
+        if (mTestHalEnabled) {
+            return true;
+        }
+        return (ServiceManager.checkService(IFingerprint.DESCRIPTOR + "/" + halInstance)
+                != null);
+    }
+
+    @NonNull protected BiometricContext getBiometricContext() {
+        return mBiometricContext;
+    }
+
+    /**
+     * Returns lockout mode of this sensor.
+     */
+    @LockoutTracker.LockoutMode
+    public int getLockoutModeForUser(int userId) {
+        return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
+                Utils.getCurrentStrength(mSensorProperties.sensorId));
+    }
+
+    public void setScheduler(BiometricScheduler scheduler) {
+        mScheduler = scheduler;
+    }
+
+    public void setLazySession(
+            Supplier<AidlSession> lazySession) {
+        mLazySession = lazySession;
+    }
+
+    public FingerprintProvider getProvider() {
+        return mProvider;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java
index a4e6025..5c5b992 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java
@@ -29,7 +29,8 @@
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
-import com.android.server.biometrics.sensors.HalClientMonitor;
+import com.android.server.biometrics.sensors.StartUserClient;
+import com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession;
 
 import java.io.File;
 import java.util.Map;
@@ -38,7 +39,8 @@
 /**
  * Sets the HAL's current active user, and updates the framework's authenticatorId cache.
  */
-public class FingerprintUpdateActiveUserClient extends HalClientMonitor<IBiometricsFingerprint> {
+public class FingerprintUpdateActiveUserClient extends
+        StartUserClient<IBiometricsFingerprint, AidlSession> {
 
     private static final String TAG = "FingerprintUpdateActiveUserClient";
     private static final String FP_DATA_DIR = "fpdata";
@@ -53,11 +55,24 @@
             @NonNull Supplier<IBiometricsFingerprint> lazyDaemon, int userId,
             @NonNull String owner, int sensorId,
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
-            Supplier<Integer> currentUserId,
+            @NonNull Supplier<Integer> currentUserId,
             boolean hasEnrolledBiometrics, @NonNull Map<Integer, Long> authenticatorIds,
             boolean forceUpdateAuthenticatorId) {
-        super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner,
-                0 /* cookie */, sensorId, logger, biometricContext);
+        this(context, lazyDaemon, userId, owner, sensorId, logger, biometricContext, currentUserId,
+                hasEnrolledBiometrics, authenticatorIds, forceUpdateAuthenticatorId,
+                (newUserId, newUser, halInterfaceVersion) -> {});
+    }
+
+    FingerprintUpdateActiveUserClient(@NonNull Context context,
+            @NonNull Supplier<IBiometricsFingerprint> lazyDaemon, int userId,
+            @NonNull String owner, int sensorId,
+            @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
+            @NonNull Supplier<Integer> currentUserId,
+            boolean hasEnrolledBiometrics, @NonNull Map<Integer, Long> authenticatorIds,
+            boolean forceUpdateAuthenticatorId,
+            @NonNull UserStartedCallback<AidlSession> userStartedCallback) {
+        super(context, lazyDaemon, null /* token */, userId, sensorId, logger, biometricContext,
+                userStartedCallback);
         mCurrentUserId = currentUserId;
         mForceUpdateAuthenticatorId = forceUpdateAuthenticatorId;
         mHasEnrolledBiometrics = hasEnrolledBiometrics;
@@ -70,6 +85,7 @@
 
         if (mCurrentUserId.get() == getTargetUserId() && !mForceUpdateAuthenticatorId) {
             Slog.d(TAG, "Already user: " + mCurrentUserId + ", returning");
+            mUserStartedCallback.onUserStarted(getTargetUserId(), null, 0);
             callback.onClientFinished(this, true /* success */);
             return;
         }
@@ -119,6 +135,7 @@
             getFreshDaemon().setActiveGroup(targetId, mDirectory.getAbsolutePath());
             mAuthenticatorIds.put(targetId, mHasEnrolledBiometrics
                     ? getFreshDaemon().getAuthenticatorId() : 0L);
+            mUserStartedCallback.onUserStarted(targetId, null, 0);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
             Slog.e(TAG, "Failed to setActiveGroup: " + e);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlCallbackConverter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlCallbackConverter.java
index c3e5cbe..e9a48e7 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlCallbackConverter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlCallbackConverter.java
@@ -20,6 +20,7 @@
 import android.hardware.biometrics.fingerprint.V2_2.IBiometricsFingerprintClientCallback;
 
 import com.android.server.biometrics.HardwareAuthTokenUtils;
+import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler;
 
 import java.util.ArrayList;
@@ -73,12 +74,12 @@
 
     @Override
     public void onRemoved(long deviceId, int fingerId, int groupId, int remaining) {
-        mAidlResponseHandler.onEnrollmentsRemoved(new int[]{fingerId});
+        mAidlResponseHandler.onEnrollmentRemoved(fingerId, remaining);
     }
 
     @Override
     public void onEnumerate(long deviceId, int fingerId, int groupId, int remaining) {
-        mAidlResponseHandler.onEnrollmentsEnumerated(new int[]{fingerId});
+        mAidlResponseHandler.onEnrollmentEnumerated(fingerId, remaining);
     }
 
     void onChallengeGenerated(long challenge) {
@@ -92,4 +93,8 @@
     void onResetLockout() {
         mAidlResponseHandler.onLockoutCleared();
     }
+
+    <T extends BaseClientMonitor> void unsupportedClientScheduled(Class<T> className) {
+        mAidlResponseHandler.onUnsupportedClientScheduled(className);
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
new file mode 100644
index 0000000..0bb61415
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.biometrics.sensors.fingerprint.hidl;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.hardware.biometrics.fingerprint.SensorProps;
+import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
+import android.os.Handler;
+import android.os.IHwBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.ClientMonitorCallback;
+import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.StartUserClient;
+import com.android.server.biometrics.sensors.StopUserClient;
+import com.android.server.biometrics.sensors.UserAwareBiometricScheduler;
+import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils;
+import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
+import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler;
+import com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession;
+import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider;
+import com.android.server.biometrics.sensors.fingerprint.aidl.Sensor;
+
+import java.util.ArrayList;
+
+/**
+ * Convert HIDL sensor configurations to an AIDL Sensor.
+ */
+public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRecipient {
+    private static final String TAG = "HidlToAidlSensorAdapter";
+
+    private final Runnable mInternalCleanupRunnable;
+    private final LockoutResetDispatcher mLockoutResetDispatcher;
+    private LockoutFrameworkImpl mLockoutTracker;
+    private final AuthSessionCoordinator mAuthSessionCoordinator;
+    private final AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback;
+    private int mCurrentUserId = UserHandle.USER_NULL;
+    private IBiometricsFingerprint mDaemon;
+    private AidlSession mSession;
+
+    private final StartUserClient.UserStartedCallback<AidlSession> mUserStartedCallback =
+            (newUserId, newUser, halInterfaceVersion) -> {
+                if (mCurrentUserId != newUserId) {
+                    handleUserChanged(newUserId);
+                }
+            };
+
+    public HidlToAidlSensorAdapter(@NonNull String tag, @NonNull FingerprintProvider provider,
+            @NonNull Context context, @NonNull Handler handler,
+            @NonNull SensorProps prop,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresHardwareAuthToken,
+            @NonNull Runnable internalCleanupRunnable) {
+        this(tag, provider, context, handler, prop, lockoutResetDispatcher,
+                gestureAvailabilityDispatcher, biometricContext,
+                resetLockoutRequiresHardwareAuthToken, internalCleanupRunnable,
+                new AuthSessionCoordinator(), null /* daemon */,
+                null /* onEnrollSuccessCallback */);
+    }
+
+    @VisibleForTesting
+    HidlToAidlSensorAdapter(@NonNull String tag, @NonNull FingerprintProvider provider,
+            @NonNull Context context, @NonNull Handler handler,
+            @NonNull SensorProps prop,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresHardwareAuthToken,
+            @NonNull Runnable internalCleanupRunnable,
+            @NonNull AuthSessionCoordinator authSessionCoordinator,
+            @Nullable IBiometricsFingerprint daemon,
+            @Nullable AidlResponseHandler.AidlResponseHandlerCallback aidlResponseHandlerCallback) {
+        super(tag, provider, context, handler, getFingerprintSensorPropertiesInternal(prop,
+                        new ArrayList<>(), resetLockoutRequiresHardwareAuthToken),
+                lockoutResetDispatcher,
+                gestureAvailabilityDispatcher,
+                biometricContext, null /* session */);
+        mLockoutResetDispatcher = lockoutResetDispatcher;
+        mInternalCleanupRunnable = internalCleanupRunnable;
+        mAuthSessionCoordinator = authSessionCoordinator;
+        mDaemon = daemon;
+        mAidlResponseHandlerCallback = aidlResponseHandlerCallback == null
+                ? new AidlResponseHandler.AidlResponseHandlerCallback() {
+                    @Override
+                    public void onEnrollSuccess() {
+                        getScheduler()
+                                .scheduleClientMonitor(getFingerprintUpdateActiveUserClient(
+                                        mCurrentUserId, true /* forceUpdateAuthenticatorIds */));
+                    }
+
+                    @Override
+                    public void onHardwareUnavailable() {
+                        mDaemon = null;
+                        mSession = null;
+                        mCurrentUserId = UserHandle.USER_NULL;
+                    }
+                } : aidlResponseHandlerCallback;
+    }
+
+    @Override
+    public void serviceDied(long cookie) {
+        Slog.d(TAG, "HAL died.");
+        mSession = null;
+        mDaemon = null;
+    }
+
+    @Override
+    @LockoutTracker.LockoutMode
+    public int getLockoutModeForUser(int userId) {
+        return mLockoutTracker.getLockoutModeForUser(userId);
+    }
+
+    @Override
+    public void init(GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            LockoutResetDispatcher lockoutResetDispatcher) {
+        setLazySession(this::getSession);
+        setScheduler(new UserAwareBiometricScheduler(TAG,
+                BiometricScheduler.sensorTypeFromFingerprintProperties(getSensorProperties()),
+                gestureAvailabilityDispatcher, () -> mCurrentUserId, getUserSwitchCallback()));
+        mLockoutTracker = new LockoutFrameworkImpl(getContext(),
+                userId -> mLockoutResetDispatcher.notifyLockoutResetCallbacks(
+                        getSensorProperties().sensorId));
+    }
+
+    @Override
+    @Nullable
+    protected AidlSession getSessionForUser(int userId) {
+        if (mSession != null && mSession.getUserId() == userId) {
+            return mSession;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    protected boolean isHardwareDetected(String halInstance) {
+        return getIBiometricsFingerprint() != null;
+    }
+
+    @NonNull
+    @Override
+    protected LockoutTracker getLockoutTracker(boolean forAuth) {
+        return mLockoutTracker;
+    }
+
+    private synchronized AidlSession getSession() {
+        if (mSession != null && mDaemon != null) {
+            return mSession;
+        } else {
+            return mSession = new AidlSession(this::getIBiometricsFingerprint,
+                    mCurrentUserId, getAidlResponseHandler());
+        }
+    }
+
+    private AidlResponseHandler getAidlResponseHandler() {
+        return new AidlResponseHandler(getContext(),
+                getScheduler(),
+                getSensorProperties().sensorId,
+                mCurrentUserId,
+                mLockoutTracker,
+                mLockoutResetDispatcher,
+                mAuthSessionCoordinator,
+                () -> {}, mAidlResponseHandlerCallback);
+    }
+
+    @VisibleForTesting IBiometricsFingerprint getIBiometricsFingerprint() {
+        if (getProvider().getTestHalEnabled()) {
+            final TestHal testHal = new TestHal(getContext(), getSensorProperties().sensorId);
+            testHal.setNotify(new HidlToAidlCallbackConverter(getAidlResponseHandler()));
+            return testHal;
+        }
+
+        if (mDaemon != null) {
+            return mDaemon;
+        }
+
+        try {
+            mDaemon = IBiometricsFingerprint.getService();
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to get fingerprint HAL", e);
+        } catch (java.util.NoSuchElementException e) {
+            // Service doesn't exist or cannot be opened.
+            Slog.w(TAG, "NoSuchElementException", e);
+        }
+
+        if (mDaemon == null) {
+            Slog.w(TAG, "Fingerprint HAL not available");
+            return null;
+        }
+
+        mDaemon.asBinder().linkToDeath(this, 0 /* flags */);
+
+        Slog.d(TAG, "Fingerprint HAL ready");
+
+        scheduleLoadAuthenticatorIds();
+        mInternalCleanupRunnable.run();
+        return mDaemon;
+    }
+
+    private UserAwareBiometricScheduler.UserSwitchCallback getUserSwitchCallback() {
+        return new UserAwareBiometricScheduler.UserSwitchCallback() {
+            @NonNull
+            @Override
+            public StopUserClient<?> getStopUserClient(int userId) {
+                return new StopUserClient<IBiometricsFingerprint>(getContext(),
+                        HidlToAidlSensorAdapter.this::getIBiometricsFingerprint,
+                        null /* token */, userId, getSensorProperties().sensorId,
+                        BiometricLogger.ofUnknown(getContext()), getBiometricContext(),
+                        () -> {
+                            mCurrentUserId = UserHandle.USER_NULL;
+                            mSession = null;
+                        }) {
+                    @Override
+                    public void start(@NonNull ClientMonitorCallback callback) {
+                        super.start(callback);
+                        startHalOperation();
+                    }
+
+                    @Override
+                    protected void startHalOperation() {
+                        onUserStopped();
+                    }
+
+                    @Override
+                    public void unableToStart() {
+                        getCallback().onClientFinished(this, false /* success */);
+                    }
+                };
+            }
+
+            @NonNull
+            @Override
+            public StartUserClient<?, ?> getStartUserClient(int newUserId) {
+                return getFingerprintUpdateActiveUserClient(newUserId,
+                        false /* forceUpdateAuthenticatorId */);
+            }
+        };
+    }
+
+    private FingerprintUpdateActiveUserClient getFingerprintUpdateActiveUserClient(int newUserId,
+            boolean forceUpdateAuthenticatorIds) {
+        return new FingerprintUpdateActiveUserClient(getContext(),
+                this::getIBiometricsFingerprint, newUserId, TAG,
+                getSensorProperties().sensorId, BiometricLogger.ofUnknown(getContext()),
+                getBiometricContext(), () -> mCurrentUserId,
+                !FingerprintUtils.getInstance(getSensorProperties().sensorId)
+                        .getBiometricsForUser(getContext(),
+                newUserId).isEmpty(), getAuthenticatorIds(), forceUpdateAuthenticatorIds,
+                mUserStartedCallback);
+    }
+
+    private void scheduleLoadAuthenticatorIds() {
+        getHandler().post(() -> {
+            for (UserInfo user : UserManager.get(getContext()).getAliveUsers()) {
+                final int targetUserId = user.id;
+                if (!getAuthenticatorIds().containsKey(targetUserId)) {
+                    getScheduler().scheduleClientMonitor(getFingerprintUpdateActiveUserClient(
+                            targetUserId, true /* forceUpdateAuthenticatorIds */));
+                }
+            }
+        });
+    }
+
+    @VisibleForTesting void handleUserChanged(int newUserId) {
+        Slog.d(TAG, "User changed. Current user is " + newUserId);
+        mSession = null;
+        mCurrentUserId = newUserId;
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java
similarity index 73%
rename from services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapter.java
rename to services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java
index b48d232..2fc00e1 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java
@@ -25,20 +25,25 @@
 import android.hardware.keymaster.HardwareAuthToken;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.util.Log;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.HardwareAuthTokenUtils;
 import com.android.server.biometrics.sensors.fingerprint.UdfpsHelper;
 import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler;
+import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintGetAuthenticatorIdClient;
+import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintInvalidationClient;
 
 import java.util.function.Supplier;
 
 /**
- * Adapter to convert AIDL-specific interface {@link ISession} methods to HIDL implementation.
+ * Adapter to convert HIDL methods into AIDL interface {@link ISession}.
  */
-public class AidlToHidlAdapter implements ISession {
-    private final String TAG = "AidlToHidlAdapter";
+public class HidlToAidlSessionAdapter implements ISession {
+
+    private final String TAG = "HidlToAidlSessionAdapter";
+
     @VisibleForTesting
     static final int ENROLL_TIMEOUT_SEC = 60;
     @NonNull
@@ -46,22 +51,13 @@
     private final int mUserId;
     private HidlToAidlCallbackConverter mHidlToAidlCallbackConverter;
 
-    public AidlToHidlAdapter(Supplier<IBiometricsFingerprint> session, int userId,
+    public HidlToAidlSessionAdapter(Supplier<IBiometricsFingerprint> session, int userId,
             AidlResponseHandler aidlResponseHandler) {
         mSession = session;
         mUserId = userId;
         setCallback(aidlResponseHandler);
     }
 
-    private void setCallback(AidlResponseHandler aidlResponseHandler) {
-        mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler);
-        try {
-            mSession.get().setNotify(mHidlToAidlCallbackConverter);
-        } catch (RemoteException e) {
-            Slog.d(TAG, "Failed to set callback");
-        }
-    }
-
     @Override
     public IBinder asBinder() {
         return null;
@@ -125,12 +121,16 @@
 
     @Override
     public void getAuthenticatorId() throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "getAuthenticatorId unsupported in HIDL");
+        mHidlToAidlCallbackConverter.unsupportedClientScheduled(
+                FingerprintGetAuthenticatorIdClient.class);
     }
 
     @Override
     public void invalidateAuthenticatorId() throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "invalidateAuthenticatorId unsupported in HIDL");
+        mHidlToAidlCallbackConverter.unsupportedClientScheduled(
+                FingerprintInvalidationClient.class);
     }
 
     @Override
@@ -140,72 +140,92 @@
 
     @Override
     public void close() throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "close unsupported in HIDL");
     }
 
     @Override
     public void onUiReady() throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "onUiReady unsupported in HIDL");
     }
 
     @Override
     public ICancellationSignal authenticateWithContext(long operationId, OperationContext context)
             throws RemoteException {
-        //Unsupported in HIDL
-        return null;
+        Log.e(TAG, "authenticateWithContext unsupported in HIDL");
+        return authenticate(operationId);
     }
 
     @Override
     public ICancellationSignal enrollWithContext(HardwareAuthToken hat, OperationContext context)
             throws RemoteException {
-        //Unsupported in HIDL
-        return null;
+        Log.e(TAG, "enrollWithContext unsupported in HIDL");
+        return enroll(hat);
     }
 
     @Override
     public ICancellationSignal detectInteractionWithContext(OperationContext context)
             throws RemoteException {
-        //Unsupported in HIDL
-        return null;
+        Log.e(TAG, "enrollWithContext unsupported in HIDL");
+        return detectInteraction();
     }
 
     @Override
     public void onPointerDownWithContext(PointerContext context) throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "onPointerDownWithContext unsupported in HIDL");
+        onPointerDown(context.pointerId, (int) context.x, (int) context.y, context.minor,
+                context.major);
     }
 
     @Override
     public void onPointerUpWithContext(PointerContext context) throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "onPointerUpWithContext unsupported in HIDL");
+        onPointerUp(context.pointerId);
     }
 
     @Override
     public void onContextChanged(OperationContext context) throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "onContextChanged unsupported in HIDL");
     }
 
     @Override
     public void onPointerCancelWithContext(PointerContext context) throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "onPointerCancelWithContext unsupported in HIDL");
     }
 
     @Override
     public void setIgnoreDisplayTouches(boolean shouldIgnore) throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "setIgnoreDisplayTouches unsupported in HIDL");
     }
 
     @Override
     public int getInterfaceVersion() throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "getInterfaceVersion unsupported in HIDL");
         return 0;
     }
 
     @Override
     public String getInterfaceHash() throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "getInterfaceHash unsupported in HIDL");
         return null;
     }
 
+    private void setCallback(AidlResponseHandler aidlResponseHandler) {
+        mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler);
+        try {
+            if (mSession.get() != null) {
+                long halId = mSession.get().setNotify(mHidlToAidlCallbackConverter);
+                Slog.d(TAG, "Fingerprint HAL ready, HAL ID: " + halId);
+                if (halId == 0) {
+                    Slog.d(TAG, "Unable to set HIDL callback.");
+                }
+            } else {
+                Slog.e(TAG, "Unable to set HIDL callback. HIDL daemon is null.");
+            }
+        } catch (RemoteException e) {
+            Slog.d(TAG, "Failed to set callback");
+        }
+    }
+
     private class Cancellation extends ICancellationSignal.Stub {
 
         Cancellation() {}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
index 0730c67..2f77275 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.RESET_FINGERPRINT_LOCKOUT;
 
+import android.annotation.NonNull;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -31,15 +32,18 @@
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.sensors.LockoutTracker;
 
+import java.util.function.Function;
+
 /**
  * Tracks and enforces biometric lockout for biometric sensors that do not support lockout in the
  * HAL.
  */
 public class LockoutFrameworkImpl implements LockoutTracker {
 
-    private static final String TAG = "LockoutTracker";
+    private static final String TAG = "LockoutFrameworkImpl";
     private static final String ACTION_LOCKOUT_RESET =
             "com.android.server.biometrics.sensors.fingerprint.ACTION_LOCKOUT_RESET";
     private static final int MAX_FAILED_ATTEMPTS_LOCKOUT_TIMED = 5;
@@ -65,22 +69,32 @@
         void onLockoutReset(int userId);
     }
 
-    private final Context mContext;
     private final LockoutResetCallback mLockoutResetCallback;
     private final SparseBooleanArray mTimedLockoutCleared;
     private final SparseIntArray mFailedAttempts;
     private final AlarmManager mAlarmManager;
     private final LockoutReceiver mLockoutReceiver;
     private final Handler mHandler;
+    private final Function<Integer, PendingIntent> mLockoutResetIntent;
 
-    public LockoutFrameworkImpl(Context context, LockoutResetCallback lockoutResetCallback) {
-        mContext = context;
+    public LockoutFrameworkImpl(@NonNull Context context,
+            @NonNull LockoutResetCallback lockoutResetCallback) {
+        this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId,
+                new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId),
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
+    }
+
+    @VisibleForTesting
+    LockoutFrameworkImpl(@NonNull Context context,
+            @NonNull LockoutResetCallback lockoutResetCallback,
+            @NonNull Function<Integer, PendingIntent> lockoutResetIntent) {
         mLockoutResetCallback = lockoutResetCallback;
         mTimedLockoutCleared = new SparseBooleanArray();
         mFailedAttempts = new SparseIntArray();
         mAlarmManager = context.getSystemService(AlarmManager.class);
         mLockoutReceiver = new LockoutReceiver();
         mHandler = new Handler(Looper.getMainLooper());
+        mLockoutResetIntent = lockoutResetIntent;
 
         context.registerReceiver(mLockoutReceiver, new IntentFilter(ACTION_LOCKOUT_RESET),
                 RESET_FINGERPRINT_LOCKOUT, null /* handler */, Context.RECEIVER_EXPORTED);
@@ -129,34 +143,18 @@
         return LOCKOUT_NONE;
     }
 
-    /**
-     * Clears lockout for Fingerprint HIDL HAL
-     */
     @Override
-    public void setLockoutModeForUser(int userId, int mode) {
-        mFailedAttempts.put(userId, 0);
-        mTimedLockoutCleared.put(userId, true);
-        // If we're asked to reset failed attempts externally (i.e. from Keyguard),
-        // the alarm might still be pending; remove it.
-        cancelLockoutResetForUser(userId);
-        mLockoutResetCallback.onLockoutReset(userId);
-    }
+    public void setLockoutModeForUser(int userId, int mode) {}
 
     private void cancelLockoutResetForUser(int userId) {
-        mAlarmManager.cancel(getLockoutResetIntentForUser(userId));
+        mAlarmManager.cancel(mLockoutResetIntent.apply(userId));
     }
 
     private void scheduleLockoutResetForUser(int userId) {
         mHandler.post(() -> {
             mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
-                SystemClock.elapsedRealtime() + FAIL_LOCKOUT_TIMEOUT_MS,
-                getLockoutResetIntentForUser(userId));
+                    SystemClock.elapsedRealtime() + FAIL_LOCKOUT_TIMEOUT_MS,
+                    mLockoutResetIntent.apply(userId));
         });
     }
-
-    private PendingIntent getLockoutResetIntentForUser(int userId) {
-        return PendingIntent.getBroadcast(mContext, userId,
-                new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId),
-                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-    }
 }
diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
index acd253b..f2ffd4d 100644
--- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
+++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
@@ -35,7 +35,6 @@
 import android.util.Spline;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.display.BrightnessUtils;
 import com.android.internal.util.Preconditions;
 import com.android.server.display.utils.Plog;
@@ -97,28 +96,17 @@
         float[] brightnessLevels = null;
         float[] luxLevels = null;
         switch (mode) {
-            case AUTO_BRIGHTNESS_MODE_DEFAULT:
+            case AUTO_BRIGHTNESS_MODE_DEFAULT -> {
                 brightnessLevelsNits = displayDeviceConfig.getAutoBrightnessBrighteningLevelsNits();
                 luxLevels = displayDeviceConfig.getAutoBrightnessBrighteningLevelsLux();
-
                 brightnessLevels = displayDeviceConfig.getAutoBrightnessBrighteningLevels();
-                if (brightnessLevels == null || brightnessLevels.length == 0) {
-                    // Load the old configuration in the range [0, 255]. The values need to be
-                    // normalized to the range [0, 1].
-                    int[] brightnessLevelsInt = resources.getIntArray(
-                            com.android.internal.R.array.config_autoBrightnessLcdBacklightValues);
-                    brightnessLevels = new float[brightnessLevelsInt.length];
-                    for (int i = 0; i < brightnessLevels.length; i++) {
-                        brightnessLevels[i] = normalizeAbsoluteBrightness(brightnessLevelsInt[i]);
-                    }
-                }
-                break;
-            case AUTO_BRIGHTNESS_MODE_IDLE:
+            }
+            case AUTO_BRIGHTNESS_MODE_IDLE -> {
                 brightnessLevelsNits = getFloatArray(resources.obtainTypedArray(
                         com.android.internal.R.array.config_autoBrightnessDisplayValuesNitsIdle));
                 luxLevels = getLuxLevels(resources.getIntArray(
                         com.android.internal.R.array.config_autoBrightnessLevelsIdle));
-                break;
+            }
         }
 
         // Display independent, mode independent values
@@ -426,11 +414,6 @@
         }
     }
 
-    // Normalize entire brightness range to 0 - 1.
-    protected static float normalizeAbsoluteBrightness(int brightness) {
-        return BrightnessSynchronizer.brightnessIntToFloat(brightness);
-    }
-
     private Pair<float[], float[]> insertControlPoint(
             float[] luxLevels, float[] brightnessLevels, float lux, float brightness) {
         final int idx = findInsertionPoint(luxLevels, lux);
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index d97127c..7d22a87 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -49,6 +49,7 @@
 import com.android.server.display.config.BrightnessThrottlingMap;
 import com.android.server.display.config.BrightnessThrottlingPoint;
 import com.android.server.display.config.Density;
+import com.android.server.display.config.DisplayBrightnessMappingConfig;
 import com.android.server.display.config.DisplayBrightnessPoint;
 import com.android.server.display.config.DisplayConfiguration;
 import com.android.server.display.config.DisplayQuirks;
@@ -57,7 +58,6 @@
 import com.android.server.display.config.HighBrightnessMode;
 import com.android.server.display.config.IntegerArray;
 import com.android.server.display.config.LuxThrottling;
-import com.android.server.display.config.LuxToBrightnessMapping;
 import com.android.server.display.config.NitsMap;
 import com.android.server.display.config.NonNegativeFloatToFloatPoint;
 import com.android.server.display.config.Point;
@@ -313,6 +313,21 @@
  *              1000
  *          </darkeningLightDebounceIdleMillis>
  *          <luxToBrightnessMapping>
+ *            <mode>default</mode>
+ *            <map>
+ *              <point>
+ *                <first>0</first>
+ *                <second>0.2</second>
+ *              </point>
+ *              <point>
+ *                <first>80</first>
+ *                <second>0.3</second>
+ *              </point>
+ *            </map>
+ *          </luxToBrightnessMapping>
+ *          <luxToBrightnessMapping>
+ *            <mode>doze</mode>
+ *            <setting>dim</setting>
  *            <map>
  *              <point>
  *                <first>0</first>
@@ -634,36 +649,8 @@
     // for the corresponding values above
     private float[] mBrightness;
 
-    /**
-     * Array of desired screen brightness in nits corresponding to the lux values
-     * in the mBrightnessLevelsLux array. The display brightness is defined as the
-     * measured brightness of an all-white image. The brightness values must be non-negative and
-     * non-decreasing. This must be overridden in platform specific overlays
-     */
-    private float[] mBrightnessLevelsNits;
-
-    /**
-     * Array of desired screen brightness corresponding to the lux values
-     * in the mBrightnessLevelsLux array. The brightness values must be non-negative and
-     * non-decreasing. They must be between {@link PowerManager.BRIGHTNESS_MIN} and
-     * {@link PowerManager.BRIGHTNESS_MAX}. This must be overridden in platform specific overlays
-     */
-    private float[] mBrightnessLevels;
-
-    /**
-     * Array of light sensor lux values to define our levels for auto-brightness support.
-     *
-     * The first lux value is always 0.
-     *
-     * The control points must be strictly increasing. Each control point corresponds to an entry
-     * in the brightness values arrays. For example, if lux == luxLevels[1] (second element
-     * of the levels array) then the brightness will be determined by brightnessLevels[1] (second
-     * element of the brightness values array).
-     *
-     * Spline interpolation is used to determine the auto-brightness values for lux levels between
-     * these control points.
-     */
-    private float[] mBrightnessLevelsLux;
+    @Nullable
+    private DisplayBrightnessMappingConfig mDisplayBrightnessMapping;
 
     private float mBacklightMinimum = Float.NaN;
     private float mBacklightMaximum = Float.NaN;
@@ -1604,24 +1591,57 @@
     }
 
     /**
-     * @return Auto brightness brightening ambient lux levels
+     * @return The default auto-brightness brightening ambient lux levels
      */
     public float[] getAutoBrightnessBrighteningLevelsLux() {
-        return mBrightnessLevelsLux;
+        if (mDisplayBrightnessMapping == null) {
+            return null;
+        }
+        return mDisplayBrightnessMapping.getLuxArray();
+    }
+
+    /**
+     * @param mode The auto-brightness mode
+     * @param setting The brightness setting
+     * @return Auto brightness brightening ambient lux levels for the specified mode and setting
+     */
+    public float[] getAutoBrightnessBrighteningLevelsLux(String mode, String setting) {
+        if (mDisplayBrightnessMapping == null) {
+            return null;
+        }
+        return mDisplayBrightnessMapping.getLuxArray(mode, setting);
     }
 
     /**
      * @return Auto brightness brightening nits levels
      */
     public float[] getAutoBrightnessBrighteningLevelsNits() {
-        return mBrightnessLevelsNits;
+        if (mDisplayBrightnessMapping == null) {
+            return null;
+        }
+        return mDisplayBrightnessMapping.getNitsArray();
     }
 
     /**
-     * @return Auto brightness brightening levels
+     * @return The default auto-brightness brightening levels
      */
     public float[] getAutoBrightnessBrighteningLevels() {
-        return mBrightnessLevels;
+        if (mDisplayBrightnessMapping == null) {
+            return null;
+        }
+        return mDisplayBrightnessMapping.getBrightnessArray();
+    }
+
+    /**
+     * @param mode The auto-brightness mode
+     * @param setting The brightness setting
+     * @return Auto brightness brightening backlight levels for the specified mode and setting
+     */
+    public float[] getAutoBrightnessBrighteningLevels(String mode, String setting) {
+        if (mDisplayBrightnessMapping == null) {
+            return null;
+        }
+        return mDisplayBrightnessMapping.getBrightnessArray(mode, setting);
     }
 
     /**
@@ -1875,9 +1895,7 @@
                 + mAutoBrightnessBrighteningLightDebounceIdle
                 + ", mAutoBrightnessDarkeningLightDebounceIdle= "
                 + mAutoBrightnessDarkeningLightDebounceIdle
-                + ", mBrightnessLevelsLux= " + Arrays.toString(mBrightnessLevelsLux)
-                + ", mBrightnessLevelsNits= " + Arrays.toString(mBrightnessLevelsNits)
-                + ", mBrightnessLevels= " + Arrays.toString(mBrightnessLevels)
+                + ", mDisplayBrightnessMapping= " + mDisplayBrightnessMapping
                 + ", mDdcAutoBrightnessAvailable= " + mDdcAutoBrightnessAvailable
                 + ", mAutoBrightnessAvailable= " + mAutoBrightnessAvailable
                 + "\n"
@@ -2568,7 +2586,8 @@
         // Idle must be called after interactive, since we fall back to it if needed.
         loadAutoBrightnessBrighteningLightDebounceIdle(autoBrightness);
         loadAutoBrightnessDarkeningLightDebounceIdle(autoBrightness);
-        loadAutoBrightnessDisplayBrightnessMapping(autoBrightness);
+        mDisplayBrightnessMapping = new DisplayBrightnessMappingConfig(mContext, mFlags,
+                autoBrightness, mBacklightToBrightnessSpline);
         loadEnableAutoBrightness(autoBrightness);
     }
 
@@ -2633,38 +2652,6 @@
         }
     }
 
-    /**
-     * Loads the auto-brightness display brightness mappings. Internally, this takes care of
-     * loading the value from the display config, and if not present, falls back to config.xml.
-     */
-    private void loadAutoBrightnessDisplayBrightnessMapping(AutoBrightness autoBrightnessConfig) {
-        if (mFlags.areAutoBrightnessModesEnabled() && autoBrightnessConfig != null
-                && autoBrightnessConfig.getLuxToBrightnessMapping() != null) {
-            LuxToBrightnessMapping mapping = autoBrightnessConfig.getLuxToBrightnessMapping();
-            final int size = mapping.getMap().getPoint().size();
-            mBrightnessLevels = new float[size];
-            mBrightnessLevelsLux = new float[size];
-            for (int i = 0; i < size; i++) {
-                float backlight = mapping.getMap().getPoint().get(i).getSecond().floatValue();
-                mBrightnessLevels[i] = mBacklightToBrightnessSpline.interpolate(backlight);
-                mBrightnessLevelsLux[i] = mapping.getMap().getPoint().get(i).getFirst()
-                        .floatValue();
-            }
-            if (size > 0 && mBrightnessLevelsLux[0] != 0) {
-                throw new IllegalArgumentException(
-                        "The first lux value in the display brightness mapping must be 0");
-            }
-        } else {
-            mBrightnessLevelsNits = getFloatArray(mContext.getResources()
-                    .obtainTypedArray(com.android.internal.R.array
-                            .config_autoBrightnessDisplayValuesNits), PowerManager
-                    .BRIGHTNESS_OFF_FLOAT);
-            mBrightnessLevelsLux = getLuxLevels(mContext.getResources()
-                    .getIntArray(com.android.internal.R.array
-                            .config_autoBrightnessLevels));
-        }
-    }
-
     private void loadAutoBrightnessAvailableFromConfigXml() {
         mAutoBrightnessAvailable = mContext.getResources().getBoolean(
                 R.bool.config_automatic_brightness_available);
@@ -2977,7 +2964,8 @@
     }
 
     private void loadAutoBrightnessConfigsFromConfigXml() {
-        loadAutoBrightnessDisplayBrightnessMapping(null /*AutoBrightnessConfig*/);
+        mDisplayBrightnessMapping = new DisplayBrightnessMappingConfig(mContext, mFlags,
+                /* autoBrightnessConfig= */ null, mBacklightToBrightnessSpline);
     }
 
     private void loadBrightnessChangeThresholdsFromXml() {
@@ -3347,7 +3335,12 @@
         return vals;
     }
 
-    private static float[] getLuxLevels(int[] lux) {
+    /**
+     * @param lux The lux array
+     * @return The lux array with 0 appended at the beginning - the first lux value should always
+     * be 0
+     */
+    public static float[] getLuxLevels(int[] lux) {
         // The first control point is implicit and always at 0 lux.
         float[] levels = new float[lux.length + 1];
         for (int i = 0; i < lux.length; i++) {
diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
index 1bd556b..4e341a9 100644
--- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
+++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
@@ -56,6 +56,15 @@
         }
     }
 
+    @Override
+    public boolean blockScreenOn(Runnable unblocker) {
+        if (mDisplayOffloader == null) {
+            return false;
+        }
+        mDisplayOffloader.onBlockingScreenOn(unblocker);
+        return true;
+    }
+
     /**
      * Start the offload session. The method returns if the session is already active.
      * @return Whether the session was started successfully
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 52c53f3..6d09cc9 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -126,6 +126,8 @@
     // To enable these logs, run:
     // 'adb shell setprop persist.log.tag.DisplayPowerController2 DEBUG && adb reboot'
     private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+    private static final String SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME =
+            "Screen on blocked by displayoffload";
 
     // If true, uses the color fade on animation.
     // We might want to turn this off if we cannot get a guarantee that the screen
@@ -155,6 +157,7 @@
     private static final int MSG_SET_DWBC_COLOR_OVERRIDE = 15;
     private static final int MSG_SET_DWBC_LOGGING_ENABLED = 16;
     private static final int MSG_SET_BRIGHTNESS_FROM_OFFLOAD = 17;
+    private static final int MSG_OFFLOADING_SCREEN_ON_UNBLOCKED = 18;
 
 
 
@@ -339,6 +342,7 @@
     // we are waiting for a callback to release it and unblock the screen.
     private ScreenOnUnblocker mPendingScreenOnUnblocker;
     private ScreenOffUnblocker mPendingScreenOffUnblocker;
+    private Runnable mPendingScreenOnUnblockerByDisplayOffload;
 
     // True if we were in the process of turning off the screen.
     // This allows us to recover more gracefully from situations where we abort
@@ -348,10 +352,15 @@
     // The elapsed real time when the screen on was blocked.
     private long mScreenOnBlockStartRealTime;
     private long mScreenOffBlockStartRealTime;
+    private long mScreenOnBlockByDisplayOffloadStartRealTime;
 
     // Screen state we reported to policy. Must be one of REPORTED_TO_POLICY_* fields.
     private int mReportedScreenStateToPolicy = REPORTED_TO_POLICY_UNREPORTED;
 
+    // Used to deduplicate the displayoffload blocking screen on logic. One block per turning on.
+    // This value is reset when screen on is reported or the blocking is cancelled.
+    private boolean mScreenTurningOnWasBlockedByDisplayOffload;
+
     // If the last recorded screen state was dozing or not.
     private boolean mDozing;
 
@@ -472,7 +481,7 @@
     private boolean mBootCompleted;
     private final DisplayManagerFlags mFlags;
 
-    private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
+    private DisplayOffloadSession mDisplayOffloadSession;
 
     /**
      * Creates the display power controller.
@@ -772,6 +781,10 @@
 
     @Override
     public void setDisplayOffloadSession(DisplayOffloadSession session) {
+        if (session == mDisplayOffloadSession) {
+            return;
+        }
+        unblockScreenOnByDisplayOffload();
         mDisplayOffloadSession = session;
     }
 
@@ -1735,6 +1748,7 @@
         // reporting the display is ready because we only need to ensure the screen is in the
         // right power state even as it continues to converge on the desired brightness.
         final boolean ready = mPendingScreenOnUnblocker == null
+                && mPendingScreenOnUnblockerByDisplayOffload == null
                 && (!mColorFadeEnabled || (!mColorFadeOnAnimator.isStarted()
                         && !mColorFadeOffAnimator.isStarted()))
                 && mPowerState.waitUntilClean(mCleanListener);
@@ -1983,15 +1997,69 @@
         }
     }
 
+    private void blockScreenOnByDisplayOffload(DisplayOffloadSession displayOffloadSession) {
+        if (mPendingScreenOnUnblockerByDisplayOffload != null || displayOffloadSession == null) {
+            return;
+        }
+        mScreenTurningOnWasBlockedByDisplayOffload = true;
+
+        Trace.asyncTraceBegin(
+                Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0);
+        mScreenOnBlockByDisplayOffloadStartRealTime = SystemClock.elapsedRealtime();
+
+        mPendingScreenOnUnblockerByDisplayOffload =
+                () -> onDisplayOffloadUnblockScreenOn(displayOffloadSession);
+        if (!displayOffloadSession.blockScreenOn(mPendingScreenOnUnblockerByDisplayOffload)) {
+            mPendingScreenOnUnblockerByDisplayOffload = null;
+            long delay =
+                    SystemClock.elapsedRealtime() - mScreenOnBlockByDisplayOffloadStartRealTime;
+            Slog.w(mTag, "Tried blocking screen on for offloading but failed. So, end trace after "
+                    + delay + " ms.");
+            Trace.asyncTraceEnd(
+                    Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0);
+            return;
+        }
+        Slog.i(mTag, "Blocking screen on for offloading.");
+    }
+
+    private void onDisplayOffloadUnblockScreenOn(DisplayOffloadSession displayOffloadSession) {
+        Message msg = mHandler.obtainMessage(MSG_OFFLOADING_SCREEN_ON_UNBLOCKED,
+                displayOffloadSession);
+        mHandler.sendMessage(msg);
+    }
+
+    private void unblockScreenOnByDisplayOffload() {
+        if (mPendingScreenOnUnblockerByDisplayOffload == null) {
+            return;
+        }
+        mPendingScreenOnUnblockerByDisplayOffload = null;
+        long delay = SystemClock.elapsedRealtime() - mScreenOnBlockByDisplayOffloadStartRealTime;
+        Slog.i(mTag, "Unblocked screen on for offloading after " + delay + " ms");
+        Trace.asyncTraceEnd(
+                Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0);
+    }
+
     private boolean setScreenState(int state) {
         return setScreenState(state, false /*reportOnly*/);
     }
 
     private boolean setScreenState(int state, boolean reportOnly) {
         final boolean isOff = (state == Display.STATE_OFF);
+        final boolean isOn = (state == Display.STATE_ON);
+        final boolean changed = mPowerState.getScreenState() != state;
 
-        if (mPowerState.getScreenState() != state
-                || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) {
+        // If the screen is turning on, give displayoffload a chance to do something before the
+        // screen actually turns on.
+        // TODO(b/316941732): add tests for this displayoffload screen-on blocker.
+        if (isOn && changed && !mScreenTurningOnWasBlockedByDisplayOffload) {
+            blockScreenOnByDisplayOffload(mDisplayOffloadSession);
+        } else if (!isOn && mScreenTurningOnWasBlockedByDisplayOffload) {
+            // No longer turning screen on, so unblock previous screen on blocking immediately.
+            unblockScreenOnByDisplayOffload();
+            mScreenTurningOnWasBlockedByDisplayOffload = false;
+        }
+
+        if (changed || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) {
             // If we are trying to turn screen off, give policy a chance to do something before we
             // actually turn the screen off.
             if (isOff && !mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
@@ -2007,8 +2075,9 @@
                 }
             }
 
-            if (!reportOnly && mPowerState.getScreenState() != state
-                    && readyToUpdateDisplayState()) {
+            if (!reportOnly && changed && readyToUpdateDisplayState()
+                    && mPendingScreenOffUnblocker == null
+                    && mPendingScreenOnUnblockerByDisplayOffload == null) {
                 Trace.traceCounter(Trace.TRACE_TAG_POWER, "ScreenState", state);
 
                 String propertyKey = "debug.tracing.screen_state";
@@ -2060,12 +2129,16 @@
         }
 
         // Return true if the screen isn't blocked.
-        return mPendingScreenOnUnblocker == null;
+        return mPendingScreenOnUnblocker == null
+                && mPendingScreenOnUnblockerByDisplayOffload == null;
     }
 
     private void setReportedScreenState(int state) {
         Trace.traceCounter(Trace.TRACE_TAG_POWER, "ReportedScreenStateToPolicy", state);
         mReportedScreenStateToPolicy = state;
+        if (state == REPORTED_TO_POLICY_SCREEN_ON) {
+            mScreenTurningOnWasBlockedByDisplayOffload = false;
+        }
     }
 
     private void loadAmbientLightSensor() {
@@ -2813,6 +2886,12 @@
                         updatePowerState();
                     }
                     break;
+                case MSG_OFFLOADING_SCREEN_ON_UNBLOCKED:
+                    if (mDisplayOffloadSession == msg.obj) {
+                        unblockScreenOnByDisplayOffload();
+                        updatePowerState();
+                    }
+                    break;
                 case MSG_CONFIGURE_BRIGHTNESS:
                     BrightnessConfiguration brightnessConfiguration =
                             (BrightnessConfiguration) msg.obj;
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 22898a6..25576ce 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -25,6 +25,7 @@
 import android.content.res.Resources;
 import android.hardware.display.DisplayManagerInternal.DisplayOffloadSession;
 import android.hardware.sidekick.SidekickInternal;
+import android.media.MediaDrm;
 import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
@@ -242,6 +243,10 @@
         private SurfaceControl.DisplayMode mActiveSfDisplayMode;
         // The active display vsync period in SurfaceFlinger
         private float mActiveRenderFrameRate;
+        // The current HDCP level supported by the display, 0 indicates unset
+        // values are defined in hardware/interfaces/drm/aidl/android/hardware/drm/HdcpLevel.aidl
+        private int mConnectedHdcpLevel;
+
         private DisplayEventReceiver.FrameRateOverride[] mFrameRateOverrides =
                 new DisplayEventReceiver.FrameRateOverride[0];
 
@@ -675,8 +680,9 @@
                 mInfo.yDpi = mActiveSfDisplayMode.yDpi;
                 mInfo.deviceProductInfo = mStaticDisplayInfo.deviceProductInfo;
 
-                // Assume that all built-in displays that have secure output (eg. HDCP) also
-                // support compositing from gralloc protected buffers.
+                if (mConnectedHdcpLevel != 0) {
+                    mStaticDisplayInfo.secure = mConnectedHdcpLevel >= MediaDrm.HDCP_V1;
+                }
                 if (mStaticDisplayInfo.secure) {
                     mInfo.flags = DisplayDeviceInfo.FLAG_SECURE
                             | DisplayDeviceInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS;
@@ -1093,6 +1099,12 @@
             }
         }
 
+        public void onHdcpLevelsChangedLocked(int connectedLevel, int maxLevel) {
+            if (updateHdcpLevelsLocked(connectedLevel, maxLevel)) {
+                updateDeviceInfoLocked();
+            }
+        }
+
         public boolean updateActiveModeLocked(int activeSfModeId, float renderFrameRate) {
             if (mActiveSfDisplayMode.id == activeSfModeId
                     && mActiveRenderFrameRate == renderFrameRate) {
@@ -1118,6 +1130,22 @@
             return true;
         }
 
+        public boolean updateHdcpLevelsLocked(int connectedLevel, int maxLevel) {
+            if (connectedLevel > maxLevel) {
+                Slog.w(TAG, "HDCP connected level: " + connectedLevel
+                        + " is larger than max level: " + maxLevel
+                        + ", ignoring request.");
+                return false;
+            }
+
+            if (mConnectedHdcpLevel == connectedLevel) {
+                return false;
+            }
+
+            mConnectedHdcpLevel = connectedLevel;
+            return true;
+        }
+
         public void requestColorModeLocked(int colorMode) {
             if (mActiveColorMode == colorMode) {
                 return;
@@ -1387,6 +1415,7 @@
                 long renderPeriod);
         void onFrameRateOverridesChanged(long timestampNanos, long physicalDisplayId,
                 DisplayEventReceiver.FrameRateOverride[] overrides);
+        void onHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, int maxLevel);
 
     }
 
@@ -1420,6 +1449,11 @@
                 DisplayEventReceiver.FrameRateOverride[] overrides) {
             mListener.onFrameRateOverridesChanged(timestampNanos, physicalDisplayId, overrides);
         }
+
+        @Override
+        public void onHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, int maxLevel) {
+            mListener.onHdcpLevelsChanged(physicalDisplayId, connectedLevel, maxLevel);
+        }
     }
 
     private final class LocalDisplayEventListener implements DisplayEventListener {
@@ -1489,6 +1523,26 @@
                 device.onFrameRateOverridesChanged(overrides);
             }
         }
+
+        @Override
+        public void onHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, int maxLevel) {
+            if (DEBUG) {
+                Slog.d(TAG, "onHdcpLevelsChanged(physicalDisplayId=" + physicalDisplayId
+                        + ", connectedLevel=" + connectedLevel + ", maxLevel=" + maxLevel + ")");
+            }
+            synchronized (getSyncRoot()) {
+                LocalDisplayDevice device = mDevices.get(physicalDisplayId);
+                if (device == null) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Received hdcp levels change for unhandled physical display: "
+                                + "physicalDisplayId=" + physicalDisplayId);
+                    }
+                    return;
+                }
+
+                device.onHdcpLevelsChangedLocked(connectedLevel, maxLevel);
+            }
+        }
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
new file mode 100644
index 0000000..2162850
--- /dev/null
+++ b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.config;
+
+import android.content.Context;
+import android.os.PowerManager;
+import android.util.Spline;
+
+import com.android.internal.display.BrightnessSynchronizer;
+import com.android.server.display.DisplayDeviceConfig;
+import com.android.server.display.feature.DisplayManagerFlags;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides a mapping between lux and brightness values in order to support auto-brightness.
+ */
+public class DisplayBrightnessMappingConfig {
+
+    private static final String DEFAULT_BRIGHTNESS_MAPPING_KEY = "default_normal";
+
+    /**
+     * Array of desired screen brightness in nits corresponding to the lux values
+     * in the mBrightnessLevelsLuxMap.get(DEFAULT_ID) array. The display brightness is defined as
+     * the measured brightness of an all-white image. The brightness values must be non-negative and
+     * non-decreasing. This must be overridden in platform specific overlays
+     */
+    private float[] mBrightnessLevelsNits;
+
+    /**
+     * Map of arrays of desired screen brightness corresponding to the lux values
+     * in mBrightnessLevelsLuxMap, indexed by the auto-brightness mode and the brightness setting.
+     * The brightness values must be non-negative and non-decreasing. They must be between
+     * {@link PowerManager.BRIGHTNESS_MIN} and {@link PowerManager.BRIGHTNESS_MAX}.
+     *
+     * The keys are a concatenation of the auto-brightness mode and the brightness setting
+     * separated by an underscore, e.g. default_normal, default_dim, default_bright, doze_normal,
+     * doze_dim, doze_bright.
+     */
+    private final Map<String, float[]> mBrightnessLevelsMap = new HashMap<>();
+
+    /**
+     * Map of arrays of light sensor lux values to define our levels for auto-brightness support,
+     * indexed by the auto-brightness mode and the brightness setting.
+     *
+     * The first lux value in every array is always 0.
+     *
+     * The control points must be strictly increasing. Each control point corresponds to an entry
+     * in the brightness values arrays. For example, if lux == luxLevels[1] (second element
+     * of the levels array) then the brightness will be determined by brightnessLevels[1] (second
+     * element of the brightness values array).
+     *
+     * Spline interpolation is used to determine the auto-brightness values for lux levels between
+     * these control points.
+     *
+     * The keys are a concatenation of the auto-brightness mode and the brightness setting
+     * separated by an underscore, e.g. default_normal, default_dim, default_bright, doze_normal,
+     * doze_dim, doze_bright.
+     */
+    private final Map<String, float[]> mBrightnessLevelsLuxMap = new HashMap<>();
+
+    /**
+     * Loads the auto-brightness display brightness mappings. Internally, this takes care of
+     * loading the value from the display config, and if not present, falls back to config.xml.
+     */
+    public DisplayBrightnessMappingConfig(Context context, DisplayManagerFlags flags,
+            AutoBrightness autoBrightnessConfig, Spline backlightToBrightnessSpline) {
+        if (flags.areAutoBrightnessModesEnabled() && autoBrightnessConfig != null
+                && autoBrightnessConfig.getLuxToBrightnessMapping() != null
+                && autoBrightnessConfig.getLuxToBrightnessMapping().size() > 0) {
+            for (LuxToBrightnessMapping mapping
+                    : autoBrightnessConfig.getLuxToBrightnessMapping()) {
+                final int size = mapping.getMap().getPoint().size();
+                float[] brightnessLevels = new float[size];
+                float[] brightnessLevelsLux = new float[size];
+                for (int i = 0; i < size; i++) {
+                    float backlight = mapping.getMap().getPoint().get(i).getSecond().floatValue();
+                    brightnessLevels[i] = backlightToBrightnessSpline.interpolate(backlight);
+                    brightnessLevelsLux[i] = mapping.getMap().getPoint().get(i).getFirst()
+                            .floatValue();
+                }
+                if (size == 0) {
+                    throw new IllegalArgumentException(
+                            "A display brightness mapping should not be empty");
+                }
+                if (brightnessLevelsLux[0] != 0) {
+                    throw new IllegalArgumentException(
+                            "The first lux value in the display brightness mapping must be 0");
+                }
+
+                String key = (mapping.getMode() == null ? "default" : mapping.getMode()) + "_"
+                        + (mapping.getSetting() == null ? "normal" : mapping.getSetting());
+                if (mBrightnessLevelsMap.containsKey(key)
+                        || mBrightnessLevelsLuxMap.containsKey(key)) {
+                    throw new IllegalArgumentException(
+                            "A display brightness mapping with key " + key + " already exists");
+                }
+                mBrightnessLevelsMap.put(key, brightnessLevels);
+                mBrightnessLevelsLuxMap.put(key, brightnessLevelsLux);
+            }
+        }
+
+        if (!mBrightnessLevelsMap.containsKey(DEFAULT_BRIGHTNESS_MAPPING_KEY)
+                || !mBrightnessLevelsLuxMap.containsKey(DEFAULT_BRIGHTNESS_MAPPING_KEY)) {
+            mBrightnessLevelsNits = DisplayDeviceConfig.getFloatArray(context.getResources()
+                    .obtainTypedArray(com.android.internal.R.array
+                            .config_autoBrightnessDisplayValuesNits), PowerManager
+                    .BRIGHTNESS_OFF_FLOAT);
+
+            float[] brightnessLevelsLux = DisplayDeviceConfig.getLuxLevels(context.getResources()
+                    .getIntArray(com.android.internal.R.array
+                            .config_autoBrightnessLevels));
+            mBrightnessLevelsLuxMap.put(DEFAULT_BRIGHTNESS_MAPPING_KEY, brightnessLevelsLux);
+
+            // Load the old configuration in the range [0, 255]. The values need to be normalized
+            // to the range [0, 1].
+            int[] brightnessLevels = context.getResources().getIntArray(
+                    com.android.internal.R.array.config_autoBrightnessLcdBacklightValues);
+            mBrightnessLevelsMap.put(DEFAULT_BRIGHTNESS_MAPPING_KEY,
+                    brightnessArrayIntToFloat(brightnessLevels, backlightToBrightnessSpline));
+        }
+    }
+
+    /**
+     * @return The default auto-brightness brightening ambient lux levels
+     */
+    public float[] getLuxArray() {
+        return mBrightnessLevelsLuxMap.get(DEFAULT_BRIGHTNESS_MAPPING_KEY);
+    }
+
+    /**
+     * @param mode The auto-brightness mode
+     * @param setting The brightness setting
+     * @return Auto brightness brightening ambient lux levels for the specified mode and setting
+     */
+    public float[] getLuxArray(String mode, String setting) {
+        return mBrightnessLevelsLuxMap.get(mode + "_" + setting);
+    }
+
+    /**
+     * @return Auto brightness brightening nits levels
+     */
+    public float[] getNitsArray() {
+        return mBrightnessLevelsNits;
+    }
+
+    /**
+     * @return The default auto-brightness brightening levels
+     */
+    public float[] getBrightnessArray() {
+        return mBrightnessLevelsMap.get(DEFAULT_BRIGHTNESS_MAPPING_KEY);
+    }
+
+    /**
+     * @param mode The auto-brightness mode
+     * @param setting The brightness setting
+     * @return Auto brightness brightening ambient lux levels for the specified mode and setting
+     */
+    public float[] getBrightnessArray(String mode, String setting) {
+        return mBrightnessLevelsMap.get(mode + "_" + setting);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder brightnessLevelsLuxMapString = new StringBuilder("{");
+        for (Map.Entry<String, float[]> entry : mBrightnessLevelsLuxMap.entrySet()) {
+            brightnessLevelsLuxMapString.append(entry.getKey()).append("=").append(
+                    Arrays.toString(entry.getValue())).append(", ");
+        }
+        if (brightnessLevelsLuxMapString.length() > 2) {
+            brightnessLevelsLuxMapString.delete(brightnessLevelsLuxMapString.length() - 2,
+                    brightnessLevelsLuxMapString.length());
+        }
+        brightnessLevelsLuxMapString.append("}");
+
+        StringBuilder brightnessLevelsMapString = new StringBuilder("{");
+        for (Map.Entry<String, float[]> entry : mBrightnessLevelsMap.entrySet()) {
+            brightnessLevelsMapString.append(entry.getKey()).append("=").append(
+                    Arrays.toString(entry.getValue())).append(", ");
+        }
+        if (brightnessLevelsMapString.length() > 2) {
+            brightnessLevelsMapString.delete(brightnessLevelsMapString.length() - 2,
+                    brightnessLevelsMapString.length());
+        }
+        brightnessLevelsMapString.append("}");
+
+        return "mBrightnessLevelsNits= " + Arrays.toString(mBrightnessLevelsNits)
+                + ", mBrightnessLevelsLuxMap= " + brightnessLevelsLuxMapString
+                + ", mBrightnessLevelsMap= " + brightnessLevelsMapString;
+    }
+
+    private float[] brightnessArrayIntToFloat(int[] brightnessInt,
+            Spline backlightToBrightnessSpline) {
+        float[] brightnessFloat = new float[brightnessInt.length];
+        for (int i = 0; i < brightnessInt.length; i++) {
+            brightnessFloat[i] = backlightToBrightnessSpline.interpolate(
+                    BrightnessSynchronizer.brightnessIntToFloat(brightnessInt[i]));
+        }
+        return brightnessFloat;
+    }
+}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index 4089a81..dda50ca 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -167,13 +167,17 @@
 
     /**
      * Indicates that the IME window has re-parented to the new target when the IME control changed.
+     *
+     * @param displayId the display hosting the IME window
      */
-    public abstract void onImeParentChanged();
+    public abstract void onImeParentChanged(int displayId);
 
     /**
-     * Destroys the IME surface.
+     * Destroys the IME surface for the given display.
+     *
+     * @param displayId the display hosting the IME window
      */
-    public abstract void removeImeSurface();
+    public abstract void removeImeSurface(int displayId);
 
     /**
      * Updates the IME visibility, back disposition and show IME picker status for SystemUI.
@@ -298,11 +302,11 @@
                 }
 
                 @Override
-                public void onImeParentChanged() {
+                public void onImeParentChanged(int displayId) {
                 }
 
                 @Override
-                public void removeImeSurface() {
+                public void removeImeSurface(int displayId) {
                 }
 
                 @Override
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 16e043c..0d29b7d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -5671,7 +5671,7 @@
         }
 
         @Override
-        public void onImeParentChanged() {
+        public void onImeParentChanged(int displayId) {
             synchronized (ImfLock.class) {
                 // Hide the IME method menu only when the IME surface parent is changed by the
                 // input target changed, in case seeing the dialog dismiss flickering during
@@ -5683,7 +5683,7 @@
         }
 
         @Override
-        public void removeImeSurface() {
+        public void removeImeSurface(int displayId) {
             mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget();
         }
 
diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
index 78c8cde..403b421 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
@@ -19,6 +19,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 
 import android.content.Context;
+import android.location.flags.Flags;
 import android.net.ConnectivityManager;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -48,6 +49,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Handles network connection requests and network state change updates for AGPS data download.
@@ -91,6 +93,10 @@
     // network with SUPL connectivity or report an error.
     private static final int SUPL_NETWORK_REQUEST_TIMEOUT_MILLIS = 20 * 1000;
 
+    // If the chipset does not request to release a SUPL connection before the specified timeout in
+    // milliseconds, the connection will be automatically released.
+    private static final long SUPL_CONNECTION_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(1);
+
     private static final int HASH_MAP_INITIAL_CAPACITY_TO_TRACK_CONNECTED_NETWORKS = 5;
 
     // Keeps track of networks and their state as notified by the network request callbacks.
@@ -121,6 +127,8 @@
     private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000;
     private final PowerManager.WakeLock mWakeLock;
 
+    private final Object mSuplConnectionReleaseOnTimeoutToken = new Object();
+
     /**
      * Network attributes needed when updating HAL about network connectivity status changes.
      */
@@ -609,6 +617,13 @@
                     mSuplConnectivityCallback,
                     mHandler,
                     SUPL_NETWORK_REQUEST_TIMEOUT_MILLIS);
+            if (Flags.releaseSuplConnectionOnTimeout()) {
+                // Schedule to release the SUPL connection after timeout
+                mHandler.removeCallbacksAndMessages(mSuplConnectionReleaseOnTimeoutToken);
+                mHandler.postDelayed(() -> handleReleaseSuplConnection(GPS_RELEASE_AGPS_DATA_CONN),
+                        mSuplConnectionReleaseOnTimeoutToken,
+                        SUPL_CONNECTION_TIMEOUT_MILLIS);
+            }
         } catch (RuntimeException e) {
             Log.e(TAG, "Failed to request network.", e);
             mSuplConnectivityCallback = null;
@@ -639,6 +654,10 @@
             Log.d(TAG, message);
         }
 
+        if (Flags.releaseSuplConnectionOnTimeout()) {
+            // Remove pending task to avoid releasing an incorrect connection
+            mHandler.removeCallbacksAndMessages(mSuplConnectionReleaseOnTimeoutToken);
+        }
         if (mAGpsDataConnectionState == AGPS_DATA_CONNECTION_CLOSED) {
             return;
         }
diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java
index 2da1a68..66e61c0 100644
--- a/services/core/java/com/android/server/notification/ConditionProviders.java
+++ b/services/core/java/com/android/server/notification/ConditionProviders.java
@@ -234,7 +234,7 @@
             if (pkgList != null && (pkgList.length > 0)) {
                 for (String pkgName : pkgList) {
                     try {
-                        inm.removeAutomaticZenRules(pkgName);
+                        inm.removeAutomaticZenRules(pkgName, /* fromUser= */ false);
                         inm.setNotificationPolicyAccessGranted(pkgName, false);
                     } catch (Exception e) {
                         Slog.e(TAG, "Failed to clean up rules for " + pkgName, e);
diff --git a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
index 8855666..71a6b5e 100644
--- a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
+++ b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
@@ -109,7 +109,6 @@
         if (origin == ZenModeConfig.UPDATE_ORIGIN_INIT
                 || origin == ZenModeConfig.UPDATE_ORIGIN_INIT_USER
                 || origin == ZenModeConfig.UPDATE_ORIGIN_USER
-                || origin == ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
                 || !mPowerManager.isInteractive()) {
             unregisterScreenOffReceiver();
             updateNightModeImmediately(useNightMode);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 75d3dce..135a467 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -5343,13 +5343,14 @@
         }
 
         @Override
-        public void setZenMode(int mode, Uri conditionId, String reason) throws RemoteException {
+        public void setZenMode(int mode, Uri conditionId, String reason, boolean fromUser) {
             enforceSystemOrSystemUI("INotificationManager.setZenMode");
             final int callingUid = Binder.getCallingUid();
             final long identity = Binder.clearCallingIdentity();
+            enforceUserOriginOnlyFromSystem(fromUser, "setZenMode");
+
             try {
-                mZenModeHelper.setManualZenMode(mode, conditionId,
-                        ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, // Checked by enforce()
+                mZenModeHelper.setManualZenMode(mode, conditionId, computeZenOrigin(fromUser),
                         reason, /* caller= */ null, callingUid);
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -5380,7 +5381,8 @@
         }
 
         @Override
-        public String addAutomaticZenRule(AutomaticZenRule automaticZenRule, String pkg) {
+        public String addAutomaticZenRule(AutomaticZenRule automaticZenRule, String pkg,
+                boolean fromUser) {
             validateAutomaticZenRule(automaticZenRule);
             checkCallerIsSameApp(pkg);
             if (automaticZenRule.getZenPolicy() != null
@@ -5389,6 +5391,7 @@
                         + "INTERRUPTION_FILTER_PRIORITY filters");
             }
             enforcePolicyAccess(Binder.getCallingUid(), "addAutomaticZenRule");
+            enforceUserOriginOnlyFromSystem(fromUser, "addAutomaticZenRule");
 
             // If the calling app is the system (from any user), take the package name from the
             // rule's owner rather than from the caller's package.
@@ -5400,24 +5403,18 @@
             }
 
             return mZenModeHelper.addAutomaticZenRule(rulePkg, automaticZenRule,
-                    // TODO: b/308670715: Distinguish origin properly (e.g. USER if creating a rule
-                    //  manually in Settings).
-                    isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                            : ZenModeConfig.UPDATE_ORIGIN_APP,
-                    "addAutomaticZenRule", Binder.getCallingUid());
+                    computeZenOrigin(fromUser), "addAutomaticZenRule", Binder.getCallingUid());
         }
 
         @Override
-        public boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule) {
+        public boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule,
+                boolean fromUser) throws RemoteException {
             validateAutomaticZenRule(automaticZenRule);
             enforcePolicyAccess(Binder.getCallingUid(), "updateAutomaticZenRule");
+            enforceUserOriginOnlyFromSystem(fromUser, "updateAutomaticZenRule");
 
-            // TODO: b/308670715: Distinguish origin properly (e.g. USER if updating a rule
-            //  manually in Settings).
             return mZenModeHelper.updateAutomaticZenRule(id, automaticZenRule,
-                    isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                            : ZenModeConfig.UPDATE_ORIGIN_APP,
-                    "updateAutomaticZenRule", Binder.getCallingUid());
+                    computeZenOrigin(fromUser), "updateAutomaticZenRule", Binder.getCallingUid());
         }
 
         private void validateAutomaticZenRule(AutomaticZenRule rule) {
@@ -5445,27 +5442,24 @@
         }
 
         @Override
-        public boolean removeAutomaticZenRule(String id) throws RemoteException {
+        public boolean removeAutomaticZenRule(String id, boolean fromUser) throws RemoteException {
             Objects.requireNonNull(id, "Id is null");
             // Verify that they can modify zen rules.
             enforcePolicyAccess(Binder.getCallingUid(), "removeAutomaticZenRule");
+            enforceUserOriginOnlyFromSystem(fromUser, "removeAutomaticZenRule");
 
-            // TODO: b/308670715: Distinguish origin properly (e.g. USER if removing a rule
-            //  manually in Settings).
-            return mZenModeHelper.removeAutomaticZenRule(id,
-                    isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                            : ZenModeConfig.UPDATE_ORIGIN_APP,
+            return mZenModeHelper.removeAutomaticZenRule(id, computeZenOrigin(fromUser),
                     "removeAutomaticZenRule", Binder.getCallingUid());
         }
 
         @Override
-        public boolean removeAutomaticZenRules(String packageName) throws RemoteException {
+        public boolean removeAutomaticZenRules(String packageName, boolean fromUser)
+                throws RemoteException {
             Objects.requireNonNull(packageName, "Package name is null");
             enforceSystemOrSystemUI("removeAutomaticZenRules");
+            enforceUserOriginOnlyFromSystem(fromUser, "removeAutomaticZenRules");
 
-            return mZenModeHelper.removeAutomaticZenRules(packageName,
-                    isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                            : ZenModeConfig.UPDATE_ORIGIN_APP,
+            return mZenModeHelper.removeAutomaticZenRules(packageName, computeZenOrigin(fromUser),
                     packageName + "|removeAutomaticZenRules", Binder.getCallingUid());
         }
 
@@ -5478,28 +5472,54 @@
         }
 
         @Override
-        public void setAutomaticZenRuleState(String id, Condition condition) {
+        public void setAutomaticZenRuleState(String id, Condition condition, boolean fromUser) {
             Objects.requireNonNull(id, "id is null");
             Objects.requireNonNull(condition, "Condition is null");
             condition.validate();
 
             enforcePolicyAccess(Binder.getCallingUid(), "setAutomaticZenRuleState");
 
-            // TODO: b/308670715: Distinguish origin properly (e.g. USER if toggling a rule
-            //  manually in Settings).
-            mZenModeHelper.setAutomaticZenRuleState(id, condition,
-                    isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                            : ZenModeConfig.UPDATE_ORIGIN_APP,
+            if (android.app.Flags.modesApi()) {
+                if (fromUser != (condition.source == Condition.SOURCE_USER_ACTION)) {
+                    throw new IllegalArgumentException(String.format(
+                            "Mismatch between fromUser (%s) and condition.source (%s)",
+                            fromUser, Condition.sourceToString(condition.source)));
+                }
+            }
+
+            mZenModeHelper.setAutomaticZenRuleState(id, condition, computeZenOrigin(fromUser),
                     Binder.getCallingUid());
         }
 
+        @ZenModeConfig.ConfigChangeOrigin
+        private int computeZenOrigin(boolean fromUser) {
+            // "fromUser" is introduced with MODES_API, so only consider it in that case.
+            // (Non-MODES_API behavior should also not depend at all on UPDATE_ORIGIN_USER).
+            if (android.app.Flags.modesApi() && fromUser) {
+                return ZenModeConfig.UPDATE_ORIGIN_USER;
+            } else if (isCallerSystemOrSystemUi()) {
+                return ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI;
+            } else {
+                return ZenModeConfig.UPDATE_ORIGIN_APP;
+            }
+        }
+
+        private void enforceUserOriginOnlyFromSystem(boolean fromUser, String method) {
+            if (android.app.Flags.modesApi()
+                    && fromUser
+                    && !isCallerSystemOrSystemUiOrShell()) {
+                throw new SecurityException(String.format(
+                        "Calling %s with fromUser == true is only allowed for system", method));
+            }
+        }
+
         @Override
-        public void setInterruptionFilter(String pkg, int filter) throws RemoteException {
+        public void setInterruptionFilter(String pkg, int filter, boolean fromUser) {
             enforcePolicyAccess(pkg, "setInterruptionFilter");
             final int zen = NotificationManager.zenModeFromInterruptionFilter(filter, -1);
             if (zen == -1) throw new IllegalArgumentException("Invalid filter: " + filter);
             final int callingUid = Binder.getCallingUid();
-            final boolean isSystemOrSystemUi = isCallerSystemOrSystemUi();
+            enforceUserOriginOnlyFromSystem(fromUser, "setInterruptionFilter");
 
             if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) {
                 mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, callingUid, zen);
@@ -5508,9 +5528,7 @@
 
             final long identity = Binder.clearCallingIdentity();
             try {
-                mZenModeHelper.setManualZenMode(zen, null,
-                        isSystemOrSystemUi ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                                : ZenModeConfig.UPDATE_ORIGIN_APP,
+                mZenModeHelper.setManualZenMode(zen, null, computeZenOrigin(fromUser),
                         /* reason= */ "setInterruptionFilter", /* caller= */ pkg,
                         callingUid);
             } finally {
@@ -5825,10 +5843,11 @@
          * {@link Policy#PRIORITY_CATEGORY_MEDIA} from bypassing dnd
          */
         @Override
-        public void setNotificationPolicy(String pkg, Policy policy) {
+        public void setNotificationPolicy(String pkg, Policy policy, boolean fromUser) {
             enforcePolicyAccess(pkg, "setNotificationPolicy");
+            enforceUserOriginOnlyFromSystem(fromUser, "setNotificationPolicy");
             int callingUid = Binder.getCallingUid();
-            boolean isSystemOrSystemUi = isCallerSystemOrSystemUi();
+            @ZenModeConfig.ConfigChangeOrigin int origin = computeZenOrigin(fromUser);
 
             boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi()
                     && !canManageGlobalZenPolicy(pkg, callingUid);
@@ -5873,14 +5892,12 @@
                         newVisualEffects, policy.priorityConversationSenders);
 
                 if (shouldApplyAsImplicitRule) {
-                    mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy);
+                    mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy,
+                            origin);
                 } else {
                     ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion,
                             policy);
-                    mZenModeHelper.setNotificationPolicy(policy,
-                            isSystemOrSystemUi ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                                    : ZenModeConfig.UPDATE_ORIGIN_APP,
-                            callingUid);
+                    mZenModeHelper.setNotificationPolicy(policy, origin, callingUid);
                 }
             } catch (RemoteException e) {
                 Slog.e(TAG, "Failed to set notification policy", e);
@@ -7001,12 +7018,14 @@
             return false;
         }
 
-        final boolean hasBitmap = n.extras.containsKey(Notification.EXTRA_PICTURE);
+        final boolean hasBitmap = n.extras.containsKey(Notification.EXTRA_PICTURE)
+                && n.extras.getParcelable(Notification.EXTRA_PICTURE) != null;
         if (hasBitmap) {
             return true;
         }
 
-        final boolean hasIcon = n.extras.containsKey(Notification.EXTRA_PICTURE_ICON);
+        final boolean hasIcon = n.extras.containsKey(Notification.EXTRA_PICTURE_ICON)
+                && n.extras.getParcelable(Notification.EXTRA_PICTURE_ICON) != null;
         if (hasIcon) {
             return true;
         }
@@ -7022,9 +7041,10 @@
         if (!isBigPictureWithBitmapOrIcon(r.getNotification())) {
             return;
         }
-        // Remove Notification object's reference to picture bitmap or URI
-        r.getNotification().extras.remove(Notification.EXTRA_PICTURE);
-        r.getNotification().extras.remove(Notification.EXTRA_PICTURE_ICON);
+        // Remove Notification object's reference to picture bitmap or URI. Leave the extras set to
+        // null to avoid crashing apps that came to expect them to be present but null.
+        r.getNotification().extras.putParcelable(Notification.EXTRA_PICTURE, null);
+        r.getNotification().extras.putParcelable(Notification.EXTRA_PICTURE_ICON, null);
 
         // Make Notification silent
         r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
diff --git a/services/core/java/com/android/server/notification/NotificationShellCmd.java b/services/core/java/com/android/server/notification/NotificationShellCmd.java
index dc0cf4e..9f3104c 100644
--- a/services/core/java/com/android/server/notification/NotificationShellCmd.java
+++ b/services/core/java/com/android/server/notification/NotificationShellCmd.java
@@ -117,7 +117,6 @@
     private final NotificationManagerService mDirectService;
     private final INotificationManager mBinderService;
     private final PackageManager mPm;
-    private NotificationChannel mChannel;
 
     public NotificationShellCmd(NotificationManagerService service) {
         mDirectService = service;
@@ -183,7 +182,13 @@
                             interruptionFilter = INTERRUPTION_FILTER_ALL;
                     }
                     final int filter = interruptionFilter;
-                    mBinderService.setInterruptionFilter(callingPackage, filter);
+                    if (android.app.Flags.modesApi()) {
+                        mBinderService.setInterruptionFilter(callingPackage, filter,
+                                /* fromUser= */ true);
+                    } else {
+                        mBinderService.setInterruptionFilter(callingPackage, filter,
+                                /* fromUser= */ false);
+                    }
                 }
                 break;
                 case "allow_dnd": {
diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
index 87158cd..df570a0 100644
--- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java
+++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
@@ -29,6 +29,7 @@
 import android.os.Process;
 import android.service.notification.DNDPolicyProto;
 import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ConfigChangeOrigin;
 import android.service.notification.ZenModeDiff;
 import android.service.notification.ZenPolicy;
 import android.util.ArrayMap;
@@ -58,7 +59,7 @@
     // mode change.
     ZenModeEventLogger.ZenStateChanges mChangeState = new ZenModeEventLogger.ZenStateChanges();
 
-    private PackageManager mPm;
+    private final PackageManager mPm;
 
     ZenModeEventLogger(PackageManager pm) {
         mPm = pm;
@@ -97,11 +98,11 @@
      * @param newInfo     ZenModeInfo after this change takes effect
      * @param callingUid  the calling UID associated with the change; may be used to attribute the
      *                    change to a particular package or determine if this is a user action
-     * @param fromSystemOrSystemUi whether the calling UID is either system UID or system UI
+     * @param origin      The origin of the Zen change.
      */
     public final void maybeLogZenChange(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid,
-            boolean fromSystemOrSystemUi) {
-        mChangeState.init(prevInfo, newInfo, callingUid, fromSystemOrSystemUi);
+            @ConfigChangeOrigin int origin) {
+        mChangeState.init(prevInfo, newInfo, callingUid, origin);
         if (mChangeState.shouldLogChanges()) {
             maybeReassignCallingUid();
             logChanges();
@@ -124,7 +125,7 @@
         // We don't consider the manual rule in the old config because if a manual rule is turning
         // off with a call from system, that could easily be a user action to explicitly turn it off
         if (mChangeState.getChangedRuleType() == RULE_TYPE_MANUAL) {
-            if (!mChangeState.mFromSystemOrSystemUi
+            if (!mChangeState.isFromSystemOrSystemUi()
                     || mChangeState.getNewManualRuleEnabler() == null) {
                 return;
             }
@@ -136,7 +137,7 @@
         //   - we've determined it's not a user action
         //   - our current best guess is that the calling uid is system/sysui
         if (mChangeState.getChangedRuleType() == RULE_TYPE_AUTOMATIC) {
-            if (mChangeState.getIsUserAction() || !mChangeState.mFromSystemOrSystemUi) {
+            if (mChangeState.getIsUserAction() || !mChangeState.isFromSystemOrSystemUi()) {
                 return;
             }
 
@@ -221,10 +222,10 @@
         ZenModeConfig mPrevConfig, mNewConfig;
         NotificationManager.Policy mPrevPolicy, mNewPolicy;
         int mCallingUid = Process.INVALID_UID;
-        boolean mFromSystemOrSystemUi = false;
+        @ConfigChangeOrigin int mOrigin = ZenModeConfig.UPDATE_ORIGIN_UNKNOWN;
 
         private void init(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid,
-                boolean fromSystemOrSystemUi) {
+                @ConfigChangeOrigin int origin) {
             // previous & new may be the same -- that would indicate that zen mode hasn't changed.
             mPrevZenMode = prevInfo.mZenMode;
             mNewZenMode = newInfo.mZenMode;
@@ -233,7 +234,7 @@
             mPrevPolicy = prevInfo.mPolicy;
             mNewPolicy = newInfo.mPolicy;
             mCallingUid = callingUid;
-            mFromSystemOrSystemUi = fromSystemOrSystemUi;
+            mOrigin = origin;
         }
 
         /**
@@ -389,12 +390,16 @@
 
         /**
          * Return our best guess as to whether the changes observed are due to a user action.
-         * Note that this won't be 100% accurate as we can't necessarily distinguish between a
-         * system uid call indicating "user interacted with Settings" vs "a system app changed
-         * something automatically".
+         * Note that this (before {@code MODES_API}) won't be 100% accurate as we can't necessarily
+         * distinguish between a system uid call indicating "user interacted with Settings" vs "a
+         * system app changed something automatically".
          */
         boolean getIsUserAction() {
-            // Approach:
+            if (Flags.modesApi()) {
+                return mOrigin == ZenModeConfig.UPDATE_ORIGIN_USER;
+            }
+
+            // Approach for pre-MODES_API:
             //   - if manual rule turned on or off, the calling UID is system, and the new manual
             //     rule does not have an enabler set, guess that this is likely to be a user action.
             //     This may represent a system app turning on DND automatically, but we guess "user"
@@ -419,13 +424,13 @@
             switch (getChangedRuleType()) {
                 case RULE_TYPE_MANUAL:
                     // TODO(b/278888961): Distinguish the automatically-turned-off state
-                    return mFromSystemOrSystemUi && (getNewManualRuleEnabler() == null);
+                    return isFromSystemOrSystemUi() && (getNewManualRuleEnabler() == null);
                 case RULE_TYPE_AUTOMATIC:
                     for (ZenModeDiff.RuleDiff d : getChangedAutomaticRules().values()) {
                         if (d.wasAdded() || d.wasRemoved()) {
                             // If the change comes from system, a rule being added/removed indicates
                             // a likely user action. From an app, it's harder to know for sure.
-                            return mFromSystemOrSystemUi;
+                            return isFromSystemOrSystemUi();
                         }
                         ZenModeDiff.FieldDiff enabled = d.getDiffForField(
                                 ZenModeDiff.RuleDiff.FIELD_ENABLED);
@@ -455,6 +460,13 @@
             return false;
         }
 
+        boolean isFromSystemOrSystemUi() {
+            return mOrigin == ZenModeConfig.UPDATE_ORIGIN_INIT
+                    || mOrigin == ZenModeConfig.UPDATE_ORIGIN_INIT_USER
+                    || mOrigin == ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
+                    || mOrigin == ZenModeConfig.UPDATE_ORIGIN_RESTORE_BACKUP;
+        }
+
         /**
          * Get the package UID associated with this change, which is just the calling UID for the
          * relevant method changes. This may get reset by ZenModeEventLogger, which has access to
@@ -612,7 +624,7 @@
             copy.mPrevPolicy = mPrevPolicy.copy();
             copy.mNewPolicy = mNewPolicy.copy();
             copy.mCallingUid = mCallingUid;
-            copy.mFromSystemOrSystemUi = mFromSystemOrSystemUi;
+            copy.mOrigin = mOrigin;
             return copy;
         }
     }
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 3f8b595..0a46901 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -561,7 +561,7 @@
      * {@link Global#ZEN_MODE_IMPORTANT_INTERRUPTIONS}.
      */
     void applyGlobalPolicyAsImplicitZenRule(String callingPkg, int callingUid,
-            NotificationManager.Policy policy) {
+            NotificationManager.Policy policy, @ConfigChangeOrigin int origin) {
         if (!android.app.Flags.modesApi()) {
             Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!");
             return;
@@ -579,7 +579,7 @@
             }
             // TODO: b/308673679 - Keep user customization of this rule!
             rule.zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy);
-            setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP,
+            setConfigLocked(newConfig, /* triggeringComponent= */ null, origin,
                     "applyGlobalPolicyAsImplicitZenRule", callingUid);
         }
     }
@@ -1371,12 +1371,8 @@
         if (logZenModeEvents) {
             ZenModeEventLogger.ZenModeInfo newInfo = new ZenModeEventLogger.ZenModeInfo(
                     mZenMode, mConfig, mConsolidatedPolicy);
-            boolean fromSystemOrSystemUi = origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                    || origin == UPDATE_ORIGIN_INIT
-                    || origin == UPDATE_ORIGIN_INIT_USER
-                    || origin == UPDATE_ORIGIN_RESTORE_BACKUP;
             mZenModeEventLogger.maybeLogZenChange(prevInfo, newInfo, callingUid,
-                    fromSystemOrSystemUi);
+                    origin);
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 2864a8b..dcfc855d 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -132,6 +132,11 @@
     private static final String EXTRA_INSTALLER_TITLE =
             "com.android.content.pm.extra.UNARCHIVE_INSTALLER_TITLE";
 
+    private static final PorterDuffColorFilter OPACITY_LAYER_FILTER =
+            new PorterDuffColorFilter(
+                    Color.argb(0.5f /* alpha */, 0f /* red */, 0f /* green */, 0f /* blue */),
+                    PorterDuff.Mode.SRC_ATOP);
+
     private final Context mContext;
     private final PackageManagerService mPm;
 
@@ -746,11 +751,7 @@
             return bitmap;
         }
         BitmapDrawable appIconDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
-        PorterDuffColorFilter colorFilter =
-                new PorterDuffColorFilter(
-                        Color.argb(0.32f /* alpha */, 0f /* red */, 0f /* green */, 0f /* blue */),
-                        PorterDuff.Mode.SRC_ATOP);
-        appIconDrawable.setColorFilter(colorFilter);
+        appIconDrawable.setColorFilter(OPACITY_LAYER_FILTER);
         appIconDrawable.setBounds(
                 0 /* left */,
                 0 /* top */,
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 671e031..3afba39 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -3291,7 +3291,7 @@
         final PermissionAllowlist permissionAllowlist =
                 SystemConfig.getInstance().getPermissionAllowlist();
         final String packageName = packageState.getPackageName();
-        if (packageState.isVendor()) {
+        if (packageState.isVendor() || packageState.isOdm()) {
             return permissionAllowlist.getVendorPrivilegedAppAllowlistState(packageName,
                     permissionName);
         } else if (packageState.isProduct()) {
@@ -3386,7 +3386,7 @@
             // the permission's protectionLevel does not have the extra 'vendorPrivileged'
             // flag.
             if (allowed && isPrivilegedPermission && !bp.isVendorPrivileged()
-                    && pkgSetting.isVendor()) {
+                    && (pkgSetting.isVendor() || pkgSetting.isOdm())) {
                 Slog.w(TAG, "Permission " + permissionName
                         + " cannot be granted to privileged vendor apk " + pkg.getPackageName()
                         + " because it isn't a 'vendorPrivileged' permission.");
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 938ed23..e8b54d58 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -6865,13 +6865,13 @@
     static class ButtonOverridePermissionChecker {
         boolean canAppOverrideSystemKey(Context context, int uid) {
             return PermissionChecker.checkPermissionForDataDelivery(
-                            context,
-                            OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW,
-                            PID_UNKNOWN,
-                            uid,
-                            null,
-                            null,
-                            null)
+                    context,
+                    OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW,
+                    PID_UNKNOWN,
+                    uid,
+                    null,
+                    null,
+                    null)
                     == PERMISSION_GRANTED;
         }
     }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index f3922f9..1ce87a7 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -565,7 +565,6 @@
     boolean idle;           // has the activity gone idle?
     boolean hasBeenLaunched;// has this activity ever been launched?
     boolean immersive;      // immersive mode (don't interrupt if possible)
-    boolean forceNewConfig; // force re-create with new config next time
     boolean supportsEnterPipOnTaskSwitch;  // This flag is set by the system to indicate that the
         // activity can enter picture in picture while pausing (only when switching to another task)
     // The PiP params used when deferring the entering of picture-in-picture.
@@ -9600,7 +9599,7 @@
         // configurations because there are cases (like moving a task to the root pinned task) where
         // the combine configurations are equal, but would otherwise differ in the override config
         mTmpConfig.setTo(mLastReportedConfiguration.getMergedConfiguration());
-        if (getConfiguration().equals(mTmpConfig) && !forceNewConfig && !displayChanged) {
+        if (getConfiguration().equals(mTmpConfig) && !displayChanged) {
             ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration & display "
                     + "unchanged in %s", this);
             return true;
@@ -9627,7 +9626,7 @@
             return true;
         }
 
-        if (changes == 0 && !forceNewConfig) {
+        if (changes == 0) {
             ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration no differences in %s",
                     this);
             // There are no significant differences, so we won't relaunch but should still deliver
@@ -9649,7 +9648,6 @@
         // pick that up next time it starts.
         if (!attachedToProcess()) {
             ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration doesn't matter not running %s", this);
-            forceNewConfig = false;
             return true;
         }
 
@@ -9659,11 +9657,10 @@
                 Integer.toHexString(changes), Integer.toHexString(info.getRealConfigChanged()),
                 mLastReportedConfiguration);
 
-        if (shouldRelaunchLocked(changes, mTmpConfig) || forceNewConfig) {
+        if (shouldRelaunchLocked(changes, mTmpConfig)) {
             // Aha, the activity isn't handling the change, so DIE DIE DIE.
             configChangeFlags |= changes;
             startFreezingScreenLocked(globalChanges);
-            forceNewConfig = false;
             // Do not preserve window if it is freezing screen because the original window won't be
             // able to update drawn state that causes freeze timeout.
             preserveWindow &= isResizeOnlyChange(changes) && !mFreezingScreen;
@@ -9883,7 +9880,6 @@
         try {
             ProtoLog.i(WM_DEBUG_STATES, "Moving to %s Relaunching %s callers=%s" ,
                     (andResume ? "RESUMED" : "PAUSED"), this, Debug.getCallers(6));
-            forceNewConfig = false;
             final ClientTransactionItem callbackItem = ActivityRelaunchItem.obtain(token,
                     pendingResults, pendingNewIntents, configChangeFlags,
                     new MergedConfiguration(getProcessGlobalConfiguration(),
diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
index 86be6ba..7af494c 100644
--- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java
+++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
@@ -33,6 +33,7 @@
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
+import com.android.window.flags.Flags;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -121,7 +122,8 @@
 
     // TODO remove when enabled
     static boolean isSnapshotEnabled() {
-        return SystemProperties.getInt("persist.wm.debug.activity_screenshot", 0) != 0;
+        return SystemProperties.getInt("persist.wm.debug.activity_screenshot", 0) != 0
+                || Flags.activitySnapshotByDefault();
     }
 
     static PersistInfoProvider createPersistInfoProvider(
diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
index 1b45c1b..e7621ff 100644
--- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
+++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
@@ -224,7 +224,7 @@
             // before issuing the work challenge.
             return true;
         }
-        if (interceptLockedManagedProfileIfNeeded()) {
+        if (interceptLockedProfileIfNeeded()) {
             return true;
         }
         if (interceptHomeIfNeeded()) {
@@ -378,7 +378,7 @@
         return true;
     }
 
-    private boolean interceptLockedManagedProfileIfNeeded() {
+    private boolean interceptLockedProfileIfNeeded() {
         final Intent interceptingIntent = interceptWithConfirmCredentialsIfNeeded(mAInfo, mUserId);
         if (interceptingIntent == null) {
             return false;
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 630b9e1..cb2adbc 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -579,30 +579,11 @@
                     computeResolveFilterUid(callingUid, realCallingUid, filterCallingUid),
                     realCallingPid);
             if (resolveInfo == null) {
-                final UserInfo userInfo = supervisor.getUserInfo(userId);
-                if (userInfo != null && userInfo.isManagedProfile()) {
-                    // Special case for managed profiles, if attempting to launch non-cryto aware
-                    // app in a locked managed profile from an unlocked parent allow it to resolve
-                    // as user will be sent via confirm credentials to unlock the profile.
-                    final UserManager userManager = UserManager.get(supervisor.mService.mContext);
-                    boolean profileLockedAndParentUnlockingOrUnlocked = false;
-                    final long token = Binder.clearCallingIdentity();
-                    try {
-                        final UserInfo parent = userManager.getProfileParent(userId);
-                        profileLockedAndParentUnlockingOrUnlocked = (parent != null)
-                                && userManager.isUserUnlockingOrUnlocked(parent.id)
-                                && !userManager.isUserUnlockingOrUnlocked(userId);
-                    } finally {
-                        Binder.restoreCallingIdentity(token);
-                    }
-                    if (profileLockedAndParentUnlockingOrUnlocked) {
-                        resolveInfo = supervisor.resolveIntent(intent, resolvedType, userId,
-                                PackageManager.MATCH_DIRECT_BOOT_AWARE
-                                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
-                                computeResolveFilterUid(callingUid, realCallingUid,
-                                        filterCallingUid), realCallingPid);
-                    }
-                }
+                // Special case for profiles: If attempting to launch non-crypto aware app in a
+                // locked profile or launch an app in a profile that is stopped by quiet mode from
+                // an unlocked parent, allow it to resolve as user will be sent via confirm
+                // credentials to unlock the profile.
+                resolveInfo = resolveIntentForLockedOrStoppedProfiles(supervisor);
             }
 
             // Collect information about the target of the Intent.
@@ -616,6 +597,36 @@
                         UserHandle.getUserId(activityInfo.applicationInfo.uid));
             }
         }
+
+        /**
+         * Resolve intent for locked or stopped profiles if the parent profile is unlocking or
+         * unlocked.
+         */
+        ResolveInfo resolveIntentForLockedOrStoppedProfiles(
+                ActivityTaskSupervisor supervisor) {
+            final UserInfo userInfo = supervisor.getUserInfo(userId);
+            if (userInfo != null && userInfo.isProfile()) {
+                final UserManager userManager = UserManager.get(supervisor.mService.mContext);
+                boolean profileLockedAndParentUnlockingOrUnlocked = false;
+                final long token = Binder.clearCallingIdentity();
+                try {
+                    final UserInfo parent = userManager.getProfileParent(userId);
+                    profileLockedAndParentUnlockingOrUnlocked = (parent != null)
+                            && userManager.isUserUnlockingOrUnlocked(parent.id)
+                            && !userManager.isUserUnlockingOrUnlocked(userId);
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
+                if (profileLockedAndParentUnlockingOrUnlocked) {
+                    return supervisor.resolveIntent(intent, resolvedType, userId,
+                            PackageManager.MATCH_DIRECT_BOOT_AWARE
+                                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
+                            computeResolveFilterUid(callingUid, realCallingUid,
+                                    filterCallingUid), realCallingPid);
+                }
+            }
+            return null;
+        }
     }
 
     ActivityStarter(ActivityStartController controller, ActivityTaskManagerService service,
@@ -2767,10 +2778,7 @@
             }
         }
 
-        // If the target task is not in the front, then we need to bring it to the front...
-        // except...  well, with SINGLE_TASK_LAUNCH it's not entirely clear. We'd like to have
-        // the same behavior as if a new instance was being started, which means not bringing it
-        // to the front if the caller is not itself in the front.
+        // If the target task is not in the front, then we need to bring it to the front.
         final boolean differentTopTask;
         if (mTargetRootTask.getDisplayArea() == mPreferredTaskDisplayArea) {
             final Task focusRootTask = mTargetRootTask.mDisplayContent.getFocusedRootTask();
@@ -2787,49 +2795,47 @@
 
         if (differentTopTask && !avoidMoveToFront()) {
             mStartActivity.intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT);
-            if (mSourceRecord == null || inTopNonFinishingTask(mSourceRecord)) {
-                // We really do want to push this one into the user's face, right now.
-                if (mLaunchTaskBehind && mSourceRecord != null) {
-                    intentActivity.setTaskToAffiliateWith(mSourceRecord.getTask());
-                }
-
-                if (intentActivity.isDescendantOf(mTargetRootTask)) {
-                    // TODO(b/151572268): Figure out a better way to move tasks in above 2-levels
-                    //  tasks hierarchies.
-                    if (mTargetRootTask != intentTask
-                            && mTargetRootTask != intentTask.getParent().asTask()) {
-                        intentTask.getParent().positionChildAt(POSITION_TOP, intentTask,
-                                false /* includingParents */);
-                        intentTask = intentTask.getParent().asTaskFragment().getTask();
-                    }
-                    // If the activity is visible in multi-windowing mode, it may already be on
-                    // the top (visible to user but not the global top), then the result code
-                    // should be START_DELIVERED_TO_TOP instead of START_TASK_TO_FRONT.
-                    final boolean wasTopOfVisibleRootTask = intentActivity.isVisibleRequested()
-                            && intentActivity.inMultiWindowMode()
-                            && intentActivity == mTargetRootTask.topRunningActivity()
-                            && !intentActivity.mTransitionController.isTransientHide(
-                                    mTargetRootTask);
-                    // We only want to move to the front, if we aren't going to launch on a
-                    // different root task. If we launch on a different root task, we will put the
-                    // task on top there.
-                    // Defer resuming the top activity while moving task to top, since the
-                    // current task-top activity may not be the activity that should be resumed.
-                    mTargetRootTask.moveTaskToFront(intentTask, mNoAnimation, mOptions,
-                            mStartActivity.appTimeTracker, DEFER_RESUME,
-                            "bringingFoundTaskToFront");
-                    mMovedToFront = !wasTopOfVisibleRootTask;
-                } else if (intentActivity.getWindowingMode() != WINDOWING_MODE_PINNED) {
-                    // Leaves reparenting pinned task operations to task organizer to make sure it
-                    // dismisses pinned task properly.
-                    // TODO(b/199997762): Consider leaving all reparent operation of organized tasks
-                    //  to task organizer.
-                    intentTask.reparent(mTargetRootTask, ON_TOP, REPARENT_MOVE_ROOT_TASK_TO_FRONT,
-                            ANIMATE, DEFER_RESUME, "reparentToTargetRootTask");
-                    mMovedToFront = true;
-                }
-                mOptions = null;
+            // We really do want to push this one into the user's face, right now.
+            if (mLaunchTaskBehind && mSourceRecord != null) {
+                intentActivity.setTaskToAffiliateWith(mSourceRecord.getTask());
             }
+
+            if (intentActivity.isDescendantOf(mTargetRootTask)) {
+                // TODO(b/151572268): Figure out a better way to move tasks in above 2-levels
+                //  tasks hierarchies.
+                if (mTargetRootTask != intentTask
+                        && mTargetRootTask != intentTask.getParent().asTask()) {
+                    intentTask.getParent().positionChildAt(POSITION_TOP, intentTask,
+                            false /* includingParents */);
+                    intentTask = intentTask.getParent().asTaskFragment().getTask();
+                }
+                // If the activity is visible in multi-windowing mode, it may already be on
+                // the top (visible to user but not the global top), then the result code
+                // should be START_DELIVERED_TO_TOP instead of START_TASK_TO_FRONT.
+                final boolean wasTopOfVisibleRootTask = intentActivity.isVisibleRequested()
+                        && intentActivity.inMultiWindowMode()
+                        && intentActivity == mTargetRootTask.topRunningActivity()
+                        && !intentActivity.mTransitionController.isTransientHide(
+                                mTargetRootTask);
+                // We only want to move to the front, if we aren't going to launch on a
+                // different root task. If we launch on a different root task, we will put the
+                // task on top there.
+                // Defer resuming the top activity while moving task to top, since the
+                // current task-top activity may not be the activity that should be resumed.
+                mTargetRootTask.moveTaskToFront(intentTask, mNoAnimation, mOptions,
+                        mStartActivity.appTimeTracker, DEFER_RESUME,
+                        "bringingFoundTaskToFront");
+                mMovedToFront = !wasTopOfVisibleRootTask;
+            } else if (intentActivity.getWindowingMode() != WINDOWING_MODE_PINNED) {
+                // Leaves reparenting pinned task operations to task organizer to make sure it
+                // dismisses pinned task properly.
+                // TODO(b/199997762): Consider leaving all reparent operation of organized tasks
+                //  to task organizer.
+                intentTask.reparent(mTargetRootTask, ON_TOP, REPARENT_MOVE_ROOT_TASK_TO_FRONT,
+                        ANIMATE, DEFER_RESUME, "reparentToTargetRootTask");
+                mMovedToFront = true;
+            }
+            mOptions = null;
         }
         if (differentTopTask) {
             logPIOnlyCreatorAllowsBAL();
@@ -2850,20 +2856,6 @@
                 mRootWindowContainer.getDefaultTaskDisplayArea(), mTargetRootTask);
     }
 
-    private boolean inTopNonFinishingTask(ActivityRecord r) {
-        if (r == null || r.getTask() == null) {
-            return false;
-        }
-
-        final Task rTask = r.getTask();
-        final Task parent = rTask.getCreatedByOrganizerTask() != null
-                ? rTask.getCreatedByOrganizerTask() : r.getRootTask();
-        final ActivityRecord topNonFinishingActivity = parent != null
-                ? parent.getTopNonFinishingActivity() : null;
-
-        return topNonFinishingActivity != null && topNonFinishingActivity.getTask() == rTask;
-    }
-
     private void resumeTargetRootTaskIfNeeded() {
         if (mDoResume) {
             final ActivityRecord next = mTargetRootTask.topRunningActivity(
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 908c49e..dbae29b 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -62,6 +62,7 @@
 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RTL;
 import static android.provider.Settings.Global.HIDE_ERROR_DIALOGS;
 import static android.provider.Settings.System.FONT_SCALE;
+import static android.service.controls.flags.Flags.homePanelDream;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManager.TRANSIT_CHANGE;
@@ -1501,14 +1502,19 @@
         a.exported = true;
         a.name = DreamActivity.class.getName();
         a.enabled = true;
-        a.launchMode = ActivityInfo.LAUNCH_SINGLE_INSTANCE;
         a.persistableMode = ActivityInfo.PERSIST_NEVER;
         a.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
         a.colorMode = ActivityInfo.COLOR_MODE_DEFAULT;
         a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS;
-        a.resizeMode = RESIZE_MODE_UNRESIZEABLE;
         a.configChanges = 0xffffffff;
 
+        if (homePanelDream()) {
+            a.launchMode = ActivityInfo.LAUNCH_SINGLE_TASK;
+        } else {
+            a.resizeMode = RESIZE_MODE_UNRESIZEABLE;
+            a.launchMode = ActivityInfo.LAUNCH_SINGLE_INSTANCE;
+        }
+
         final ActivityOptions options = ActivityOptions.makeBasic();
         options.setLaunchActivityType(ACTIVITY_TYPE_DREAM);
 
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index e59601c..10efb94 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -906,7 +906,6 @@
                 }
                 mService.getPackageManagerInternalLocked().notifyPackageUse(
                         r.intent.getComponent().getPackageName(), NOTIFY_PACKAGE_USE_ACTIVITY);
-                r.forceNewConfig = false;
                 mService.getAppWarningsLocked().onStartActivity(r);
 
                 // Because we could be starting an Activity in the system process this may not go
diff --git a/services/core/java/com/android/server/wm/CompatModePackages.java b/services/core/java/com/android/server/wm/CompatModePackages.java
index 73bcc8d..1a8927e 100644
--- a/services/core/java/com/android/server/wm/CompatModePackages.java
+++ b/services/core/java/com/android/server/wm/CompatModePackages.java
@@ -19,7 +19,6 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
 import static com.android.server.wm.CompatScaleProvider.COMPAT_SCALE_MODE_SYSTEM_FIRST;
 import static com.android.server.wm.CompatScaleProvider.COMPAT_SCALE_MODE_SYSTEM_LAST;
 
@@ -47,6 +46,7 @@
 import android.util.DisplayMetrics;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.util.Xml;
 
 import com.android.internal.protolog.common.ProtoLog;
@@ -60,6 +60,7 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
@@ -347,6 +348,7 @@
     private GameManagerInternal mGameManager;
     private final AtomicFile mFile;
     private final HashMap<String, Integer> mPackages = new HashMap<>();
+    private final SparseBooleanArray mLegacyScreenCompatPackages = new SparseBooleanArray();
     private final CompatHandler mHandler;
 
     private final SparseArray<CompatScaleProvider> mProviders = new SparseArray<>();
@@ -427,6 +429,7 @@
             mPackages.remove(packageName);
             scheduleWrite();
         }
+        mLegacyScreenCompatPackages.delete(packageName.hashCode());
     }
 
     public void handlePackageAddedLocked(String packageName, boolean updated) {
@@ -458,6 +461,17 @@
         mHandler.sendMessageDelayed(msg, 10000);
     }
 
+    /**
+     * Returns {@code true} if the windows belonging to the package should be scaled with
+     * {@link DisplayContent#mCompatibleScreenScale}.
+     */
+    boolean useLegacyScreenCompatMode(String packageName) {
+        if (mLegacyScreenCompatPackages.size() == 0) {
+            return false;
+        }
+        return mLegacyScreenCompatPackages.get(packageName.hashCode());
+    }
+
     public CompatibilityInfo compatibilityInfoForPackageLocked(ApplicationInfo ai) {
         final boolean forceCompat = getPackageCompatModeEnabledLocked(ai);
         final CompatScale compatScale = getCompatScaleFromProvider(ai.packageName, ai.uid);
@@ -466,8 +480,18 @@
                 : getCompatScale(ai.packageName, ai.uid, /* checkProvider= */ false);
         final float densityScale = compatScale != null ? compatScale.mDensityScaleFactor : appScale;
         final Configuration config = mService.getGlobalConfiguration();
-        return new CompatibilityInfo(ai, config.screenLayout, config.smallestScreenWidthDp,
-                forceCompat, appScale, densityScale);
+        final CompatibilityInfo info = new CompatibilityInfo(ai, config.screenLayout,
+                config.smallestScreenWidthDp, forceCompat, appScale, densityScale);
+        // Ignore invalid info which may be a placeholder of isolated process.
+        if (ai.flags != 0 && ai.sourceDir != null) {
+            if (!info.supportsScreen() && !"android".equals(ai.packageName)) {
+                Slog.i(TAG, "Use legacy screen compat mode: " + ai.packageName);
+                mLegacyScreenCompatPackages.put(ai.packageName.hashCode(), true);
+            } else if (mLegacyScreenCompatPackages.size() > 0) {
+                mLegacyScreenCompatPackages.delete(ai.packageName.hashCode());
+            }
+        }
+        return info;
     }
 
     float getCompatScale(String packageName, int uid) {
@@ -718,14 +742,23 @@
 
             scheduleWrite();
 
-            final Task rootTask = mService.getTopDisplayFocusedRootTask();
-            ActivityRecord starting = rootTask.restartPackage(packageName);
-
+            final ArrayList<WindowProcessController> restartedApps = new ArrayList<>();
+            mService.mRootWindowContainer.forAllWindows(w -> {
+                final ActivityRecord ar = w.mActivityRecord;
+                if (ar != null) {
+                    if (ar.packageName.equals(packageName) && !restartedApps.contains(ar.app)) {
+                        ar.restartProcessIfVisible();
+                        restartedApps.add(ar.app);
+                    }
+                } else if (w.getProcess().mInfo.packageName.equals(packageName)) {
+                    w.updateGlobalScale();
+                }
+            }, true /* traverseTopToBottom */);
             // Tell all processes that loaded this package about the change.
             SparseArray<WindowProcessController> pidMap = mService.mProcessMap.getPidMap();
             for (int i = pidMap.size() - 1; i >= 0; i--) {
                 final WindowProcessController app = pidMap.valueAt(i);
-                if (!app.containsPackage(packageName)) {
+                if (!app.containsPackage(packageName) || restartedApps.contains(app)) {
                     continue;
                 }
                 try {
@@ -737,14 +770,6 @@
                 } catch (Exception e) {
                 }
             }
-
-            if (starting != null) {
-                starting.ensureActivityConfiguration(0 /* globalChanges */,
-                        false /* preserveWindow */);
-                // And we need to make sure at this point that all other activities
-                // are made visible with the correct configuration.
-                rootTask.ensureActivitiesVisible(starting, 0, !PRESERVE_WINDOWS);
-            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index ae10ce3..f8dc9c7 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -512,7 +512,11 @@
      */
     private final DisplayMetrics mCompatDisplayMetrics = new DisplayMetrics();
 
-    /** The desired scaling factor for compatible apps. */
+    /**
+     * The desired scaling factor for compatible apps. It limits the size of the window to be
+     * original size ([320x480] x density). Used to scale window for applications running under
+     * legacy compatibility mode.
+     */
     float mCompatibleScreenScale;
 
     /** @see #getCurrentOverrideConfigurationChanges */
@@ -4794,7 +4798,8 @@
             assignRelativeLayerForIme(getSyncTransaction(), true /* forceUpdate */);
             scheduleAnimation();
 
-            mWmService.mH.post(() -> InputMethodManagerInternal.get().onImeParentChanged());
+            mWmService.mH.post(
+                    () -> InputMethodManagerInternal.get().onImeParentChanged(getDisplayId()));
         } else if (mImeControlTarget != null && mImeControlTarget == mImeLayeringTarget) {
             // Even if the IME surface parent is not changed, the layer target belonging to the
             // parent may have changes. Then attempt to reassign if the IME control target is
@@ -7090,7 +7095,7 @@
         }
 
         @Override
-        public void notifyInsetsControlChanged() {
+        public void notifyInsetsControlChanged(int displayId) {
             final InsetsStateController stateController = getInsetsStateController();
             try {
                 mRemoteInsetsController.insetsControlChanged(stateController.getRawInsetsState(),
diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java
index 8ecbc17..b74eb56 100644
--- a/services/core/java/com/android/server/wm/InsetsControlTarget.java
+++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java
@@ -29,8 +29,10 @@
 
     /**
      * Notifies the control target that the insets control has changed.
+     *
+     * @param displayId the display hosting the window of this target
      */
-    default void notifyInsetsControlChanged() {
+    default void notifyInsetsControlChanged(int displayId) {
     };
 
     /**
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 7815679..3c556bf 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -728,7 +728,7 @@
         }
 
         @Override
-        public void notifyInsetsControlChanged() {
+        public void notifyInsetsControlChanged(int displayId) {
             mHandler.post(this);
         }
 
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index c4d0129..6b9fcf4 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -72,7 +72,7 @@
     };
     private final InsetsControlTarget mEmptyImeControlTarget = new InsetsControlTarget() {
         @Override
-        public void notifyInsetsControlChanged() {
+        public void notifyInsetsControlChanged(int displayId) {
             InsetsSourceControl[] controls = getControlsForDispatch(this);
             if (controls == null) {
                 return;
@@ -80,7 +80,7 @@
             for (InsetsSourceControl control : controls) {
                 if (control.getType() == WindowInsets.Type.ime()) {
                     mDisplayContent.mWmService.mH.post(() ->
-                            InputMethodManagerInternal.get().removeImeSurface());
+                            InputMethodManagerInternal.get().removeImeSurface(displayId));
                 }
             }
         }
@@ -370,9 +370,10 @@
                 provider.onSurfaceTransactionApplied();
             }
             final ArraySet<InsetsControlTarget> newControlTargets = new ArraySet<>();
+            int displayId = mDisplayContent.getDisplayId();
             for (int i = mPendingControlChanged.size() - 1; i >= 0; i--) {
                 final InsetsControlTarget controlTarget = mPendingControlChanged.valueAt(i);
-                controlTarget.notifyInsetsControlChanged();
+                controlTarget.notifyInsetsControlChanged(displayId);
                 if (mControlTargetProvidersMap.containsKey(controlTarget)) {
                     // We only collect targets who get controls, not lose controls.
                     newControlTargets.add(controlTarget);
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 671acfc..dbfcc22 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -34,7 +34,6 @@
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS;
 import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
-import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
 import static android.content.pm.ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY;
 import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY;
@@ -5917,22 +5916,6 @@
         return activities;
     }
 
-    ActivityRecord restartPackage(String packageName) {
-        ActivityRecord starting = topRunningActivity();
-
-        // All activities that came from the package must be
-        // restarted as if there was a config change.
-        forAllActivities(r -> {
-            if (!r.info.packageName.equals(packageName)) return;
-            r.forceNewConfig = true;
-            if (starting != null && r == starting && r.isVisibleRequested()) {
-                r.startFreezingScreenLocked(CONFIG_SCREEN_LAYOUT);
-            }
-        });
-
-        return starting;
-    }
-
     Task reuseOrCreateTask(ActivityInfo info, Intent intent, boolean toTop) {
         return reuseOrCreateTask(info, intent, null /*voiceSession*/, null /*voiceInteractor*/,
                 toTop, null /*activity*/, null /*source*/, null /*options*/);
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index f5f0dc6..9c21e4c 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -52,7 +52,6 @@
 import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.MATCH_PARENT;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NOT_MAGNIFIABLE;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY;
@@ -1247,13 +1246,14 @@
      * @see ActivityRecord#hasSizeCompatBounds()
      */
     boolean hasCompatScale() {
-        if ((mAttrs.privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0) {
-            return true;
-        }
         if (mAttrs.type == TYPE_APPLICATION_STARTING) {
             // Exclude starting window because it is not displayed by the application.
             return false;
         }
+        if (mWmService.mAtmService.mCompatModePackages.useLegacyScreenCompatMode(
+                mSession.mProcess.mInfo.packageName)) {
+            return true;
+        }
         return mActivityRecord != null && mActivityRecord.hasSizeCompatBounds()
                 || mOverrideScale != 1f;
     }
@@ -3775,7 +3775,7 @@
     }
 
     @Override
-    public void notifyInsetsControlChanged() {
+    public void notifyInsetsControlChanged(int displayId) {
         ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "notifyInsetsControlChanged for %s ", this);
         if (mRemoved) {
             return;
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index c625b1e..adbd3c9 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -595,7 +595,7 @@
             <!-- Sets the brightness mapping of the desired screen brightness to the corresponding
             lux for the current display -->
             <xs:element name="luxToBrightnessMapping" type="luxToBrightnessMapping"
-                        minOccurs="0" maxOccurs="1">
+                        minOccurs="0" maxOccurs="unbounded">
                 <xs:annotation name="final"/>
             </xs:element>
         </xs:sequence>
@@ -619,12 +619,20 @@
 
     This is used in place of config_autoBrightnessLevels and config_autoBrightnessLcdBacklightValues
     defined in the config XML resource.
+
+    On devices that allow users to choose from a set of predefined options in display
+    auto-brightness settings, multiple mappings for different modes and settings can be defined.
+
+    If no mode is specified, the mapping will be used for the default mode.
+    If no setting is specified, the mapping will be used for the normal brightness setting.
     -->
     <xs:complexType name="luxToBrightnessMapping">
         <xs:element name="map" type="nonNegativeFloatToFloatMap">
             <xs:annotation name="nonnull"/>
             <xs:annotation name="final"/>
         </xs:element>
+        <xs:element name="mode" type="xs:string" minOccurs="0"/>
+        <xs:element name="setting" type="xs:string" minOccurs="0"/>
     </xs:complexType>
 
     <!-- Represents a point in the display brightness mapping, representing the lux level from the
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 8c8c123..98c95ed 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -8,13 +8,12 @@
     method public final java.math.BigInteger getDarkeningLightDebounceIdleMillis();
     method public final java.math.BigInteger getDarkeningLightDebounceMillis();
     method public boolean getEnabled();
-    method public final com.android.server.display.config.LuxToBrightnessMapping getLuxToBrightnessMapping();
+    method public final java.util.List<com.android.server.display.config.LuxToBrightnessMapping> getLuxToBrightnessMapping();
     method public final void setBrighteningLightDebounceIdleMillis(java.math.BigInteger);
     method public final void setBrighteningLightDebounceMillis(java.math.BigInteger);
     method public final void setDarkeningLightDebounceIdleMillis(java.math.BigInteger);
     method public final void setDarkeningLightDebounceMillis(java.math.BigInteger);
     method public void setEnabled(boolean);
-    method public final void setLuxToBrightnessMapping(com.android.server.display.config.LuxToBrightnessMapping);
   }
 
   public class BlockingZoneConfig {
@@ -220,7 +219,11 @@
   public class LuxToBrightnessMapping {
     ctor public LuxToBrightnessMapping();
     method @NonNull public final com.android.server.display.config.NonNegativeFloatToFloatMap getMap();
+    method public String getMode();
+    method public String getSetting();
     method public final void setMap(@NonNull com.android.server.display.config.NonNegativeFloatToFloatMap);
+    method public void setMode(String);
+    method public void setSetting(String);
   }
 
   public class NitsMap {
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
index f69f628..022268d 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
@@ -1262,7 +1262,7 @@
         val apexModuleName = packageState.apexModuleName
         val packageName = packageState.packageName
         return when {
-            packageState.isVendor ->
+            packageState.isVendor || packageState.isOdm ->
                 permissionAllowlist.getVendorPrivilegedAppAllowlistState(
                     packageName,
                     permissionName
@@ -1471,12 +1471,15 @@
                     // In any case, don't grant a privileged permission to privileged vendor apps,
                     // if the permission's protectionLevel does not have the extra vendorPrivileged
                     // flag.
-                    if (packageState.isVendor && !permission.isVendorPrivileged) {
+                    if (
+                        (packageState.isVendor || packageState.isOdm) &&
+                            !permission.isVendorPrivileged
+                    ) {
                         Slog.w(
                             LOG_TAG,
                             "Permission $permissionName cannot be granted to privileged" +
-                                " vendor app $packageName because it isn't a vendorPrivileged" +
-                                " permission"
+                                " vendor (or odm) app $packageName because it isn't a" +
+                                " vendorPrivileged permission"
                         )
                         return false
                     }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java
index 189d9bb..c5a1ba1 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java
@@ -107,20 +107,6 @@
         468.5f,
     };
 
-    private static final int[] DISPLAY_LEVELS_INT = {
-        9,
-        30,
-        45,
-        62,
-        78,
-        96,
-        119,
-        146,
-        178,
-        221,
-        255
-    };
-
     private static final float[] DISPLAY_LEVELS = {
         0.03f,
         0.11f,
@@ -172,62 +158,23 @@
     DisplayWhiteBalanceController mMockDwbc;
 
     @Test
-    public void testSimpleStrategyMappingAtControlPoints_IntConfig() {
-        Resources res = createResources(DISPLAY_LEVELS_INT);
-        DisplayDeviceConfig ddc = createDdc();
+    public void testSimpleStrategyMappingAtControlPoints() {
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS).build();
         BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc,
-                AUTO_BRIGHTNESS_MODE_DEFAULT,
-                mMockDwbc);
+                AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         assertNotNull("BrightnessMappingStrategy should not be null", simple);
         for (int i = 0; i < LUX_LEVELS.length; i++) {
-            final float expectedLevel = MathUtils.map(PowerManager.BRIGHTNESS_OFF + 1,
-                    PowerManager.BRIGHTNESS_ON, PowerManager.BRIGHTNESS_MIN,
-                    PowerManager.BRIGHTNESS_MAX, DISPLAY_LEVELS_INT[i]);
-            assertEquals(expectedLevel,
-                    simple.getBrightness(LUX_LEVELS[i]), 0.0001f /*tolerance*/);
+            assertEquals(DISPLAY_LEVELS[i], simple.getBrightness(LUX_LEVELS[i]), TOLERANCE);
         }
     }
 
     @Test
-    public void testSimpleStrategyMappingBetweenControlPoints_IntConfig() {
-        Resources res = createResources(DISPLAY_LEVELS_INT);
-        DisplayDeviceConfig ddc = createDdc();
+    public void testSimpleStrategyMappingBetweenControlPoints() {
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS).build();
         BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc,
-                AUTO_BRIGHTNESS_MODE_DEFAULT,
-                mMockDwbc);
-        assertNotNull("BrightnessMappingStrategy should not be null", simple);
-        for (int i = 1; i < LUX_LEVELS.length; i++) {
-            final float lux = (LUX_LEVELS[i - 1] + LUX_LEVELS[i]) / 2;
-            final float backlight = simple.getBrightness(lux) * PowerManager.BRIGHTNESS_ON;
-            assertTrue("Desired brightness should be between adjacent control points.",
-                    backlight > DISPLAY_LEVELS_INT[i - 1]
-                            && backlight < DISPLAY_LEVELS_INT[i]);
-        }
-    }
-
-    @Test
-    public void testSimpleStrategyMappingAtControlPoints_FloatConfig() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(EMPTY_FLOAT_ARRAY, EMPTY_FLOAT_ARRAY, LUX_LEVELS,
-                EMPTY_FLOAT_ARRAY, DISPLAY_LEVELS);
-        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc,
-                AUTO_BRIGHTNESS_MODE_DEFAULT,
-                mMockDwbc);
-        assertNotNull("BrightnessMappingStrategy should not be null", simple);
-        for (int i = 0; i < LUX_LEVELS.length; i++) {
-            assertEquals(DISPLAY_LEVELS[i], simple.getBrightness(LUX_LEVELS[i]),
-                    /* tolerance= */ 0.0001f);
-        }
-    }
-
-    @Test
-    public void testSimpleStrategyMappingBetweenControlPoints_FloatConfig() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(EMPTY_FLOAT_ARRAY, EMPTY_FLOAT_ARRAY, LUX_LEVELS,
-                EMPTY_FLOAT_ARRAY, DISPLAY_LEVELS);
-        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc,
-                AUTO_BRIGHTNESS_MODE_DEFAULT,
-                mMockDwbc);
+                AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         assertNotNull("BrightnessMappingStrategy should not be null", simple);
         for (int i = 1; i < LUX_LEVELS.length; i++) {
             final float lux = (LUX_LEVELS[i - 1] + LUX_LEVELS[i]) / 2;
@@ -239,8 +186,8 @@
 
     @Test
     public void testSimpleStrategyIgnoresNewConfiguration() {
-        Resources res = createResources(DISPLAY_LEVELS_INT);
-        DisplayDeviceConfig ddc = createDdc();
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS).build();
         BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
 
@@ -255,25 +202,23 @@
 
     @Test
     public void testSimpleStrategyIgnoresNullConfiguration() {
-        Resources res = createResources(DISPLAY_LEVELS_INT);
-        DisplayDeviceConfig ddc = createDdc();
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS).build();
         BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
 
         strategy.setBrightnessConfiguration(null);
-        final int n = DISPLAY_LEVELS_INT.length;
-        final float expectedBrightness =
-                (float) DISPLAY_LEVELS_INT[n - 1] / PowerManager.BRIGHTNESS_ON;
+        final int n = DISPLAY_LEVELS.length;
+        final float expectedBrightness = DISPLAY_LEVELS[n - 1];
         assertEquals(expectedBrightness,
                 strategy.getBrightness(LUX_LEVELS[n - 1]), 0.0001f /*tolerance*/);
     }
 
     @Test
     public void testPhysicalStrategyMappingAtControlPoints() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT,
-                LUX_LEVELS, DISPLAY_LEVELS_NITS);
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS)
+                .build();
         BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         assertNotNull("BrightnessMappingStrategy should not be null", physical);
@@ -290,9 +235,9 @@
 
     @Test
     public void testPhysicalStrategyMappingBetweenControlPoints() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE,
-                LUX_LEVELS, DISPLAY_LEVELS_NITS);
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build();
         BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         assertNotNull("BrightnessMappingStrategy should not be null", physical);
@@ -309,9 +254,9 @@
 
     @Test
     public void testPhysicalStrategyUsesNewConfigurations() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS);
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS)
+                .build();
         BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
 
@@ -336,9 +281,9 @@
 
     @Test
     public void testPhysicalStrategyRecalculateSplines() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS);
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS)
+                .build();
         BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         float[] adjustedNits50p = new float[DISPLAY_RANGE_NITS.length];
@@ -381,9 +326,10 @@
 
     @Test
     public void testDefaultStrategyIsPhysical() {
-        Resources res = createResources(DISPLAY_LEVELS_INT);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS);
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS)
+                .build();
         BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         assertTrue(strategy instanceof BrightnessMappingStrategy.PhysicalMappingStrategy);
@@ -396,17 +342,17 @@
         float tmp = lux[idx];
         lux[idx] = lux[idx + 1];
         lux[idx + 1] = tmp;
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux, DISPLAY_LEVELS_NITS);
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(lux)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build();
         BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         assertNull(strategy);
 
         // And make sure we get the same result even if it's monotone but not increasing.
         lux[idx] = lux[idx + 1];
-        ddc = createDdc(DISPLAY_RANGE_NITS, DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux,
-                DISPLAY_LEVELS_NITS);
+        ddc = new DdcBuilder().setAutoBrightnessLevelsLux(lux)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build();
         strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT,
                 mMockDwbc);
         assertNull(strategy);
@@ -419,25 +365,25 @@
         // Make sure it's strictly increasing so that the only failure is the differing array
         // lengths
         lux[lux.length - 1] = lux[lux.length - 2] + 1;
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux, DISPLAY_LEVELS_NITS);
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(lux)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build();
         BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         assertNull(strategy);
 
-        res = createResources(DISPLAY_LEVELS_INT);
+        ddc = new DdcBuilder().setAutoBrightnessLevelsLux(lux)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS)
+                .setAutoBrightnessLevels(DISPLAY_LEVELS).build();
         strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT,
                 mMockDwbc);
         assertNull(strategy);
 
         // Extra backlight level
-        final int[] backlight = Arrays.copyOf(
-                DISPLAY_LEVELS_INT, DISPLAY_LEVELS_INT.length + 1);
+        final float[] backlight = Arrays.copyOf(DISPLAY_LEVELS, DISPLAY_LEVELS.length + 1);
         backlight[backlight.length - 1] = backlight[backlight.length - 2] + 1;
-        res = createResources(backlight);
-        ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, EMPTY_FLOAT_ARRAY);
+        res = createResources();
+        ddc = new DdcBuilder().setAutoBrightnessLevels(backlight).build();
         strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT,
                 mMockDwbc);
         assertNull(strategy);
@@ -445,9 +391,9 @@
         // Extra nits level
         final float[] nits = Arrays.copyOf(DISPLAY_RANGE_NITS, DISPLAY_LEVELS_NITS.length + 1);
         nits[nits.length - 1] = nits[nits.length - 2] + 1;
-        res = createResources(EMPTY_INT_ARRAY);
-        ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, nits);
+        res = createResources();
+        ddc = new DdcBuilder().setAutoBrightnessLevelsNits(nits)
+                .setAutoBrightnessLevels(EMPTY_FLOAT_ARRAY).build();
         strategy = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT,
                 mMockDwbc);
         assertNull(strategy);
@@ -455,40 +401,32 @@
 
     @Test
     public void testPhysicalStrategyRequiresNitsMapping() {
-        Resources res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
-        DisplayDeviceConfig ddc = createDdc(EMPTY_FLOAT_ARRAY /*nitsRange*/);
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setNitsRange(EMPTY_FLOAT_ARRAY).build();
         BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         assertNull(physical);
-
-        res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
-        physical = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT,
-                mMockDwbc);
-        assertNull(physical);
-
-        res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
-        physical = BrightnessMappingStrategy.create(res, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT,
-                mMockDwbc);
-        assertNull(physical);
     }
 
     @Test
     public void testStrategiesAdaptToUserDataPoint() {
-        Resources res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE,
-                LUX_LEVELS, DISPLAY_LEVELS_NITS);
+        Resources res = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build();
         assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc));
-        ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE);
-        res = createResources(DISPLAY_LEVELS_INT);
+        ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE)
+                .setAutoBrightnessLevels(DISPLAY_LEVELS).build();
+        res = createResources();
         assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc));
     }
 
     @Test
     public void testIdleModeConfigLoadsCorrectly() {
-        Resources res = createResourcesIdle(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE);
+        Resources res = createResources(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE);
+        DisplayDeviceConfig ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE)
+                .build();
 
         // Create an idle mode bms
         // This will fail if it tries to fetch the wrong configuration.
@@ -562,17 +500,11 @@
         assertEquals(minBrightness, strategy.getBrightness(LUX_LEVELS[0]), 0.0001f /*tolerance*/);
     }
 
-    private Resources createResources(int[] brightnessLevelsBacklight) {
-        return createResources(brightnessLevelsBacklight, EMPTY_INT_ARRAY, EMPTY_FLOAT_ARRAY);
+    private Resources createResources() {
+        return createResources(EMPTY_INT_ARRAY, EMPTY_FLOAT_ARRAY);
     }
 
-    private Resources createResourcesIdle(int[] luxLevelsIdle, float[] brightnessLevelsNitsIdle) {
-        return createResources(EMPTY_INT_ARRAY,
-                luxLevelsIdle, brightnessLevelsNitsIdle);
-    }
-
-    private Resources createResources(int[] brightnessLevelsBacklight, int[] luxLevelsIdle,
-            float[] brightnessLevelsNitsIdle) {
+    private Resources createResources(int[] luxLevelsIdle, float[] brightnessLevelsNitsIdle) {
 
         Resources mockResources = mock(Resources.class);
         if (luxLevelsIdle.length > 0) {
@@ -583,10 +515,6 @@
                     .thenReturn(luxLevelsIdleResource);
         }
 
-        when(mockResources.getIntArray(
-                com.android.internal.R.array.config_autoBrightnessLcdBacklightValues))
-                .thenReturn(brightnessLevelsBacklight);
-
         TypedArray mockBrightnessLevelNitsIdle = createFloatTypedArray(brightnessLevelsNitsIdle);
         when(mockResources.obtainTypedArray(
                 com.android.internal.R.array.config_autoBrightnessDisplayValuesNitsIdle))
@@ -604,41 +532,6 @@
         return mockResources;
     }
 
-    private DisplayDeviceConfig createDdc() {
-        return createDdc(DISPLAY_RANGE_NITS);
-    }
-
-    private DisplayDeviceConfig createDdc(float[] nitsArray) {
-        return createDdc(nitsArray, DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT);
-    }
-
-    private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray) {
-        DisplayDeviceConfig mockDdc = mock(DisplayDeviceConfig.class);
-        when(mockDdc.getNits()).thenReturn(nitsArray);
-        when(mockDdc.getBrightness()).thenReturn(backlightArray);
-        when(mockDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(LUX_LEVELS);
-        when(mockDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(EMPTY_FLOAT_ARRAY);
-        when(mockDdc.getAutoBrightnessBrighteningLevels()).thenReturn(EMPTY_FLOAT_ARRAY);
-        return mockDdc;
-    }
-
-    private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray,
-            float[] luxLevelsFloat, float[] brightnessLevelsNits) {
-        return createDdc(nitsArray, backlightArray, luxLevelsFloat, brightnessLevelsNits,
-                EMPTY_FLOAT_ARRAY);
-    }
-
-    private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray,
-            float[] luxLevelsFloat, float[] brightnessLevelsNits, float[] brightnessLevels) {
-        DisplayDeviceConfig mockDdc = mock(DisplayDeviceConfig.class);
-        when(mockDdc.getNits()).thenReturn(nitsArray);
-        when(mockDdc.getBrightness()).thenReturn(backlightArray);
-        when(mockDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(luxLevelsFloat);
-        when(mockDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(brightnessLevelsNits);
-        when(mockDdc.getAutoBrightnessBrighteningLevels()).thenReturn(brightnessLevels);
-        return mockDdc;
-    }
-
     private TypedArray createFloatTypedArray(float[] vals) {
         TypedArray mockArray = mock(TypedArray.class);
         when(mockArray.length()).thenAnswer(invocation -> {
@@ -677,10 +570,9 @@
         final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2);
         final float y3 = GAMMA_CORRECTION_SPLINE.interpolate(x3);
 
-        Resources resources = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
-                GAMMA_CORRECTION_NITS);
+        Resources resources = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(GAMMA_CORRECTION_LUX)
+                .setAutoBrightnessLevelsNits(GAMMA_CORRECTION_NITS).build();
         BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         // Let's start with a validity check:
@@ -708,10 +600,9 @@
         final float y1 = GAMMA_CORRECTION_SPLINE.interpolate(x1);
         final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2);
         final float y3 = GAMMA_CORRECTION_SPLINE.interpolate(x3);
-        Resources resources = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
-                GAMMA_CORRECTION_NITS);
+        Resources resources = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(GAMMA_CORRECTION_LUX)
+                .setAutoBrightnessLevelsNits(GAMMA_CORRECTION_NITS).build();
         BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         // Validity check:
@@ -736,10 +627,9 @@
     public void testGammaCorrectionExtremeChangeAtCenter() {
         // Extreme changes (e.g. setting brightness to 0.0 or 1.0) can't be gamma corrected, so we
         // just make sure the adjustment reflects the change.
-        Resources resources = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
-                GAMMA_CORRECTION_NITS);
+        Resources resources = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(GAMMA_CORRECTION_LUX)
+                .setAutoBrightnessLevelsNits(GAMMA_CORRECTION_NITS).build();
         BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         assertEquals(0.0f, strategy.getAutoBrightnessAdjustment(), /* delta= */ 0.0001f);
@@ -760,10 +650,9 @@
         final float y0 = GAMMA_CORRECTION_SPLINE.interpolate(x0);
         final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2);
         final float y4 = GAMMA_CORRECTION_SPLINE.interpolate(x4);
-        Resources resources = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
-                GAMMA_CORRECTION_NITS);
+        Resources resources = createResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(GAMMA_CORRECTION_LUX)
+                .setAutoBrightnessLevelsNits(GAMMA_CORRECTION_NITS).build();
         BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
                 AUTO_BRIGHTNESS_MODE_DEFAULT, mMockDwbc);
         // Validity, as per tradition:
@@ -790,11 +679,54 @@
 
     @Test
     public void testGetMode() {
-        Resources res = createResourcesIdle(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE);
+        Resources res = createResources(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE);
+        DisplayDeviceConfig ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE)
+                .build();
         BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc,
                 AUTO_BRIGHTNESS_MODE_IDLE,
                 mMockDwbc);
         assertEquals(AUTO_BRIGHTNESS_MODE_IDLE, strategy.getMode());
     }
+
+    private static class DdcBuilder {
+        private DisplayDeviceConfig mDdc;
+
+        DdcBuilder() {
+            mDdc = mock(DisplayDeviceConfig.class);
+            when(mDdc.getNits()).thenReturn(DISPLAY_RANGE_NITS);
+            when(mDdc.getBrightness()).thenReturn(DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT);
+            when(mDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(LUX_LEVELS);
+            when(mDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(EMPTY_FLOAT_ARRAY);
+            when(mDdc.getAutoBrightnessBrighteningLevels()).thenReturn(EMPTY_FLOAT_ARRAY);
+        }
+
+        DdcBuilder setNitsRange(float[] nitsArray) {
+            when(mDdc.getNits()).thenReturn(nitsArray);
+            return this;
+        }
+
+        DdcBuilder setBrightnessRange(float[] brightnessArray) {
+            when(mDdc.getBrightness()).thenReturn(brightnessArray);
+            return this;
+        }
+
+        DdcBuilder setAutoBrightnessLevelsLux(float[] luxLevels) {
+            when(mDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(luxLevels);
+            return this;
+        }
+
+        DdcBuilder setAutoBrightnessLevelsNits(float[] brightnessLevelsNits) {
+            when(mDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(brightnessLevelsNits);
+            return this;
+        }
+
+        DdcBuilder setAutoBrightnessLevels(float[] brightnessLevels) {
+            when(mDdc.getAutoBrightnessBrighteningLevels()).thenReturn(brightnessLevels);
+            return this;
+        }
+
+        DisplayDeviceConfig build() {
+            return mDdc;
+        }
+    }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 31d7e88..a4c15b5 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -17,6 +17,7 @@
 package com.android.server.display;
 
 
+import static com.android.internal.display.BrightnessSynchronizer.brightnessIntToFloat;
 import static com.android.server.display.config.SensorData.SupportedMode;
 import static com.android.server.display.utils.DeviceConfigParsingUtils.ambientBrightnessThresholdsIntToFloat;
 import static com.android.server.display.utils.DeviceConfigParsingUtils.displayBrightnessThresholdsIntToFloat;
@@ -47,7 +48,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
-import com.android.internal.display.BrightnessSynchronizer;
 import com.android.server.display.config.HdrBrightnessData;
 import com.android.server.display.config.ThermalStatus;
 import com.android.server.display.feature.DisplayManagerFlags;
@@ -609,6 +609,9 @@
                 float[]{2.0f, 200.0f, 600.0f}, ZERO_DELTA);
         assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new
                 float[]{0.0f, 110.0f, 500.0f}, ZERO_DELTA);
+        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(), new
+                float[]{brightnessIntToFloat(50), brightnessIntToFloat(100),
+                brightnessIntToFloat(150)}, SMALL_DELTA);
 
         // Test thresholds
         assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), ZERO_DELTA);
@@ -674,7 +677,7 @@
         assertEquals("test_light_sensor", mDisplayDeviceConfig.getAmbientLightSensor().type);
         assertEquals("", mDisplayDeviceConfig.getAmbientLightSensor().name);
 
-        assertEquals(BrightnessSynchronizer.brightnessIntToFloat(35),
+        assertEquals(brightnessIntToFloat(35),
                 mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA);
     }
 
@@ -737,6 +740,27 @@
                 mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), ZERO_DELTA);
         assertArrayEquals(new float[]{0.2f, 0.3f},
                 mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(), SMALL_DELTA);
+
+        assertArrayEquals(new float[]{0.0f, 90},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux("default", "dim"),
+                ZERO_DELTA);
+        assertArrayEquals(new float[]{0.3f, 0.4f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels("default", "dim"),
+                SMALL_DELTA);
+
+        assertArrayEquals(new float[]{0.0f, 95},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux("doze", "normal"),
+                ZERO_DELTA);
+        assertArrayEquals(new float[]{0.35f, 0.45f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels("doze", "normal"),
+                SMALL_DELTA);
+
+        assertArrayEquals(new float[]{0.0f, 100},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux("doze", "bright"),
+                ZERO_DELTA);
+        assertArrayEquals(new float[]{0.4f, 0.5f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels("doze", "bright"),
+                SMALL_DELTA);
     }
 
     @Test
@@ -746,7 +770,9 @@
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
                 getValidProxSensor(), /* includeIdleMode= */ false));
 
-        assertNull(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels());
+        assertArrayEquals(new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100),
+                        brightnessIntToFloat(150)},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(), SMALL_DELTA);
         assertArrayEquals(new float[]{0, 110, 500},
                 mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), ZERO_DELTA);
         assertArrayEquals(new float[]{2, 200, 600},
@@ -1138,6 +1164,46 @@
                 +               "</point>\n"
                 +           "</map>\n"
                 +       "</luxToBrightnessMapping>\n"
+                +       "<luxToBrightnessMapping>\n"
+                +           "<setting>dim</setting>\n"
+                +           "<map>\n"
+                +               "<point>\n"
+                +                   "<first>0</first>\n"
+                +                   "<second>0.3</second>\n"
+                +               "</point>\n"
+                +               "<point>\n"
+                +                   "<first>90</first>\n"
+                +                   "<second>0.4</second>\n"
+                +               "</point>\n"
+                +           "</map>\n"
+                +       "</luxToBrightnessMapping>\n"
+                +       "<luxToBrightnessMapping>\n"
+                +           "<mode>doze</mode>\n"
+                +           "<map>\n"
+                +               "<point>\n"
+                +                   "<first>0</first>\n"
+                +                   "<second>0.35</second>\n"
+                +               "</point>\n"
+                +               "<point>\n"
+                +                   "<first>95</first>\n"
+                +                   "<second>0.45</second>\n"
+                +               "</point>\n"
+                +           "</map>\n"
+                +       "</luxToBrightnessMapping>\n"
+                +       "<luxToBrightnessMapping>\n"
+                +           "<mode>doze</mode>\n"
+                +           "<setting>bright</setting>\n"
+                +           "<map>\n"
+                +               "<point>\n"
+                +                   "<first>0</first>\n"
+                +                   "<second>0.4</second>\n"
+                +               "</point>\n"
+                +               "<point>\n"
+                +                   "<first>100</first>\n"
+                +                   "<second>0.5</second>\n"
+                +               "</point>\n"
+                +           "</map>\n"
+                +       "</luxToBrightnessMapping>\n"
                 +   "</autoBrightness>\n"
                 +  getPowerThrottlingConfig()
                 +   "<highBrightnessMode enabled=\"true\">\n"
@@ -1435,6 +1501,10 @@
         when(mResources.getIntArray(
                 com.android.internal.R.array.config_autoBrightnessLevels))
                 .thenReturn(screenBrightnessLevelLux);
+        int[] screenBrightnessLevels = new int[]{50, 100, 150};
+        when(mResources.getIntArray(
+                com.android.internal.R.array.config_autoBrightnessLcdBacklightValues))
+                .thenReturn(screenBrightnessLevels);
 
         // Thresholds
         // Config.xml requires the levels arrays to be of length N and the thresholds arrays to be
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
index dea838d..fbb14c3 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -88,4 +89,12 @@
 
         verify(mDisplayPowerController).setBrightnessFromOffload(brightness);
     }
+
+    @Test
+    public void testBlockScreenOn() {
+        Runnable unblocker = () -> {};
+        mSession.blockScreenOn(unblocker);
+
+        verify(mDisplayOffloader).onBlockingScreenOn(eq(unblocker));
+    }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index f36854b..00f9892 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -206,6 +206,9 @@
         when(mMockedResources.getIntArray(
             com.android.internal.R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate))
             .thenReturn(new int[]{});
+        when(mMockedResources.getIntArray(
+                com.android.internal.R.array.config_autoBrightnessLcdBacklightValues))
+                .thenReturn(new int[]{});
         doReturn(true).when(mFlags).isDisplayOffloadEnabled();
         initDisplayOffloadSession();
     }
@@ -1235,6 +1238,9 @@
 
             @Override
             public void stopOffload() {}
+
+            @Override
+            public void onBlockingScreenOn(Runnable unblocker) {}
         });
 
         mDisplayOffloadSession = new DisplayOffloadSessionImpl(mDisplayOffloader,
diff --git a/services/tests/media/OWNERS b/services/tests/media/OWNERS
new file mode 100644
index 0000000..160767a6
--- /dev/null
+++ b/services/tests/media/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 137631
+include platform/frameworks/av:/media/janitors/media_solutions_OWNERS
diff --git a/services/tests/media/mediarouterservicetest/Android.bp b/services/tests/media/mediarouterservicetest/Android.bp
new file mode 100644
index 0000000..aed3af6
--- /dev/null
+++ b/services/tests/media/mediarouterservicetest/Android.bp
@@ -0,0 +1,39 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "MediaRouterServiceTests",
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.rules",
+        "androidx.test.runner",
+        "compatibility-device-util-axt",
+        "junit",
+        "platform-test-annotations",
+        "services.core",
+        "truth",
+    ],
+
+    platform_apis: true,
+
+    test_suites: [
+        // "device-tests",
+        "general-tests",
+    ],
+
+    certificate: "platform",
+    dxflags: ["--multi-dex"],
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/services/tests/media/mediarouterservicetest/AndroidManifest.xml b/services/tests/media/mediarouterservicetest/AndroidManifest.xml
new file mode 100644
index 0000000..fe65f86
--- /dev/null
+++ b/services/tests/media/mediarouterservicetest/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.server.media.tests">
+
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
+
+    <application android:testOnly="true" android:debuggable="true">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.server.media.tests"
+         android:label="Frameworks Services Tests"/>
+</manifest>
diff --git a/services/tests/media/mediarouterservicetest/AndroidTest.xml b/services/tests/media/mediarouterservicetest/AndroidTest.xml
new file mode 100644
index 0000000..b065681
--- /dev/null
+++ b/services/tests/media/mediarouterservicetest/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs MediaRouter Service tests.">
+    <option name="test-tag" value="MediaRouterServiceTests" />
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="MediaRouterServiceTests.apk"/>
+        <option name="install-arg" value="-t" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.server.media.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java
new file mode 100644
index 0000000..6f9b6fa
--- /dev/null
+++ b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.media;
+
+import static com.android.server.media.AudioRoutingUtils.ATTRIBUTES_MEDIA;
+import static com.android.server.media.AudioRoutingUtils.getMediaAudioProductStrategy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceCallback;
+import android.media.AudioDeviceInfo;
+import android.media.AudioDevicePort;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.media.MediaRoute2Info;
+import android.media.audiopolicy.AudioProductStrategy;
+import android.os.Looper;
+import android.os.UserHandle;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class AudioPoliciesDeviceRouteControllerTest {
+
+    private static final String FAKE_ROUTE_NAME = "fake name";
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_SPEAKER, "name_builtin", /* address= */ null);
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_WIRED_HEADSET, "name_wired_hs", /* address= */ null);
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "name_a2dp", /* address= */ "12:34:45");
+
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_EARPIECE, /* name= */ null, /* address= */ null);
+
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_NO_NAME =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET,
+                    /* name= */ null,
+                    /* address= */ null);
+
+    private AudioDeviceInfo mSelectedAudioDeviceInfo;
+    private Set<AudioDeviceInfo> mAvailableAudioDeviceInfos;
+    @Mock private AudioManager mMockAudioManager;
+    @Mock private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
+    private AudioPoliciesDeviceRouteController mControllerUnderTest;
+    private AudioDeviceCallback mAudioDeviceCallback;
+    private AudioProductStrategy mMediaAudioProductStrategy;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        Resources mockResources = Mockito.mock(Resources.class);
+        when(mockResources.getText(anyInt())).thenReturn(FAKE_ROUTE_NAME);
+        Context realContext = InstrumentationRegistry.getInstrumentation().getContext();
+        Context mockContext = Mockito.mock(Context.class);
+        when(mockContext.getResources()).thenReturn(mockResources);
+        // The bluetooth stack needs the application info, but we cannot use a spy because the
+        // concrete class is package private, so we just return the application info through the
+        // mock.
+        when(mockContext.getApplicationInfo()).thenReturn(realContext.getApplicationInfo());
+
+        // Setup the initial state so that the route controller is created in a sensible state.
+        mSelectedAudioDeviceInfo = FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER;
+        mAvailableAudioDeviceInfos = Set.of(FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER);
+        updateMockAudioManagerState();
+        mMediaAudioProductStrategy = getMediaAudioProductStrategy();
+
+        BluetoothAdapter btAdapter =
+                realContext.getSystemService(BluetoothManager.class).getAdapter();
+        mControllerUnderTest =
+                new AudioPoliciesDeviceRouteController(
+                        mockContext,
+                        mMockAudioManager,
+                        Looper.getMainLooper(),
+                        mMediaAudioProductStrategy,
+                        btAdapter,
+                        mOnDeviceRouteChangedListener);
+        mControllerUnderTest.start(UserHandle.CURRENT_OR_SELF);
+
+        ArgumentCaptor<AudioDeviceCallback> deviceCallbackCaptor =
+                ArgumentCaptor.forClass(AudioDeviceCallback.class);
+        verify(mMockAudioManager)
+                .registerAudioDeviceCallback(deviceCallbackCaptor.capture(), any());
+        mAudioDeviceCallback = deviceCallbackCaptor.getValue();
+
+        // We clear any invocations during setup.
+        clearInvocations(mOnDeviceRouteChangedListener);
+    }
+
+    @Test
+    public void getSelectedRoute_afterDevicesConnect_returnsRightSelectedRoute() {
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP);
+        verify(mOnDeviceRouteChangedListener).onDeviceRouteChanged();
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ null, // Selected device doesn't change.
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+    }
+
+    @Test
+    public void getSelectedRoute_afterDeviceRemovals_returnsExpectedRoutes() {
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP,
+                FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        verify(mOnDeviceRouteChangedListener).onDeviceRouteChanged();
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP);
+        verify(mOnDeviceRouteChangedListener, times(2)).onDeviceRouteChanged();
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+
+        removeAvailableAudioDeviceInfos(
+                /* newSelectedDevice= */ null,
+                /* devicesToRemove...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+
+        removeAvailableAudioDeviceInfos(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER,
+                /* devicesToRemove...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+    }
+
+    @Test
+    public void onAudioDevicesAdded_clearsAudioRoutingPoliciesCorrectly() {
+        clearInvocations(mMockAudioManager);
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ null, // Selected device doesn't change.
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE);
+        verifyNoMoreInteractions(mMockAudioManager);
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP);
+        verify(mMockAudioManager).removePreferredDeviceForStrategy(mMediaAudioProductStrategy);
+    }
+
+    @Test
+    public void getAvailableDevices_ignoresInvalidMediaOutputs() {
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ null, // Selected device doesn't change.
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE);
+        verifyNoMoreInteractions(mOnDeviceRouteChangedListener);
+        assertThat(
+                        mControllerUnderTest.getAvailableRoutes().stream()
+                                .map(MediaRoute2Info::getType)
+                                .toList())
+                .containsExactly(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+    }
+
+    @Test
+    public void transferTo_setsTheExpectedRoutingPolicy() {
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP,
+                FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        MediaRoute2Info builtInSpeakerRoute =
+                getAvailableRouteWithType(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+        mControllerUnderTest.transferTo(builtInSpeakerRoute.getId());
+        verify(mMockAudioManager)
+                .setPreferredDeviceForStrategy(
+                        mMediaAudioProductStrategy,
+                        createAudioDeviceAttribute(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER));
+
+        MediaRoute2Info wiredHeadsetRoute =
+                getAvailableRouteWithType(MediaRoute2Info.TYPE_WIRED_HEADSET);
+        mControllerUnderTest.transferTo(wiredHeadsetRoute.getId());
+        verify(mMockAudioManager)
+                .setPreferredDeviceForStrategy(
+                        mMediaAudioProductStrategy,
+                        createAudioDeviceAttribute(AudioDeviceInfo.TYPE_WIRED_HEADSET));
+    }
+
+    @Test
+    public void updateVolume_propagatesCorrectlyToRouteInfo() {
+        when(mMockAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)).thenReturn(2);
+        when(mMockAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)).thenReturn(3);
+        when(mMockAudioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)).thenReturn(1);
+        when(mMockAudioManager.isVolumeFixed()).thenReturn(false);
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+
+        MediaRoute2Info selectedRoute = mControllerUnderTest.getSelectedRoute();
+        assertThat(selectedRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADSET);
+        assertThat(selectedRoute.getVolume()).isEqualTo(2);
+        assertThat(selectedRoute.getVolumeMax()).isEqualTo(3);
+        assertThat(selectedRoute.getVolumeHandling())
+                .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE);
+
+        MediaRoute2Info onlyTransferrableRoute =
+                mControllerUnderTest.getAvailableRoutes().stream()
+                        .filter(it -> !it.equals(selectedRoute))
+                        .findAny()
+                        .orElseThrow();
+        assertThat(onlyTransferrableRoute.getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+        assertThat(onlyTransferrableRoute.getVolume()).isEqualTo(0);
+        assertThat(onlyTransferrableRoute.getVolumeMax()).isEqualTo(0);
+        assertThat(onlyTransferrableRoute.getVolume()).isEqualTo(0);
+        assertThat(onlyTransferrableRoute.getVolumeHandling())
+                .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_FIXED);
+
+        when(mMockAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)).thenReturn(0);
+        when(mMockAudioManager.isVolumeFixed()).thenReturn(true);
+        mControllerUnderTest.updateVolume(0);
+        MediaRoute2Info newSelectedRoute = mControllerUnderTest.getSelectedRoute();
+        assertThat(newSelectedRoute.getVolume()).isEqualTo(0);
+        assertThat(newSelectedRoute.getVolumeHandling())
+                .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_FIXED);
+    }
+
+    @Test
+    public void getAvailableRoutes_whenNoProductNameIsProvided_usesTypeToPopulateName() {
+        assertThat(mControllerUnderTest.getSelectedRoute().getName().toString())
+                .isEqualTo(FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER.getProductName().toString());
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_NO_NAME,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_NO_NAME);
+
+        MediaRoute2Info selectedRoute = mControllerUnderTest.getSelectedRoute();
+        assertThat(selectedRoute.getName().toString()).isEqualTo(FAKE_ROUTE_NAME);
+    }
+
+    // Internal methods.
+
+    @NonNull
+    private MediaRoute2Info getAvailableRouteWithType(int type) {
+        return mControllerUnderTest.getAvailableRoutes().stream()
+                .filter(it -> it.getType() == type)
+                .findFirst()
+                .orElseThrow();
+    }
+
+    private void addAvailableAudioDeviceInfo(
+            @Nullable AudioDeviceInfo newSelectedDevice, AudioDeviceInfo... newAvailableDevices) {
+        Set<AudioDeviceInfo> newAvailableDeviceInfos = new HashSet<>(mAvailableAudioDeviceInfos);
+        newAvailableDeviceInfos.addAll(List.of(newAvailableDevices));
+        mAvailableAudioDeviceInfos = newAvailableDeviceInfos;
+        if (newSelectedDevice != null) {
+            mSelectedAudioDeviceInfo = newSelectedDevice;
+        }
+        updateMockAudioManagerState();
+        mAudioDeviceCallback.onAudioDevicesAdded(newAvailableDevices);
+    }
+
+    private void removeAvailableAudioDeviceInfos(
+            @Nullable AudioDeviceInfo newSelectedDevice, AudioDeviceInfo... devicesToRemove) {
+        Set<AudioDeviceInfo> newAvailableDeviceInfos = new HashSet<>(mAvailableAudioDeviceInfos);
+        List.of(devicesToRemove).forEach(newAvailableDeviceInfos::remove);
+        mAvailableAudioDeviceInfos = newAvailableDeviceInfos;
+        if (newSelectedDevice != null) {
+            mSelectedAudioDeviceInfo = newSelectedDevice;
+        }
+        updateMockAudioManagerState();
+        mAudioDeviceCallback.onAudioDevicesRemoved(devicesToRemove);
+    }
+
+    private void updateMockAudioManagerState() {
+        when(mMockAudioManager.getDevicesForAttributes(ATTRIBUTES_MEDIA))
+                .thenReturn(
+                        List.of(createAudioDeviceAttribute(mSelectedAudioDeviceInfo.getType())));
+        when(mMockAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
+                .thenReturn(mAvailableAudioDeviceInfos.toArray(new AudioDeviceInfo[0]));
+    }
+
+    private static AudioDeviceAttributes createAudioDeviceAttribute(int type) {
+        // Address is unused.
+        return new AudioDeviceAttributes(
+                AudioDeviceAttributes.ROLE_OUTPUT, type, /* address= */ "");
+    }
+
+    private static AudioDeviceInfo createAudioDeviceInfo(
+            int type, @NonNull String name, @NonNull String address) {
+        return new AudioDeviceInfo(AudioDevicePort.createForTesting(type, name, address));
+    }
+}
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 3b83e3c..fc2e5b0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
@@ -40,6 +40,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
@@ -109,6 +110,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Supplier;
 
 @RunWith(AndroidJUnit4.class)
@@ -126,6 +128,8 @@
 
     private MockitoSession mMockingSession;
     private String mPackageName;
+    private Map<String, Integer> mPackageCategories;
+    private Map<String, Integer> mPackageUids;
     private TestLooper mTestLooper;
     @Mock
     private PackageManager mMockPackageManager;
@@ -229,34 +233,50 @@
                 .strictness(Strictness.WARN)
                 .startMocking();
         mMockContext = new MockContext(InstrumentationRegistry.getContext());
+        mPackageCategories = new HashMap<>();
+        mPackageUids = new HashMap<>();
         mPackageName = mMockContext.getPackageName();
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_GAME);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME);
+        LocalServices.addService(PowerManagerInternal.class, mMockPowerManager);
+
+        mSetFlagsRule.enableFlags(Flags.FLAG_GAME_DEFAULT_FRAME_RATE);
+        mSetFlagsRule.disableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP);
+    }
+
+    private void mockAppCategory(String packageName, int packageUid,
+            @ApplicationInfo.Category int category)
+            throws Exception {
+        reset(mMockPackageManager);
+        mPackageCategories.put(packageName, category);
+        mPackageUids.put(packageName, packageUid);
+        final List<PackageInfo> packageInfos = new ArrayList<>();
+        for (Map.Entry<String, Integer> entry : mPackageCategories.entrySet()) {
+
+            packageName = entry.getKey();
+            packageUid = mPackageUids.get(packageName);
+            category = entry.getValue();
+            ApplicationInfo applicationInfo = new ApplicationInfo();
+            applicationInfo.packageName = packageName;
+            applicationInfo.category = category;
+            when(mMockPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(), anyInt()))
+                    .thenReturn(applicationInfo);
+
+            final PackageInfo pi = new PackageInfo();
+            pi.packageName = packageName;
+            pi.applicationInfo = applicationInfo;
+            packageInfos.add(pi);
+
+            when(mMockPackageManager.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(
+                    packageUid);
+            when(mMockPackageManager.getPackagesForUid(packageUid)).thenReturn(
+                    new String[]{packageName});
+        }
+        when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), anyInt()))
+                .thenReturn(packageInfos);
         final Resources resources =
                 InstrumentationRegistry.getInstrumentation().getContext().getResources();
         when(mMockPackageManager.getResourcesForApplication(anyString()))
                 .thenReturn(resources);
-        when(mMockPackageManager.getPackageUidAsUser(mPackageName, USER_ID_1)).thenReturn(
-                DEFAULT_PACKAGE_UID);
-        LocalServices.addService(PowerManagerInternal.class, mMockPowerManager);
-
-        mSetFlagsRule.enableFlags(Flags.FLAG_GAME_DEFAULT_FRAME_RATE);
-    }
-
-    private void mockAppCategory(String packageName, @ApplicationInfo.Category int category)
-            throws Exception {
-        reset(mMockPackageManager);
-        final ApplicationInfo gameApplicationInfo = new ApplicationInfo();
-        gameApplicationInfo.category = category;
-        gameApplicationInfo.packageName = packageName;
-        final PackageInfo pi = new PackageInfo();
-        pi.packageName = packageName;
-        pi.applicationInfo = gameApplicationInfo;
-        final List<PackageInfo> packages = new ArrayList<>();
-        packages.add(pi);
-        when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), anyInt()))
-                .thenReturn(packages);
-        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
-                .thenReturn(gameApplicationInfo);
     }
 
     @After
@@ -508,7 +528,7 @@
 
     @Test
     public void testGetGameMode_nonGame() throws Exception {
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_AUDIO);
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
         mockModifyGameModeGranted();
         assertEquals(GameManager.GAME_MODE_UNSUPPORTED,
@@ -780,7 +800,7 @@
 
     @Test
     public void testDeviceConfig_nonGame() throws Exception {
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_AUDIO);
         mockDeviceConfigAll();
         mockModifyGameModeGranted();
         checkReportedAvailableGameModes(createServiceAndStartUser(USER_ID_1));
@@ -1588,7 +1608,7 @@
 
     @Test
     public void testSetGameState_nonGame() throws Exception {
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_AUDIO);
         mockDeviceConfigNone();
         mockModifyGameModeGranted();
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
@@ -1611,14 +1631,14 @@
         gameManagerService.addGameStateListener(mockListener);
         verify(binder).linkToDeath(mDeathRecipientCaptor.capture(), anyInt());
 
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_AUDIO);
         GameState gameState = new GameState(true, GameState.MODE_NONE);
         gameManagerService.setGameState(mPackageName, gameState, USER_ID_1);
         assertFalse(gameManagerService.mHandler.hasMessages(SET_GAME_STATE));
         mTestLooper.dispatchAll();
         verify(mockListener, never()).onGameStateChanged(anyString(), any(), anyInt());
 
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_GAME);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME);
         gameState = new GameState(true, GameState.MODE_NONE);
         gameManagerService.setGameState(mPackageName, gameState, USER_ID_1);
         assertTrue(gameManagerService.mHandler.hasMessages(SET_GAME_STATE));
@@ -1654,7 +1674,7 @@
 
         gameManagerService.addGameStateListener(mockListener);
         gameManagerService.removeGameStateListener(mockListener);
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_GAME);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME);
         GameState gameState = new GameState(false, GameState.MODE_CONTENT);
         gameManagerService.setGameState(mPackageName, gameState, USER_ID_1);
         assertTrue(gameManagerService.mHandler.hasMessages(SET_GAME_STATE));
@@ -1670,13 +1690,17 @@
         return output;
     }
 
-    private void mockInterventionListForMultipleUsers() {
+    private void mockInterventionListForMultipleUsers() throws Exception {
         final String[] packageNames = new String[]{"com.android.app0",
                 "com.android.app1", "com.android.app2"};
+        int i = 1;
+        for (String p : packageNames) {
+            mockAppCategory(p, DEFAULT_PACKAGE_UID + i++, ApplicationInfo.CATEGORY_GAME);
+        }
 
         final ApplicationInfo[] applicationInfos = new ApplicationInfo[3];
         final PackageInfo[] pis = new PackageInfo[3];
-        for (int i = 0; i < 3; ++i) {
+        for (i = 0; i < 3; ++i) {
             applicationInfos[i] = new ApplicationInfo();
             applicationInfos[i].category = ApplicationInfo.CATEGORY_GAME;
             applicationInfos[i].packageName = packageNames[i];
@@ -1717,7 +1741,6 @@
                         new Injector());
         startUser(gameManagerService, USER_ID_1);
         startUser(gameManagerService, USER_ID_2);
-
         gameManagerService.setGameModeConfigOverride("com.android.app0", USER_ID_2,
                 GameManager.GAME_MODE_PERFORMANCE, "120", "0.6");
         gameManagerService.setGameModeConfigOverride("com.android.app2", USER_ID_2,
@@ -1953,7 +1976,7 @@
 
     @Test
     public void testUpdateCustomGameModeConfiguration_nonGame() throws Exception {
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_IMAGE);
         mockModifyGameModeGranted();
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
         gameManagerService.updateCustomGameModeConfiguration(mPackageName,
@@ -2013,13 +2036,14 @@
     }
 
     @Test
-    public void testWritingSettingFile_onShutdown() throws InterruptedException {
+    public void testWritingSettingFile_onShutdown() throws Exception {
         mockModifyGameModeGranted();
         mockDeviceConfigAll();
         GameManagerService gameManagerService = new GameManagerService(mMockContext);
         gameManagerService.onBootCompleted();
         startUser(gameManagerService, USER_ID_1);
         Thread.sleep(500);
+        mockAppCategory("com.android.app1", DEFAULT_PACKAGE_UID + 1, ApplicationInfo.CATEGORY_GAME);
         gameManagerService.setGameModeConfigOverride("com.android.app1", USER_ID_1,
                 GameManager.GAME_MODE_BATTERY, "60", "0.5");
         gameManagerService.setGameMode("com.android.app1", USER_ID_1,
@@ -2259,7 +2283,7 @@
         when(DeviceConfig.getProperty(anyString(), anyString()))
                 .thenReturn(configString);
         mockModifyGameModeGranted();
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_IMAGE);
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
         gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1);
         assertEquals(GameManager.GAME_MODE_UNSUPPORTED,
@@ -2277,9 +2301,7 @@
         mockModifyGameModeGranted();
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
         String someGamePkg = "some.game";
-        mockAppCategory(someGamePkg, ApplicationInfo.CATEGORY_GAME);
-        when(mMockPackageManager.getPackageUidAsUser(someGamePkg, USER_ID_1)).thenReturn(
-                DEFAULT_PACKAGE_UID + 1);
+        mockAppCategory(someGamePkg, DEFAULT_PACKAGE_UID + 1,  ApplicationInfo.CATEGORY_GAME);
         gameManagerService.setGameMode(someGamePkg, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1);
         assertEquals(GameManager.GAME_MODE_PERFORMANCE,
                 gameManagerService.getGameMode(someGamePkg, USER_ID_1));
@@ -2307,24 +2329,11 @@
     }
 
     @Test
-    public void testGamePowerMode_gamePackage() throws Exception {
-        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-        String[] packages = {mPackageName};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
-        gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
-        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
-    }
-
-    @Test
     public void testGamePowerMode_twoGames() throws Exception {
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-        String[] packages1 = {mPackageName};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages1);
         String someGamePkg = "some.game";
-        String[] packages2 = {someGamePkg};
         int somePackageId = DEFAULT_PACKAGE_UID + 1;
-        when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);
+        mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME);
         HashMap<Integer, Boolean> powerState = new HashMap<>();
         doAnswer(inv -> powerState.put(inv.getArgument(0), inv.getArgument(1)))
                 .when(mMockPowerManager).setPowerMode(anyInt(), anyBoolean());
@@ -2333,6 +2342,7 @@
         assertTrue(powerState.get(Mode.GAME));
         gameManagerService.mUidObserver.onUidStateChanged(
                 DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        assertFalse(powerState.get(Mode.GAME));
         gameManagerService.mUidObserver.onUidStateChanged(
                 somePackageId, ActivityManager.PROCESS_STATE_TOP, 0, 0);
         assertTrue(powerState.get(Mode.GAME));
@@ -2344,12 +2354,9 @@
     @Test
     public void testGamePowerMode_twoGamesOverlap() throws Exception {
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-        String[] packages1 = {mPackageName};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages1);
         String someGamePkg = "some.game";
-        String[] packages2 = {someGamePkg};
         int somePackageId = DEFAULT_PACKAGE_UID + 1;
-        when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);
+        mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME);
         gameManagerService.mUidObserver.onUidStateChanged(
                 DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
         gameManagerService.mUidObserver.onUidStateChanged(
@@ -2363,49 +2370,162 @@
     }
 
     @Test
-    public void testGamePowerMode_released() throws Exception {
-        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-        String[] packages = {mPackageName};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
-        gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
-        gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
-        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);
-    }
-
-    @Test
     public void testGamePowerMode_noPackage() throws Exception {
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
         String[] packages = {};
         when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
         gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
-        verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, true);
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
     }
 
     @Test
-    public void testGamePowerMode_notAGamePackage() throws Exception {
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE);
+    public void testGamePowerMode_gameAndNotGameApps_flagOn() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP);
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-        String[] packages = {"someapp"};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+
+        String nonGamePkg1 = "not.game1";
+        int nonGameUid1 = DEFAULT_PACKAGE_UID + 1;
+        mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE);
+
+        String nonGamePkg2 = "not.game2";
+        int nonGameUid2 = DEFAULT_PACKAGE_UID + 2;
+        mockAppCategory(nonGamePkg2, nonGameUid2, ApplicationInfo.CATEGORY_IMAGE);
+
+        String gamePkg1 = "game1";
+        int gameUid1 = DEFAULT_PACKAGE_UID + 3;
+        mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME);
+
+        String gamePkg2 = "game2";
+        int gameUid2 = DEFAULT_PACKAGE_UID + 4;
+        mockAppCategory(gamePkg2, gameUid2, ApplicationInfo.CATEGORY_GAME);
+
+        // non-game1 top and background with no-op
         gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
-        verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, true);
+                nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // game1 top to enable game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // non-game1 in foreground to disable game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // non-game2 in foreground with no-op
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid2, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // game 2 in foreground with no-op
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid2, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // non-game 1 in background with no-op
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // non-game2 in background to resume game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid2, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // game 1 in background with no-op
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // game 2 in background to stop game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid2, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
     }
 
     @Test
-    public void testGamePowerMode_notAGamePackageNotReleased() throws Exception {
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE);
+    public void testGamePowerMode_gameAndNotGameApps_flagOff() throws Exception {
+        mSetFlagsRule.disableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP);
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-        String[] packages = {"someapp"};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+
+        String nonGamePkg1 = "not.game1";
+        int nonGameUid1 = DEFAULT_PACKAGE_UID + 1;
+        mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE);
+
+        String gamePkg1 = "game1";
+        int gameUid1 = DEFAULT_PACKAGE_UID + 3;
+        mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME);
+
+        // non-game1 top and background with no-op
         gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+                nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
         gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
-        verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, false);
+                nonGameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // game1 top to enable game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // non-game1 in foreground to not interfere
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // non-game 1 in background to not interfere
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // move non-game1 to foreground again
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+
+        // with non-game1 on top, game 1 in background to still disable game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // with non-game1 on top, game 1 in foreground to still enable game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
     }
 
     @Test
@@ -2432,8 +2552,6 @@
         gameManagerService.onBootCompleted();
 
         // Set up a game in the foreground.
-        String[] packages = {mPackageName};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
         gameManagerService.mUidObserver.onUidStateChanged(
                 DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
 
@@ -2448,10 +2566,8 @@
 
         // Adding another game to the foreground.
         String anotherGamePkg = "another.game";
-        String[] packages2 = {anotherGamePkg};
-        mockAppCategory(anotherGamePkg, ApplicationInfo.CATEGORY_GAME);
         int somePackageId = DEFAULT_PACKAGE_UID + 1;
-        when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);
+        mockAppCategory(anotherGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME);
 
         gameManagerService.mUidObserver.onUidStateChanged(
                 somePackageId, ActivityManager.PROCESS_STATE_TOP, 0, 0);
@@ -2484,8 +2600,6 @@
                         }));
 
         // Set up a game in the foreground.
-        String[] packages = {mPackageName};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
         gameManagerService.mUidObserver.onUidStateChanged(
                 DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
 
@@ -2503,10 +2617,8 @@
 
         // Toggle game default frame rate off.
         String anotherGamePkg = "another.game";
-        String[] packages2 = {anotherGamePkg};
-        mockAppCategory(anotherGamePkg, ApplicationInfo.CATEGORY_GAME);
         int somePackageId = DEFAULT_PACKAGE_UID + 1;
-        when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);
+        mockAppCategory(anotherGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME);
         gameManagerService.mUidObserver.onUidStateChanged(
                 somePackageId, ActivityManager.PROCESS_STATE_TOP, 0, 0);
         gameManagerService.mUidObserver.onUidStateChanged(
diff --git a/services/tests/servicestests/src/com/android/server/audio/FadeConfigurationsTest.java b/services/tests/servicestests/src/com/android/server/audio/FadeConfigurationsTest.java
index 6fca561..69817ad 100644
--- a/services/tests/servicestests/src/com/android/server/audio/FadeConfigurationsTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/FadeConfigurationsTest.java
@@ -16,17 +16,21 @@
 
 package com.android.server.audio;
 
-import static android.media.AudioAttributes.USAGE_MEDIA;
-import static android.media.AudioAttributes.USAGE_GAME;
-import static android.media.AudioAttributes.USAGE_ASSISTANT;
 import static android.media.AudioAttributes.CONTENT_TYPE_SPEECH;
+import static android.media.AudioAttributes.USAGE_ASSISTANT;
+import static android.media.AudioAttributes.USAGE_EMERGENCY;
+import static android.media.AudioAttributes.USAGE_GAME;
+import static android.media.AudioAttributes.USAGE_MEDIA;
 import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_AAUDIO;
 import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL;
 import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK;
 import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_UNKNOWN;
+import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
 
 import android.media.AudioAttributes;
+import android.media.FadeManagerConfiguration;
 import android.media.VolumeShaper;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import com.google.common.truth.Expect;
 
@@ -42,6 +46,7 @@
 public final class FadeConfigurationsTest {
     private FadeConfigurations mFadeConfigurations;
     private static final long DEFAULT_FADE_OUT_DURATION_MS = 2_000;
+    private static final long DEFAULT_FADE_IN_DURATION_MS = 1_000;
     private static final long DEFAULT_DELAY_FADE_IN_OFFENDERS_MS = 2000;
     private static final long DURATION_FOR_UNFADEABLE_MS = 0;
     private static final int TEST_UID_SYSTEM = 1000;
@@ -60,11 +65,19 @@
     private static final VolumeShaper.Configuration DEFAULT_FADEOUT_VSHAPE =
             new VolumeShaper.Configuration.Builder()
                     .setId(PlaybackActivityMonitor.VOLUME_SHAPER_SYSTEM_FADEOUT_ID)
-                    .setCurve(/* times= */new float[]{0.f, 0.25f, 1.0f} ,
-                            /* volumes= */new float[]{1.f, 0.65f, 0.0f})
+                    .setCurve(/* times= */ new float[]{0.f, 0.25f, 1.0f},
+                            /* volumes= */ new float[]{1.f, 0.65f, 0.0f})
                     .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME)
                     .setDuration(DEFAULT_FADE_OUT_DURATION_MS)
                     .build();
+    private static final VolumeShaper.Configuration DEFAULT_FADEIN_VSHAPE =
+            new VolumeShaper.Configuration.Builder()
+                    .setId(PlaybackActivityMonitor.VOLUME_SHAPER_SYSTEM_FADEOUT_ID)
+                    .setCurve(/* times= */ new float[]{0.f, 0.50f, 1.0f},
+                            /* volumes= */ new float[]{0.f, 0.30f, 1.0f})
+                    .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME)
+                    .setDuration(DEFAULT_FADE_IN_DURATION_MS)
+                    .build();
 
     private static final AudioAttributes TEST_MEDIA_AUDIO_ATTRIBUTE =
             new AudioAttributes.Builder().setUsage(USAGE_MEDIA).build();
@@ -72,12 +85,18 @@
             new AudioAttributes.Builder().setUsage(USAGE_GAME).build();
     private static final AudioAttributes TEST_ASSISTANT_AUDIO_ATTRIBUTE =
             new AudioAttributes.Builder().setUsage(USAGE_ASSISTANT).build();
+    private static final AudioAttributes TEST_EMERGENCY_AUDIO_ATTRIBUTE =
+            new AudioAttributes.Builder().setSystemUsage(USAGE_EMERGENCY).build();
+
     private static final AudioAttributes TEST_SPEECH_AUDIO_ATTRIBUTE =
             new AudioAttributes.Builder().setContentType(CONTENT_TYPE_SPEECH).build();
 
     @Rule
     public final Expect expect = Expect.create();
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Before
     public void setUp() {
         mFadeConfigurations = new FadeConfigurations();
@@ -156,4 +175,110 @@
                 .that(mFadeConfigurations.isFadeable(TEST_GAME_AUDIO_ATTRIBUTE, TEST_UID_USER,
                         PLAYER_TYPE_AAUDIO)).isFalse();
     }
+
+    @Test
+    public void testGetFadeableUsages_withFadeManagerConfigurations_equals() {
+        mSetFlagsRule.enableFlags(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION);
+        List<Integer> usageList = List.of(AudioAttributes.USAGE_ALARM,
+                AudioAttributes.USAGE_EMERGENCY);
+        FadeManagerConfiguration fmc = createFadeMgrConfig(/* fadeableUsages= */ usageList,
+                /* unfadeableContentTypes= */ null, /* unfadeableUids= */ null,
+                /* unfadeableAudioAttrs= */ null);
+        FadeConfigurations fadeConfigs = new FadeConfigurations();
+
+        fadeConfigs.setFadeManagerConfiguration(fmc);
+
+        expect.withMessage("Fadeable usages with fade manager configuration")
+                .that(fadeConfigs.getFadeableUsages()).isEqualTo(fmc.getFadeableUsages());
+    }
+
+    @Test
+    public void testGetUnfadeableContentTypes_withFadeManagerConfigurations_equals() {
+        mSetFlagsRule.enableFlags(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION);
+        List<Integer> contentTypesList = List.of(AudioAttributes.CONTENT_TYPE_MUSIC,
+                AudioAttributes.CONTENT_TYPE_MOVIE);
+        FadeManagerConfiguration fmc = createFadeMgrConfig(/* fadeableUsages= */ null,
+                /* unfadeableContentTypes= */ contentTypesList, /* unfadeableUids= */ null,
+                /* unfadeableAudioAttrs= */ null);
+        FadeConfigurations fadeConfigs = new FadeConfigurations();
+
+        fadeConfigs.setFadeManagerConfiguration(fmc);
+
+        expect.withMessage("Unfadeable content types with fade manager configuration")
+                .that(fadeConfigs.getUnfadeableContentTypes())
+                .isEqualTo(fmc.getUnfadeableContentTypes());
+    }
+
+    @Test
+    public void testGetUnfadeableAudioAttributes_withFadeManagerConfigurations_equals() {
+        mSetFlagsRule.enableFlags(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION);
+        List<AudioAttributes> attrsList = List.of(TEST_ASSISTANT_AUDIO_ATTRIBUTE,
+                TEST_EMERGENCY_AUDIO_ATTRIBUTE);
+        FadeManagerConfiguration fmc = createFadeMgrConfig(/* fadeableUsages= */ null,
+                /* unfadeableContentTypes= */ null, /* unfadeableUids= */ null,
+                /* unfadeableAudioAttrs= */ attrsList);
+        FadeConfigurations fadeConfigs = new FadeConfigurations();
+
+        fadeConfigs.setFadeManagerConfiguration(fmc);
+
+        expect.withMessage("Unfadeable audio attributes with fade manager configuration")
+                .that(fadeConfigs.getUnfadeableAudioAttributes())
+                .isEqualTo(fmc.getUnfadeableAudioAttributes());
+    }
+
+    @Test
+    public void testGetUnfadeableUids_withFadeManagerConfigurations_equals() {
+        mSetFlagsRule.enableFlags(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION);
+        List<Integer> uidsList = List.of(TEST_UID_SYSTEM, TEST_UID_USER);
+        FadeManagerConfiguration fmc = createFadeMgrConfig(/* fadeableUsages= */ null,
+                /* unfadeableContentTypes= */ null, /* unfadeableUids= */ uidsList,
+                /* unfadeableAudioAttrs= */ null);
+        FadeConfigurations fadeConfigs = new FadeConfigurations();
+
+        fadeConfigs.setFadeManagerConfiguration(fmc);
+
+        expect.withMessage("Unfadeable uids with fade manager configuration")
+                .that(fadeConfigs.getUnfadeableUids()).isEqualTo(fmc.getUnfadeableUids());
+    }
+
+    private static FadeManagerConfiguration createFadeMgrConfig(List<Integer> fadeableUsages,
+            List<Integer> unfadeableContentTypes, List<Integer> unfadeableUids,
+            List<AudioAttributes> unfadeableAudioAttrs) {
+        FadeManagerConfiguration.Builder builder = new FadeManagerConfiguration.Builder();
+        if (fadeableUsages != null) {
+            builder.setFadeableUsages(fadeableUsages);
+        }
+        if (unfadeableContentTypes != null) {
+            builder.setUnfadeableContentTypes(unfadeableContentTypes);
+        }
+        if (unfadeableUids != null) {
+            builder.setUnfadeableUids(unfadeableUids);
+        }
+        if (unfadeableAudioAttrs != null) {
+            builder.setUnfadeableAudioAttributes(unfadeableAudioAttrs);
+        }
+        if (fadeableUsages != null) {
+            for (int index = 0; index < fadeableUsages.size(); index++) {
+                builder.setFadeOutVolumeShaperConfigForAudioAttributes(
+                        createGenericAudioAttributesForUsage(fadeableUsages.get(index)),
+                        DEFAULT_FADEOUT_VSHAPE);
+            }
+        }
+        if (fadeableUsages != null) {
+            for (int index = 0; index < fadeableUsages.size(); index++) {
+                builder.setFadeInVolumeShaperConfigForAudioAttributes(
+                        createGenericAudioAttributesForUsage(fadeableUsages.get(index)),
+                        DEFAULT_FADEIN_VSHAPE);
+            }
+        }
+
+        return builder.build();
+    }
+
+    private static AudioAttributes createGenericAudioAttributesForUsage(int usage) {
+        if (AudioAttributes.isSystemUsage(usage)) {
+            return new AudioAttributes.Builder().setSystemUsage(usage).build();
+        }
+        return new AudioAttributes.Builder().setUsage(usage).build();
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java
index 65059d5..fa94821 100644
--- a/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java
@@ -23,8 +23,6 @@
 import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK;
 import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_UNKNOWN;
 
-import static org.junit.Assert.assertThrows;
-
 import android.content.Context;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
@@ -75,20 +73,11 @@
 
     @Before
     public void setUp() {
-        mFadeOutManager = new FadeOutManager(new FadeConfigurations());
+        mFadeOutManager = new FadeOutManager();
         mContext = ApplicationProvider.getApplicationContext();
     }
 
     @Test
-    public void constructor_nullFadeConfigurations_fails() {
-        Throwable thrown = assertThrows(NullPointerException.class, () -> new FadeOutManager(
-                /* FadeConfigurations= */ null));
-
-        expect.withMessage("Constructor exception")
-                .that(thrown).hasMessageThat().contains("Fade configurations can not be null");
-    }
-
-    @Test
     public void testCanCauseFadeOut_forFaders_returnsTrue() {
         FocusRequester winner = createFocusRequester(TEST_MEDIA_AUDIO_ATTRIBUTE, "winning-client",
                 "unit-test", TEST_UID_USER,
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
index 3b5cae3..88b2ed4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
@@ -50,14 +50,22 @@
 import android.hardware.biometrics.IBiometricService;
 import android.hardware.biometrics.IBiometricServiceReceiver;
 import android.hardware.biometrics.PromptInfo;
+import android.hardware.biometrics.fingerprint.SensorProps;
+import android.hardware.face.FaceSensorConfigurations;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.face.IFaceService;
+import android.hardware.fingerprint.FingerprintSensorConfigurations;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintService;
 import android.hardware.iris.IIrisService;
 import android.os.Binder;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.InstrumentationRegistry;
@@ -89,6 +97,9 @@
 
     @Rule
     public MockitoRule mockitorule = MockitoJUnit.rule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
 
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
@@ -118,6 +129,10 @@
     @Captor
     private ArgumentCaptor<List<FingerprintSensorPropertiesInternal>> mFingerprintPropsCaptor;
     @Captor
+    private ArgumentCaptor<FingerprintSensorConfigurations> mFingerprintSensorConfigurationsCaptor;
+    @Captor
+    private ArgumentCaptor<FaceSensorConfigurations> mFaceSensorConfigurationsCaptor;
+    @Captor
     private ArgumentCaptor<List<FaceSensorPropertiesInternal>> mFacePropsCaptor;
 
     @Before
@@ -143,6 +158,9 @@
         when(mContext.getResources()).thenReturn(mResources);
         when(mInjector.getBiometricService()).thenReturn(mBiometricService);
         when(mInjector.getConfiguration(any())).thenReturn(config);
+        when(mInjector.getFaceConfiguration(any())).thenReturn(config);
+        when(mInjector.getFingerprintConfiguration(any())).thenReturn(config);
+        when(mInjector.getIrisConfiguration(any())).thenReturn(config);
         when(mInjector.getFingerprintService()).thenReturn(mFingerprintService);
         when(mInjector.getFaceService()).thenReturn(mFaceService);
         when(mInjector.getIrisService()).thenReturn(mIrisService);
@@ -173,12 +191,13 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(com.android.server.biometrics.Flags.FLAG_DE_HIDL)
     public void testRegisterAuthenticator_registerAuthenticators() throws Exception {
         final int fingerprintId = 0;
         final int fingerprintStrength = 15;
 
         final int faceId = 1;
-        final int faceStrength = 4095;
+        final int faceStrength = 15;
 
         final String[] config = {
                 // ID0:Fingerprint:Strong
@@ -206,6 +225,51 @@
                 Utils.authenticatorStrengthToPropertyStrength(faceStrength));
     }
 
+    @Test
+    @RequiresFlagsEnabled(com.android.server.biometrics.Flags.FLAG_DE_HIDL)
+    public void testRegisterAuthenticator_registerAuthenticatorsLegacy() throws RemoteException {
+        final int fingerprintId = 0;
+        final int fingerprintStrength = 15;
+
+        final int faceId = 1;
+        final int faceStrength = 4095;
+
+        final String[] config = {
+                // ID0:Fingerprint:Strong
+                String.format("%d:2:%d", fingerprintId, fingerprintStrength),
+                // ID2:Face:Convenience
+                String.format("%d:8:%d", faceId, faceStrength)
+        };
+
+        when(mInjector.getFingerprintConfiguration(any())).thenReturn(config);
+        when(mInjector.getFaceConfiguration(any())).thenReturn(config);
+        when(mInjector.getFingerprintAidlInstances()).thenReturn(new String[]{});
+        when(mInjector.getFaceAidlInstances()).thenReturn(new String[]{});
+
+        mAuthService = new AuthService(mContext, mInjector);
+        mAuthService.onStart();
+
+        verify(mFingerprintService).registerAuthenticatorsLegacy(
+                mFingerprintSensorConfigurationsCaptor.capture());
+
+        final SensorProps[] fingerprintProp = mFingerprintSensorConfigurationsCaptor.getValue()
+                .getSensorPairForInstance("defaultHIDL").second;
+
+        assertEquals(fingerprintProp[0].commonProps.sensorId, fingerprintId);
+        assertEquals(fingerprintProp[0].commonProps.sensorStrength,
+                Utils.authenticatorStrengthToPropertyStrength(fingerprintStrength));
+
+        verify(mFaceService).registerAuthenticatorsLegacy(
+                mFaceSensorConfigurationsCaptor.capture());
+
+        final android.hardware.biometrics.face.SensorProps[] faceProp =
+                mFaceSensorConfigurationsCaptor.getValue()
+                        .getSensorPairForInstance("defaultHIDL").second;
+
+        assertEquals(faceProp[0].commonProps.sensorId, faceId);
+        assertEquals(faceProp[0].commonProps.sensorStrength,
+                Utils.authenticatorStrengthToPropertyStrength(faceStrength));
+    }
 
     // TODO(b/141025588): Check that an exception is thrown when the userId != callingUserId
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
new file mode 100644
index 0000000..c9e1c4a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.biometrics.sensors.face;
+
+import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
+import static android.hardware.face.FaceSensorProperties.TYPE_UNKNOWN;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.face.IFace;
+import android.hardware.biometrics.face.SensorProps;
+import android.hardware.face.FaceSensorConfigurations;
+import android.hardware.face.FaceSensorPropertiesInternal;
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.provider.Settings;
+import android.testing.TestableContext;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.internal.util.test.FakeSettingsProviderRule;
+import com.android.server.biometrics.Flags;
+import com.android.server.biometrics.Utils;
+import com.android.server.biometrics.sensors.face.aidl.FaceProvider;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@Presubmit
+@SmallTest
+public class FaceServiceTest {
+    private static final int ID_DEFAULT = 2;
+    private static final int ID_VIRTUAL = 6;
+    private static final String NAME_DEFAULT = "default";
+    private static final String NAME_VIRTUAL = "virtual";
+
+    @Rule
+    public final MockitoRule mMockito = MockitoJUnit.rule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+    @Rule
+    public final TestableContext mContext = new TestableContext(
+            InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
+    @Rule
+    public final FakeSettingsProviderRule mSettingsRule = FakeSettingsProvider.rule();
+    @Mock
+    FaceProvider mFaceProviderDefault;
+    @Mock
+    FaceProvider mFaceProviderVirtual;
+    @Mock
+    IFace mDefaultFaceDaemon;
+    @Mock
+    IFace mVirtualFaceDaemon;
+    @Mock
+    IBiometricService mIBiometricService;
+
+    private final SensorProps mDefaultSensorProps = new SensorProps();
+    private final SensorProps mVirtualSensorProps = new SensorProps();
+    private FaceService mFaceService;
+    private final FaceSensorPropertiesInternal mSensorPropsDefault =
+            new FaceSensorPropertiesInternal(ID_DEFAULT, STRENGTH_STRONG,
+                    2 /* maxEnrollmentsPerUser */,
+                    List.of(),
+                    TYPE_UNKNOWN,
+                    true /* supportsFaceDetection */,
+                    true /* supportsSelfIllumination */,
+                    false /* resetLockoutRequiresChallenge */);
+    private final FaceSensorPropertiesInternal mSensorPropsVirtual =
+            new FaceSensorPropertiesInternal(ID_VIRTUAL, STRENGTH_STRONG,
+                    2 /* maxEnrollmentsPerUser */,
+                    List.of(),
+                    TYPE_UNKNOWN,
+                    true /* supportsFaceDetection */,
+                    true /* supportsSelfIllumination */,
+                    false /* resetLockoutRequiresChallenge */);
+    private FaceSensorConfigurations mFaceSensorConfigurations;
+
+    @Before
+    public void setUp() throws RemoteException {
+        when(mDefaultFaceDaemon.getSensorProps()).thenReturn(
+                new SensorProps[]{mDefaultSensorProps});
+        when(mVirtualFaceDaemon.getSensorProps()).thenReturn(
+                new SensorProps[]{mVirtualSensorProps});
+        when(mFaceProviderDefault.getSensorProperties()).thenReturn(List.of(mSensorPropsDefault));
+        when(mFaceProviderVirtual.getSensorProperties()).thenReturn(List.of(mSensorPropsVirtual));
+
+        mFaceSensorConfigurations = new FaceSensorConfigurations(false);
+        mFaceSensorConfigurations.addAidlConfigs(new String[]{NAME_DEFAULT, NAME_VIRTUAL},
+                (name) -> {
+                    if (name.equals(IFace.DESCRIPTOR + "/" + NAME_DEFAULT)) {
+                        return mDefaultFaceDaemon;
+                    } else if (name.equals(IFace.DESCRIPTOR + "/" + NAME_VIRTUAL)) {
+                        return mVirtualFaceDaemon;
+                    }
+                    return null;
+                });
+    }
+
+    private void initService() {
+        mFaceService = new FaceService(mContext,
+                (filteredSensorProps, resetLockoutRequiresChallenge) -> {
+                    if (NAME_DEFAULT.equals(filteredSensorProps.first)) return mFaceProviderDefault;
+                    if (NAME_VIRTUAL.equals(filteredSensorProps.first)) return mFaceProviderVirtual;
+                    return null;
+                }, () -> mIBiometricService);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void registerAuthenticatorsLegacy_defaultOnly() throws Exception {
+        initService();
+
+        mFaceService.mServiceWrapper.registerAuthenticatorsLegacy(mFaceSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_DEFAULT),
+                eq(BiometricAuthenticator.TYPE_FACE),
+                eq(Utils.propertyStrengthToAuthenticatorStrength(STRENGTH_STRONG)),
+                any());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void registerAuthenticatorsLegacy_virtualOnly() throws Exception {
+        initService();
+        Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext),
+                Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED, 1);
+
+        mFaceService.mServiceWrapper.registerAuthenticatorsLegacy(mFaceSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL),
+                eq(BiometricAuthenticator.TYPE_FACE),
+                eq(Utils.propertyStrengthToAuthenticatorStrength(STRENGTH_STRONG)), any());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void registerAuthenticatorsLegacy_virtualAlwaysWhenNoOther() throws Exception {
+        mFaceSensorConfigurations = new FaceSensorConfigurations(false);
+        mFaceSensorConfigurations.addAidlConfigs(new String[]{NAME_VIRTUAL},
+                (name) -> {
+                    if (name.equals(IFace.DESCRIPTOR + "/" + NAME_DEFAULT)) {
+                        return mDefaultFaceDaemon;
+                    } else if (name.equals(IFace.DESCRIPTOR + "/" + NAME_VIRTUAL)) {
+                        return mVirtualFaceDaemon;
+                    }
+                    return null;
+                });
+        initService();
+
+        mFaceService.mServiceWrapper.registerAuthenticatorsLegacy(mFaceSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL),
+                eq(BiometricAuthenticator.TYPE_FACE),
+                eq(Utils.propertyStrengthToAuthenticatorStrength(STRENGTH_STRONG)), any());
+    }
+
+    private void waitForRegistration() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        mFaceService.mServiceWrapper.addAuthenticatorsRegisteredCallback(
+                new IFaceAuthenticatorsRegisteredCallback.Stub() {
+                    public void onAllAuthenticatorsRegistered(
+                            List<FaceSensorPropertiesInternal> sensors) {
+                        latch.countDown();
+                    }
+                });
+        latch.await(10, TimeUnit.SECONDS);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
index f43120d..8b1a291 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
@@ -16,6 +16,9 @@
 
 package com.android.server.biometrics.sensors.face.aidl;
 
+import static android.os.UserHandle.USER_NULL;
+import static android.os.UserHandle.USER_SYSTEM;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -32,15 +35,19 @@
 import android.hardware.biometrics.face.IFace;
 import android.hardware.biometrics.face.ISession;
 import android.hardware.biometrics.face.SensorProps;
+import android.hardware.face.HidlFaceSensorConfig;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricScheduler;
@@ -49,6 +56,7 @@
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -59,6 +67,10 @@
 @SmallTest
 public class FaceProviderTest {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final String TAG = "FaceProviderTest";
 
     private static final float FRR_THRESHOLD = 0.2f;
@@ -109,7 +121,7 @@
 
         mFaceProvider = new FaceProvider(mContext, mBiometricStateCallback,
                 mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext,
-                mDaemon);
+                mDaemon, false /* resetLockoutRequiresChallenge */, false /* testHalEnabled */);
     }
 
     @Test
@@ -124,10 +136,38 @@
 
             assertThat(currentClient).isInstanceOf(FaceInternalCleanupClient.class);
             assertThat(currentClient.getSensorId()).isEqualTo(prop.commonProps.sensorId);
-            assertThat(currentClient.getTargetUserId()).isEqualTo(UserHandle.USER_SYSTEM);
+            assertThat(currentClient.getTargetUserId()).isEqualTo(USER_SYSTEM);
         }
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void testAddingHidlSensors() {
+        when(mResources.getIntArray(anyInt())).thenReturn(new int[]{});
+        when(mResources.getBoolean(anyInt())).thenReturn(false);
+
+        final int faceId = 0;
+        final int faceStrength = 15;
+        final String config = String.format("%d:8:%d", faceId, faceStrength);
+        final HidlFaceSensorConfig faceSensorConfig = new HidlFaceSensorConfig();
+        faceSensorConfig.parse(config, mContext);
+        final HidlFaceSensorConfig[] hidlFaceSensorConfig =
+                new HidlFaceSensorConfig[]{faceSensorConfig};
+        mFaceProvider = new FaceProvider(mContext,
+                mBiometricStateCallback, hidlFaceSensorConfig, TAG,
+                mLockoutResetDispatcher, mBiometricContext, mDaemon,
+                true /* resetLockoutRequiresChallenge */,
+                true /* testHalEnabled */);
+
+        assertThat(mFaceProvider.mFaceSensors.get(faceId)
+                .getLazySession().get().getUserId()).isEqualTo(USER_NULL);
+
+        waitForIdle();
+
+        assertThat(mFaceProvider.mFaceSensors.get(faceId)
+                .getLazySession().get().getUserId()).isEqualTo(USER_SYSTEM);
+    }
+
     @SuppressWarnings("rawtypes")
     @Test
     public void halServiceDied_resetsAllSchedulers() {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
index 7a293e8..e7f7195 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
@@ -157,7 +157,7 @@
                 sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */);
         final Sensor sensor = new Sensor("SensorTest", mFaceProvider, mContext,
                 null /* handler */, internalProp, mLockoutResetDispatcher, mBiometricContext);
-
+        sensor.init(mLockoutResetDispatcher, mFaceProvider);
         mScheduler.reset();
 
         assertNull(mScheduler.getCurrentClient());
@@ -185,6 +185,7 @@
                 sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */);
         final Sensor sensor = new Sensor("SensorTest", mFaceProvider, mContext, null,
                 internalProp, mLockoutResetDispatcher, mBiometricContext, mCurrentSession);
+        sensor.init(mLockoutResetDispatcher, mFaceProvider);
         mScheduler = (UserAwareBiometricScheduler) sensor.getScheduler();
         sensor.mCurrentSession = new AidlSession(0, mock(ISession.class),
                 USER_ID, mHalCallback);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java
new file mode 100644
index 0000000..4e43332
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.biometrics.sensors.face.hidl;
+
+import static com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter.ENROLL_TIMEOUT_SEC;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.face.V1_0.IBiometricsFace;
+import android.hardware.biometrics.face.V1_0.OptionalUint64;
+import android.hardware.biometrics.face.V1_0.Status;
+import android.hardware.face.Face;
+import android.hardware.face.HidlFaceSensorConfig;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+import android.testing.TestableContext;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.BiometricUtils;
+import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.face.aidl.AidlResponseHandler;
+import com.android.server.biometrics.sensors.face.aidl.FaceEnrollClient;
+import com.android.server.biometrics.sensors.face.aidl.FaceProvider;
+import com.android.server.biometrics.sensors.face.aidl.FaceResetLockoutClient;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+public class HidlToAidlSensorAdapterTest {
+    private static final String TAG = "HidlToAidlSensorAdapterTest";
+    private static final int USER_ID = 2;
+    private static final int SENSOR_ID = 4;
+    private static final byte[] HAT = new byte[69];
+    @Rule
+    public final MockitoRule mMockito = MockitoJUnit.rule();
+
+    @Mock
+    private IBiometricService mBiometricService;
+    @Mock
+    private LockoutResetDispatcher mLockoutResetDispatcherForSensor;
+    @Mock
+    private LockoutResetDispatcher mLockoutResetDispatcherForClient;
+    @Mock
+    private BiometricLogger mLogger;
+    @Mock
+    private BiometricContext mBiometricContext;
+    @Mock
+    private AuthSessionCoordinator mAuthSessionCoordinator;
+    @Mock
+    private FaceProvider mFaceProvider;
+    @Mock
+    private Runnable mInternalCleanupAndGetFeatureRunnable;
+    @Mock
+    private IBiometricsFace mDaemon;
+    @Mock
+    AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback;
+    @Mock
+    BiometricUtils<Face> mBiometricUtils;
+
+    private final TestLooper mLooper = new TestLooper();
+    private HidlToAidlSensorAdapter mHidlToAidlSensorAdapter;
+    private final TestableContext mContext = new TestableContext(
+            ApplicationProvider.getApplicationContext());
+
+    @Before
+    public void setUp() throws RemoteException {
+        final OptionalUint64 result = new OptionalUint64();
+        result.status = Status.OK;
+
+        when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator);
+        when(mDaemon.setCallback(any())).thenReturn(result);
+        doAnswer((answer) -> {
+            mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback()
+                    .onLockoutChanged(0);
+            return null;
+        }).when(mDaemon).resetLockout(any());
+        doAnswer((answer) -> {
+            mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback()
+                    .onEnrollmentProgress(1, 0);
+            return null;
+        }).when(mDaemon).enroll(any(), anyInt(), any());
+
+        mContext.getOrCreateTestableResources();
+
+        final String config = String.format("%d:8:15", SENSOR_ID);
+        final BiometricScheduler scheduler = new BiometricScheduler(TAG,
+                new Handler(mLooper.getLooper()),
+                BiometricScheduler.SENSOR_TYPE_FACE,
+                null /* gestureAvailabilityTracker */,
+                mBiometricService, 10 /* recentOperationsLimit */);
+        final HidlFaceSensorConfig faceSensorConfig = new HidlFaceSensorConfig();
+        faceSensorConfig.parse(config, mContext);
+        mHidlToAidlSensorAdapter = new HidlToAidlSensorAdapter(TAG, mFaceProvider,
+                mContext, new Handler(mLooper.getLooper()), faceSensorConfig,
+                mLockoutResetDispatcherForSensor, mBiometricContext,
+                false /* resetLockoutRequiresChallenge */, mInternalCleanupAndGetFeatureRunnable,
+                mAuthSessionCoordinator, mDaemon, mAidlResponseHandlerCallback);
+        mHidlToAidlSensorAdapter.init(mLockoutResetDispatcherForSensor, mFaceProvider);
+        mHidlToAidlSensorAdapter.setScheduler(scheduler);
+        mHidlToAidlSensorAdapter.handleUserChanged(USER_ID);
+    }
+
+    @Test
+    public void lockoutTimedResetViaClient() {
+        setLockoutTimed();
+
+        mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(
+                new FaceResetLockoutClient(mContext, mHidlToAidlSensorAdapter.getLazySession(),
+                        USER_ID, TAG, SENSOR_ID, mLogger, mBiometricContext, HAT,
+                        mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */),
+                        mLockoutResetDispatcherForClient, 0 /* biometricStrength */));
+        mLooper.dispatchAll();
+
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false/* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mLockoutResetDispatcherForClient, never()).notifyLockoutResetCallbacks(SENSOR_ID);
+        verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(SENSOR_ID);
+        verify(mAuthSessionCoordinator, never()).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void lockoutTimedResetViaCallback() {
+        setLockoutTimed();
+
+        mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback().onLockoutChanged(0);
+        mLooper.dispatchAll();
+
+        verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(eq(
+                SENSOR_ID));
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID))
+                .isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator, never()).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void lockoutPermanentResetViaCallback() {
+        setLockoutPermanent();
+
+        mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback().onLockoutChanged(0);
+        mLooper.dispatchAll();
+
+        verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(eq(
+                SENSOR_ID));
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator, never()).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void lockoutPermanentResetViaClient() {
+        setLockoutPermanent();
+
+        mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(
+                new FaceResetLockoutClient(mContext,
+                        mHidlToAidlSensorAdapter.getLazySession(),
+                        USER_ID, TAG, SENSOR_ID, mLogger, mBiometricContext, HAT,
+                        mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */),
+                        mLockoutResetDispatcherForClient, 0 /* biometricStrength */));
+        mLooper.dispatchAll();
+
+        verify(mLockoutResetDispatcherForClient, never()).notifyLockoutResetCallbacks(SENSOR_ID);
+        verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(SENSOR_ID);
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator, never()).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void verifyOnEnrollSuccessCallback() {
+        mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(new FaceEnrollClient(mContext,
+                mHidlToAidlSensorAdapter.getLazySession(), null /* token */, null /* listener */,
+                USER_ID, HAT, TAG, 1 /* requestId */, mBiometricUtils,
+                new int[]{} /* disabledFeatures */, ENROLL_TIMEOUT_SEC, null /* previewSurface */,
+                SENSOR_ID, mLogger, mBiometricContext, 1 /* maxTemplatesPerUser */,
+                false /* debugConsent */));
+        mLooper.dispatchAll();
+
+        verify(mAidlResponseHandlerCallback).onEnrollSuccess();
+    }
+
+    private void setLockoutTimed() {
+        mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback()
+                .onLockoutChanged(1);
+        mLooper.dispatchAll();
+
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID))
+                .isEqualTo(LockoutTracker.LOCKOUT_TIMED);
+    }
+
+    private void setLockoutPermanent() {
+        mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback()
+                .onLockoutChanged(-1);
+        mLooper.dispatchAll();
+
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_PERMANENT);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapterTest.java
similarity index 80%
rename from services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapterTest.java
rename to services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapterTest.java
index 9a40e8a..b9a4fb4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapterTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapterTest.java
@@ -16,7 +16,7 @@
 
 package com.android.server.biometrics.sensors.face.hidl;
 
-import static com.android.server.biometrics.sensors.face.hidl.AidlToHidlAdapter.ENROLL_TIMEOUT_SEC;
+import static com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter.ENROLL_TIMEOUT_SEC;
 import static com.android.server.biometrics.sensors.face.hidl.FaceGenerateChallengeClient.CHALLENGE_TIMEOUT_SEC;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -68,7 +68,7 @@
 
 @Presubmit
 @SmallTest
-public class AidlToHidlAdapterTest {
+public class HidlToAidlSessionAdapterTest {
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
 
@@ -84,25 +84,32 @@
     private Clock mClock;
 
     private final long mChallenge = 100L;
-    private AidlToHidlAdapter mAidlToHidlAdapter;
+    private HidlToAidlSessionAdapter mHidlToAidlSessionAdapter;
     private final Face mFace = new Face("face" /* name */, 1 /* faceId */, 0 /* deviceId */);
     private final int mFeature = BiometricFaceConstants.FEATURE_REQUIRE_REQUIRE_DIVERSITY;
     private final byte[] mFeatures = new byte[]{Feature.REQUIRE_ATTENTION};
 
     @Before
     public void setUp() throws RemoteException {
+        final OptionalUint64 setCallbackResult = new OptionalUint64();
+        setCallbackResult.value = 1;
+
+        when(mSession.setCallback(any())).thenReturn(setCallbackResult);
+
         TestableContext testableContext = new TestableContext(
                 InstrumentationRegistry.getInstrumentation().getContext());
         testableContext.addMockSystemService(FaceManager.class, mFaceManager);
-        mAidlToHidlAdapter = new AidlToHidlAdapter(testableContext, () -> mSession, 0 /* userId */,
-                mAidlResponseHandler, mClock);
+
+        mHidlToAidlSessionAdapter = new HidlToAidlSessionAdapter(testableContext, () -> mSession,
+                0 /* userId */, mAidlResponseHandler, mClock);
         mHardwareAuthToken.timestamp = new Timestamp();
         mHardwareAuthToken.mac = new byte[10];
-        final OptionalUint64 result = new OptionalUint64();
-        result.status = Status.OK;
-        result.value = mChallenge;
 
-        when(mSession.generateChallenge(anyInt())).thenReturn(result);
+        final OptionalUint64 generateChallengeResult = new OptionalUint64();
+        generateChallengeResult.status = Status.OK;
+        generateChallengeResult.value = mChallenge;
+
+        when(mSession.generateChallenge(anyInt())).thenReturn(generateChallengeResult);
         when(mFaceManager.getEnrolledFaces(anyInt())).thenReturn(List.of(mFace));
     }
 
@@ -112,16 +119,16 @@
 
         final ArgumentCaptor<Long> challengeCaptor = ArgumentCaptor.forClass(Long.class);
 
-        mAidlToHidlAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
 
         verify(mSession).generateChallenge(CHALLENGE_TIMEOUT_SEC);
         verify(mAidlResponseHandler).onChallengeGenerated(challengeCaptor.capture());
         assertThat(challengeCaptor.getValue()).isEqualTo(mChallenge);
 
         forwardTime(10 /* seconds */);
-        mAidlToHidlAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
         forwardTime(20 /* seconds */);
-        mAidlToHidlAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
 
         //Confirms that the challenge is cached and the hal method is not called again
         verifyNoMoreInteractions(mSession);
@@ -129,7 +136,7 @@
                 .onChallengeGenerated(mChallenge);
 
         forwardTime(60 /* seconds */);
-        mAidlToHidlAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
 
         //HAL method called after challenge has timed out
         verify(mSession, times(2)).generateChallenge(CHALLENGE_TIMEOUT_SEC);
@@ -138,11 +145,11 @@
     @Test
     public void testRevokeChallenge_waitsUntilEmpty() throws RemoteException {
         for (int i = 0; i < 3; i++) {
-            mAidlToHidlAdapter.generateChallenge();
+            mHidlToAidlSessionAdapter.generateChallenge();
             forwardTime(10 /* seconds */);
         }
         for (int i = 0; i < 3; i++) {
-            mAidlToHidlAdapter.revokeChallenge(0);
+            mHidlToAidlSessionAdapter.revokeChallenge(0);
             forwardTime((i + 1) * 10 /* seconds */);
         }
 
@@ -151,20 +158,19 @@
 
     @Test
     public void testRevokeChallenge_timeout() throws RemoteException {
-        mAidlToHidlAdapter.generateChallenge();
-        mAidlToHidlAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
         forwardTime(700);
-        mAidlToHidlAdapter.generateChallenge();
-        mAidlToHidlAdapter.revokeChallenge(0);
+        mHidlToAidlSessionAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.revokeChallenge(0);
 
         verify(mSession).revokeChallenge();
     }
 
     @Test
     public void testEnroll() throws RemoteException {
-        ICancellationSignal cancellationSignal = mAidlToHidlAdapter.enroll(mHardwareAuthToken,
-                EnrollmentType.DEFAULT, mFeatures,
-                null /* previewSurface */);
+        ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter.enroll(
+                mHardwareAuthToken, EnrollmentType.DEFAULT, mFeatures, null /* previewSurface */);
         ArgumentCaptor<ArrayList<Integer>> featureCaptor = ArgumentCaptor.forClass(ArrayList.class);
 
         verify(mSession).enroll(any(), eq(ENROLL_TIMEOUT_SEC), featureCaptor.capture());
@@ -182,7 +188,8 @@
     @Test
     public void testAuthenticate() throws RemoteException {
         final int operationId = 2;
-        ICancellationSignal cancellationSignal = mAidlToHidlAdapter.authenticate(operationId);
+        ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter.authenticate(
+                operationId);
 
         verify(mSession).authenticate(operationId);
 
@@ -193,7 +200,7 @@
 
     @Test
     public void testDetectInteraction() throws RemoteException {
-        ICancellationSignal cancellationSignal = mAidlToHidlAdapter.detectInteraction();
+        ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter.detectInteraction();
 
         verify(mSession).authenticate(0);
 
@@ -204,7 +211,7 @@
 
     @Test
     public void testEnumerateEnrollments() throws RemoteException {
-        mAidlToHidlAdapter.enumerateEnrollments();
+        mHidlToAidlSessionAdapter.enumerateEnrollments();
 
         verify(mSession).enumerate();
     }
@@ -212,7 +219,7 @@
     @Test
     public void testRemoveEnrollment() throws RemoteException {
         final int[] enrollments = new int[]{1};
-        mAidlToHidlAdapter.removeEnrollments(enrollments);
+        mHidlToAidlSessionAdapter.removeEnrollments(enrollments);
 
         verify(mSession).remove(enrollments[0]);
     }
@@ -226,8 +233,8 @@
 
         when(mSession.getFeature(eq(mFeature), anyInt())).thenReturn(result);
 
-        mAidlToHidlAdapter.setFeature(mFeature);
-        mAidlToHidlAdapter.getFeatures();
+        mHidlToAidlSessionAdapter.setFeature(mFeature);
+        mHidlToAidlSessionAdapter.getFeatures();
 
         verify(mSession).getFeature(eq(mFeature), anyInt());
         verify(mAidlResponseHandler).onFeaturesRetrieved(featureRetrieved.capture());
@@ -244,8 +251,8 @@
 
         when(mSession.getFeature(eq(mFeature), anyInt())).thenReturn(result);
 
-        mAidlToHidlAdapter.setFeature(mFeature);
-        mAidlToHidlAdapter.getFeatures();
+        mHidlToAidlSessionAdapter.setFeature(mFeature);
+        mHidlToAidlSessionAdapter.getFeatures();
 
         verify(mSession).getFeature(eq(mFeature), anyInt());
         verify(mAidlResponseHandler).onFeaturesRetrieved(featureRetrieved.capture());
@@ -260,8 +267,8 @@
 
         when(mSession.getFeature(eq(mFeature), anyInt())).thenReturn(result);
 
-        mAidlToHidlAdapter.setFeature(mFeature);
-        mAidlToHidlAdapter.getFeatures();
+        mHidlToAidlSessionAdapter.setFeature(mFeature);
+        mHidlToAidlSessionAdapter.getFeatures();
 
         verify(mSession).getFeature(eq(mFeature), anyInt());
         verify(mAidlResponseHandler, never()).onFeaturesRetrieved(any());
@@ -270,7 +277,7 @@
 
     @Test
     public void testGetFeatures_featureNotSet() throws RemoteException {
-        mAidlToHidlAdapter.getFeatures();
+        mHidlToAidlSessionAdapter.getFeatures();
 
         verify(mSession, never()).getFeature(eq(mFeature), anyInt());
         verify(mAidlResponseHandler, never()).onFeaturesRetrieved(any());
@@ -283,7 +290,7 @@
 
         when(mSession.setFeature(anyInt(), anyBoolean(), any(), anyInt())).thenReturn(Status.OK);
 
-        mAidlToHidlAdapter.setFeature(mHardwareAuthToken, feature, enabled);
+        mHidlToAidlSessionAdapter.setFeature(mHardwareAuthToken, feature, enabled);
 
         verify(mAidlResponseHandler).onFeatureSet(feature);
     }
@@ -296,7 +303,7 @@
         when(mSession.setFeature(anyInt(), anyBoolean(), any(), anyInt()))
                 .thenReturn(Status.INTERNAL_ERROR);
 
-        mAidlToHidlAdapter.setFeature(mHardwareAuthToken, feature, enabled);
+        mHidlToAidlSessionAdapter.setFeature(mHardwareAuthToken, feature, enabled);
 
         verify(mAidlResponseHandler).onError(BiometricFaceConstants.FACE_ERROR_UNKNOWN,
                 0 /* vendorCode */);
@@ -311,7 +318,7 @@
 
         when(mSession.getAuthenticatorId()).thenReturn(result);
 
-        mAidlToHidlAdapter.getAuthenticatorId();
+        mHidlToAidlSessionAdapter.getAuthenticatorId();
 
         verify(mSession).getAuthenticatorId();
         verify(mAidlResponseHandler).onAuthenticatorIdRetrieved(authenticatorId);
@@ -319,7 +326,7 @@
 
     @Test
     public void testResetLockout() throws RemoteException {
-        mAidlToHidlAdapter.resetLockout(mHardwareAuthToken);
+        mHidlToAidlSessionAdapter.resetLockout(mHardwareAuthToken);
 
         ArgumentCaptor<ArrayList> hatCaptor = ArgumentCaptor.forClass(ArrayList.class);
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java
index 2aa62d9..f570ba2 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java
@@ -42,13 +42,19 @@
 import android.app.AppOpsManager;
 import android.content.pm.PackageManager;
 import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.fingerprint.IFingerprint;
+import android.hardware.biometrics.fingerprint.SensorProps;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
+import android.hardware.fingerprint.FingerprintSensorConfigurations;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.os.Binder;
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.testing.TestableContext;
 
@@ -58,6 +64,7 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.internal.util.test.FakeSettingsProviderRule;
 import com.android.server.LocalServices;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider;
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
@@ -89,6 +96,9 @@
     @Rule
     public final MockitoRule mMockito = MockitoJUnit.rule();
     @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+    @Rule
     public final TestableContext mContext = new TestableContext(
             InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
     @Rule
@@ -110,6 +120,10 @@
     private IBinder mToken;
     @Mock
     private VirtualDeviceManagerInternal mVdmInternal;
+    @Mock
+    private IFingerprint mDefaultFingerprintDaemon;
+    @Mock
+    private IFingerprint mVirtualFingerprintDaemon;
 
     @Captor
     private ArgumentCaptor<FingerprintAuthenticateOptions> mAuthenticateOptionsCaptor;
@@ -126,7 +140,10 @@
                     List.of(),
                     TYPE_UDFPS_OPTICAL,
                     false /* resetLockoutRequiresHardwareAuthToken */);
+    private FingerprintSensorConfigurations mFingerprintSensorConfigurations;
     private FingerprintService mService;
+    private final SensorProps mDefaultSensorProps = new SensorProps();
+    private final SensorProps mVirtualSensorProps = new SensorProps();
 
     @Before
     public void setup() throws Exception {
@@ -139,6 +156,10 @@
                 .thenAnswer(i -> i.getArguments()[0].equals(ID_DEFAULT));
         when(mFingerprintVirtual.containsSensor(anyInt()))
                 .thenAnswer(i -> i.getArguments()[0].equals(ID_VIRTUAL));
+        when(mDefaultFingerprintDaemon.getSensorProps()).thenReturn(
+                new SensorProps[]{mDefaultSensorProps});
+        when(mVirtualFingerprintDaemon.getSensorProps()).thenReturn(
+                new SensorProps[]{mVirtualSensorProps});
 
         mContext.addMockSystemService(AppOpsManager.class, mAppOpsManager);
         for (int permission : List.of(OP_USE_BIOMETRIC, OP_USE_FINGERPRINT)) {
@@ -150,6 +171,18 @@
             mContext.getTestablePermissions().setPermission(
                     permission, PackageManager.PERMISSION_GRANTED);
         }
+
+        mFingerprintSensorConfigurations = new FingerprintSensorConfigurations(
+                true /* resetLockoutRequiresHardwareAuthToken */);
+        mFingerprintSensorConfigurations.addAidlSensors(new String[]{NAME_DEFAULT, NAME_VIRTUAL},
+                (name) -> {
+                    if (name.equals(IFingerprint.DESCRIPTOR + "/" + NAME_DEFAULT)) {
+                        return mDefaultFingerprintDaemon;
+                    } else if (name.equals(IFingerprint.DESCRIPTOR + "/" + NAME_VIRTUAL)) {
+                        return mVirtualFingerprintDaemon;
+                    }
+                    return null;
+                });
     }
 
     private void initServiceWith(String... aidlInstances) {
@@ -160,6 +193,10 @@
                     if (NAME_DEFAULT.equals(name)) return mFingerprintDefault;
                     if (NAME_VIRTUAL.equals(name)) return mFingerprintVirtual;
                     return null;
+                }, (sensorPropsPair, resetLockoutRequiresHardwareAuthToken) -> {
+                    if (NAME_DEFAULT.equals(sensorPropsPair.first)) return mFingerprintDefault;
+                    if (NAME_VIRTUAL.equals(sensorPropsPair.first)) return mFingerprintVirtual;
+                    return null;
                 });
     }
 
@@ -180,6 +217,17 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void registerAuthenticatorsLegacy_defaultOnly() throws Exception {
+        initServiceWith(NAME_DEFAULT, NAME_VIRTUAL);
+
+        mService.mServiceWrapper.registerAuthenticatorsLegacy(mFingerprintSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_DEFAULT), anyInt(), anyInt(), any());
+    }
+
+    @Test
     public void registerAuthenticators_virtualOnly() throws Exception {
         initServiceWith(NAME_DEFAULT, NAME_VIRTUAL);
         Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext),
@@ -192,6 +240,19 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void registerAuthenticatorsLegacy_virtualOnly() throws Exception {
+        initServiceWith(NAME_DEFAULT, NAME_VIRTUAL);
+        Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext),
+                Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED, 1);
+
+        mService.mServiceWrapper.registerAuthenticatorsLegacy(mFingerprintSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any());
+    }
+
+    @Test
     public void registerAuthenticators_virtualAlwaysWhenNoOther() throws Exception {
         initServiceWith(NAME_VIRTUAL);
 
@@ -201,6 +262,28 @@
         verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any());
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void registerAuthenticatorsLegacy_virtualAlwaysWhenNoOther() throws Exception {
+        mFingerprintSensorConfigurations =
+                new FingerprintSensorConfigurations(true);
+        mFingerprintSensorConfigurations.addAidlSensors(new String[]{NAME_VIRTUAL},
+                        (name) -> {
+                            if (name.equals(IFingerprint.DESCRIPTOR + "/" + NAME_DEFAULT)) {
+                                return mDefaultFingerprintDaemon;
+                            } else if (name.equals(IFingerprint.DESCRIPTOR + "/" + NAME_VIRTUAL)) {
+                                return mVirtualFingerprintDaemon;
+                            }
+                            return null;
+                        });
+        initServiceWith(NAME_VIRTUAL);
+
+        mService.mServiceWrapper.registerAuthenticatorsLegacy(mFingerprintSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any());
+    }
+
     private void waitForRegistration() throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
         mService.mServiceWrapper.addAuthenticatorsRegisteredCallback(
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
index 4cfb83f..bf5986c 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
@@ -16,6 +16,9 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
+import static android.os.UserHandle.USER_NULL;
+import static android.os.UserHandle.USER_SYSTEM;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -34,17 +37,20 @@
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.biometrics.fingerprint.SensorLocation;
 import android.hardware.biometrics.fingerprint.SensorProps;
+import android.hardware.fingerprint.HidlFingerprintSensorConfig;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.AuthenticationStateListeners;
-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.HalClientMonitor;
@@ -52,6 +58,7 @@
 import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -64,6 +71,10 @@
 
     private static final String TAG = "FingerprintProviderTest";
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Mock
     private Context mContext;
     @Mock
@@ -115,7 +126,8 @@
         mFingerprintProvider = new FingerprintProvider(mContext,
                 mBiometricStateCallback, mAuthenticationStateListeners, mSensorProps, TAG,
                 mLockoutResetDispatcher, mGestureAvailabilityDispatcher, mBiometricContext,
-                mDaemon);
+                mDaemon, false /* resetLockoutRequiresHardwareAuthToken */,
+                true /* testHalEnabled */);
     }
 
     @Test
@@ -123,17 +135,42 @@
         waitForIdle();
 
         for (SensorProps prop : mSensorProps) {
-            final BiometricScheduler scheduler =
-                    mFingerprintProvider.mFingerprintSensors.get(prop.commonProps.sensorId)
-                            .getScheduler();
-            BaseClientMonitor currentClient = scheduler.getCurrentClient();
-
-            assertThat(currentClient).isInstanceOf(FingerprintInternalCleanupClient.class);
-            assertThat(currentClient.getSensorId()).isEqualTo(prop.commonProps.sensorId);
-            assertThat(currentClient.getTargetUserId()).isEqualTo(UserHandle.USER_SYSTEM);
+            final Sensor sensor =
+                    mFingerprintProvider.mFingerprintSensors.get(prop.commonProps.sensorId);
+            assertThat(sensor.getLazySession().get().getUserId()).isEqualTo(USER_SYSTEM);
         }
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void testAddingHidlSensors() {
+        when(mResources.getIntArray(anyInt())).thenReturn(new int[]{});
+        when(mResources.getBoolean(anyInt())).thenReturn(false);
+
+        final int fingerprintId = 0;
+        final int fingerprintStrength = 15;
+        final String config = String.format("%d:2:%d", fingerprintId, fingerprintStrength);
+        final HidlFingerprintSensorConfig fingerprintSensorConfig =
+                new HidlFingerprintSensorConfig();
+        fingerprintSensorConfig.parse(config, mContext);
+        HidlFingerprintSensorConfig[] hidlFingerprintSensorConfigs =
+                new HidlFingerprintSensorConfig[]{fingerprintSensorConfig};
+        mFingerprintProvider = new FingerprintProvider(mContext,
+                mBiometricStateCallback, mAuthenticationStateListeners,
+                hidlFingerprintSensorConfigs, TAG, mLockoutResetDispatcher,
+                mGestureAvailabilityDispatcher, mBiometricContext, mDaemon,
+                false /* resetLockoutRequiresHardwareAuthToken */,
+                true /* testHalEnabled */);
+
+        assertThat(mFingerprintProvider.mFingerprintSensors.get(fingerprintId)
+                .getLazySession().get().getUserId()).isEqualTo(USER_NULL);
+
+        waitForIdle();
+
+        assertThat(mFingerprintProvider.mFingerprintSensors.get(fingerprintId)
+                .getLazySession().get().getUserId()).isEqualTo(USER_SYSTEM);
+    }
+
     @SuppressWarnings("rawtypes")
     @Test
     public void halServiceDied_resetsAllSchedulers() {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java
index 4102600..126a05e 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java
@@ -96,7 +96,7 @@
     private final TestLooper mLooper = new TestLooper();
     private final LockoutCache mLockoutCache = new LockoutCache();
 
-    private UserAwareBiometricScheduler mScheduler;
+    private BiometricScheduler mScheduler;
     private AidlResponseHandler mHalCallback;
 
     @Before
@@ -164,7 +164,8 @@
         final Sensor sensor = new Sensor("SensorTest", mFingerprintProvider, mContext,
                 null /* handler */, internalProp, mLockoutResetDispatcher,
                 mGestureAvailabilityDispatcher, mBiometricContext, mCurrentSession);
-        mScheduler = (UserAwareBiometricScheduler) sensor.getScheduler();
+        sensor.init(mGestureAvailabilityDispatcher, mLockoutResetDispatcher);
+        mScheduler = sensor.getScheduler();
         sensor.mCurrentSession = new AidlSession(0, mock(ISession.class),
                 USER_ID, mHalCallback);
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java
new file mode 100644
index 0000000..89a4961
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.biometrics.sensors.fingerprint.hidl;
+
+import static android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
+import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.HidlFingerprintSensorConfig;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
+import android.testing.TestableContext;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
+import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.BiometricUtils;
+import com.android.server.biometrics.sensors.ClientMonitorCallback;
+import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.StartUserClient;
+import com.android.server.biometrics.sensors.StopUserClient;
+import com.android.server.biometrics.sensors.UserAwareBiometricScheduler;
+import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
+import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler;
+import com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession;
+import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintEnrollClient;
+import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider;
+import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintResetLockoutClient;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@Presubmit
+@SmallTest
+public class HidlToAidlSensorAdapterTest {
+
+    private static final String TAG = "HidlToAidlSensorAdapterTest";
+    private static final int USER_ID = 2;
+    private static final int SENSOR_ID = 4;
+    private static final byte[] HAT = new byte[69];
+
+    @Rule
+    public final MockitoRule mMockito = MockitoJUnit.rule();
+
+    @Mock
+    private IBiometricService mBiometricService;
+    @Mock
+    private LockoutResetDispatcher mLockoutResetDispatcherForSensor;
+    @Mock
+    private LockoutResetDispatcher mLockoutResetDispatcherForClient;
+    @Mock
+    private BiometricLogger mLogger;
+    @Mock
+    private BiometricContext mBiometricContext;
+    @Mock
+    private AuthSessionCoordinator mAuthSessionCoordinator;
+    @Mock
+    private FingerprintProvider mFingerprintProvider;
+    @Mock
+    private GestureAvailabilityDispatcher mGestureAvailabilityDispatcher;
+    @Mock
+    private Runnable mInternalCleanupRunnable;
+    @Mock
+    private AlarmManager mAlarmManager;
+    @Mock
+    private IBiometricsFingerprint mDaemon;
+    @Mock
+    private AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback;
+    @Mock
+    private BiometricUtils<Fingerprint> mBiometricUtils;
+    @Mock
+    private AuthenticationStateListeners mAuthenticationStateListeners;
+
+    private final TestLooper mLooper = new TestLooper();
+    private HidlToAidlSensorAdapter mHidlToAidlSensorAdapter;
+    private final TestableContext mContext = new TestableContext(
+            ApplicationProvider.getApplicationContext());
+
+    private final UserAwareBiometricScheduler.UserSwitchCallback mUserSwitchCallback =
+            new UserAwareBiometricScheduler.UserSwitchCallback() {
+                @NonNull
+                @Override
+                public StopUserClient<?> getStopUserClient(int userId) {
+                    return new StopUserClient<IBiometricsFingerprint>(mContext,
+                            mHidlToAidlSensorAdapter::getIBiometricsFingerprint, null, USER_ID,
+                            SENSOR_ID, mLogger, mBiometricContext, () -> {}) {
+                        @Override
+                        protected void startHalOperation() {
+                            getCallback().onClientFinished(this, true /* success */);
+                        }
+
+                        @Override
+                        public void unableToStart() {}
+                    };
+                }
+
+                @NonNull
+                @Override
+                public StartUserClient<?, ?> getStartUserClient(int newUserId) {
+                    return new StartUserClient<IBiometricsFingerprint, AidlSession>(mContext,
+                            mHidlToAidlSensorAdapter::getIBiometricsFingerprint, null,
+                            USER_ID, SENSOR_ID,
+                            mLogger, mBiometricContext,
+                            (newUserId1, newUser, halInterfaceVersion) ->
+                                    mHidlToAidlSensorAdapter.handleUserChanged(newUserId1)) {
+                        @Override
+                        public void start(@NonNull ClientMonitorCallback callback) {
+                            super.start(callback);
+                            startHalOperation();
+                        }
+
+                        @Override
+                        protected void startHalOperation() {
+                            mUserStartedCallback.onUserStarted(USER_ID, null, 0);
+                            getCallback().onClientFinished(this, true /* success */);
+                        }
+
+                        @Override
+                        public void unableToStart() {}
+                    };
+                }
+            };;
+
+    @Before
+    public void setUp() throws RemoteException {
+        when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator);
+        doAnswer((answer) -> {
+            mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback()
+                    .onEnrollmentProgress(1 /* enrollmentId */, 0 /* remaining */);
+            return null;
+        }).when(mDaemon).enroll(any(), anyInt(), anyInt());
+
+        mContext.addMockSystemService(AlarmManager.class, mAlarmManager);
+        mContext.getOrCreateTestableResources();
+
+        final String config = String.format("%d:2:15", SENSOR_ID);
+        final UserAwareBiometricScheduler scheduler = new UserAwareBiometricScheduler(TAG,
+                new Handler(mLooper.getLooper()),
+                BiometricScheduler.SENSOR_TYPE_FP_OTHER,
+                null /* gestureAvailabilityDispatcher */,
+                mBiometricService,
+                () -> USER_ID,
+                mUserSwitchCallback);
+        final HidlFingerprintSensorConfig fingerprintSensorConfig =
+                new HidlFingerprintSensorConfig();
+        fingerprintSensorConfig.parse(config, mContext);
+        mHidlToAidlSensorAdapter = new HidlToAidlSensorAdapter(TAG,
+                mFingerprintProvider, mContext, new Handler(mLooper.getLooper()),
+                fingerprintSensorConfig, mLockoutResetDispatcherForSensor,
+                mGestureAvailabilityDispatcher, mBiometricContext,
+                false /* resetLockoutRequiresHardwareAuthToken */,
+                mInternalCleanupRunnable, mAuthSessionCoordinator, mDaemon,
+                mAidlResponseHandlerCallback);
+        mHidlToAidlSensorAdapter.init(mGestureAvailabilityDispatcher,
+                mLockoutResetDispatcherForSensor);
+        mHidlToAidlSensorAdapter.setScheduler(scheduler);
+        mHidlToAidlSensorAdapter.handleUserChanged(USER_ID);
+    }
+
+    @Test
+    public void lockoutTimedResetViaClient() {
+        setLockoutTimed();
+
+        mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(
+                new FingerprintResetLockoutClient(mContext,
+                        mHidlToAidlSensorAdapter.getLazySession(),
+                        USER_ID, TAG, SENSOR_ID, mLogger, mBiometricContext, HAT,
+                        mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */),
+                        mLockoutResetDispatcherForClient, 0 /* biometricStrength */));
+        mLooper.dispatchAll();
+
+        verify(mAlarmManager).setExact(anyInt(), anyLong(), any());
+        verify(mLockoutResetDispatcherForClient).notifyLockoutResetCallbacks(SENSOR_ID);
+        verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(SENSOR_ID);
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void lockoutTimedResetViaCallback() {
+        setLockoutTimed();
+
+        mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback().onLockoutCleared();
+        mLooper.dispatchAll();
+
+        verify(mLockoutResetDispatcherForSensor, times(2)).notifyLockoutResetCallbacks(eq(
+                SENSOR_ID));
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void lockoutPermanentResetViaCallback() {
+        setLockoutPermanent();
+
+        mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback().onLockoutCleared();
+        mLooper.dispatchAll();
+
+        verify(mLockoutResetDispatcherForSensor, times(2)).notifyLockoutResetCallbacks(eq(
+                SENSOR_ID));
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void lockoutPermanentResetViaClient() {
+        setLockoutPermanent();
+
+        mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(
+                new FingerprintResetLockoutClient(mContext,
+                        mHidlToAidlSensorAdapter.getLazySession(),
+                        USER_ID, TAG, SENSOR_ID, mLogger, mBiometricContext, HAT,
+                        mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */),
+                        mLockoutResetDispatcherForClient, 0 /* biometricStrength */));
+        mLooper.dispatchAll();
+
+        verify(mAlarmManager, atLeast(1)).setExact(anyInt(), anyLong(), any());
+        verify(mLockoutResetDispatcherForClient).notifyLockoutResetCallbacks(SENSOR_ID);
+        verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(SENSOR_ID);
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void verifyOnEnrollSuccessCallback() {
+        mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(new FingerprintEnrollClient(
+                mContext, mHidlToAidlSensorAdapter.getLazySession(), null /* token */,
+                1 /* requestId */, null /* listener */, USER_ID, HAT, TAG, mBiometricUtils,
+                SENSOR_ID, mLogger, mBiometricContext,
+                mHidlToAidlSensorAdapter.getSensorProperties(), null, null,
+                mAuthenticationStateListeners, 5 /* maxTemplatesPerUser */, ENROLL_ENROLL));
+        mLooper.dispatchAll();
+
+        verify(mAidlResponseHandlerCallback).onEnrollSuccess();
+    }
+
+    private void setLockoutPermanent() {
+        for (int i = 0; i < 20; i++) {
+            mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                    .addFailedAttemptForUser(USER_ID);
+        }
+
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_PERMANENT);
+    }
+
+    private void setLockoutTimed() {
+        for (int i = 0; i < 5; i++) {
+            mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                    .addFailedAttemptForUser(USER_ID);
+        }
+
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_TIMED);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapterTest.java
similarity index 79%
rename from services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapterTest.java
rename to services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapterTest.java
index b78ba82..d723e87 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapterTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapterTest.java
@@ -41,7 +41,7 @@
 
 @Presubmit
 @SmallTest
-public class AidlToHidlAdapterTest {
+public class HidlToAidlSessionAdapterTest {
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
 
@@ -54,11 +54,11 @@
 
     private final long mChallenge = 100L;
     private final int mUserId = 0;
-    private AidlToHidlAdapter mAidlToHidlAdapter;
+    private HidlToAidlSessionAdapter mHidlToAidlSessionAdapter;
 
     @Before
     public void setUp() {
-        mAidlToHidlAdapter = new AidlToHidlAdapter(() -> mSession, mUserId,
+        mHidlToAidlSessionAdapter = new HidlToAidlSessionAdapter(() -> mSession, mUserId,
                 mAidlResponseHandler);
         mHardwareAuthToken.timestamp = new Timestamp();
         mHardwareAuthToken.mac = new byte[10];
@@ -67,7 +67,7 @@
     @Test
     public void testGenerateChallenge() throws RemoteException {
         when(mSession.preEnroll()).thenReturn(mChallenge);
-        mAidlToHidlAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
 
         verify(mSession).preEnroll();
         verify(mAidlResponseHandler).onChallengeGenerated(mChallenge);
@@ -75,7 +75,7 @@
 
     @Test
     public void testRevokeChallenge() throws RemoteException {
-        mAidlToHidlAdapter.revokeChallenge(mChallenge);
+        mHidlToAidlSessionAdapter.revokeChallenge(mChallenge);
 
         verify(mSession).postEnroll();
         verify(mAidlResponseHandler).onChallengeRevoked(0L);
@@ -84,9 +84,9 @@
     @Test
     public void testEnroll() throws RemoteException {
         final ICancellationSignal cancellationSignal =
-                mAidlToHidlAdapter.enroll(mHardwareAuthToken);
+                mHidlToAidlSessionAdapter.enroll(mHardwareAuthToken);
 
-        verify(mSession).enroll(any(), anyInt(), eq(AidlToHidlAdapter.ENROLL_TIMEOUT_SEC));
+        verify(mSession).enroll(any(), anyInt(), eq(HidlToAidlSessionAdapter.ENROLL_TIMEOUT_SEC));
 
         cancellationSignal.cancel();
 
@@ -96,7 +96,8 @@
     @Test
     public void testAuthenticate() throws RemoteException {
         final int operationId = 2;
-        final ICancellationSignal cancellationSignal = mAidlToHidlAdapter.authenticate(operationId);
+        final ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter.authenticate(
+                operationId);
 
         verify(mSession).authenticate(operationId, mUserId);
 
@@ -107,7 +108,8 @@
 
     @Test
     public void testDetectInteraction() throws RemoteException {
-        final ICancellationSignal cancellationSignal = mAidlToHidlAdapter.detectInteraction();
+        final ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter
+                .detectInteraction();
 
         verify(mSession).authenticate(0 /* operationId */, mUserId);
 
@@ -118,7 +120,7 @@
 
     @Test
     public void testEnumerateEnrollments() throws RemoteException {
-        mAidlToHidlAdapter.enumerateEnrollments();
+        mHidlToAidlSessionAdapter.enumerateEnrollments();
 
         verify(mSession).enumerate();
     }
@@ -126,7 +128,7 @@
     @Test
     public void testRemoveEnrollment() throws RemoteException {
         final int[] enrollmentIds = new int[]{1};
-        mAidlToHidlAdapter.removeEnrollments(enrollmentIds);
+        mHidlToAidlSessionAdapter.removeEnrollments(enrollmentIds);
 
         verify(mSession).remove(mUserId, enrollmentIds[0]);
     }
@@ -134,14 +136,14 @@
     @Test
     public void testRemoveMultipleEnrollments() throws RemoteException {
         final int[] enrollmentIds = new int[]{1, 2};
-        mAidlToHidlAdapter.removeEnrollments(enrollmentIds);
+        mHidlToAidlSessionAdapter.removeEnrollments(enrollmentIds);
 
         verify(mSession).remove(mUserId, 0);
     }
 
     @Test
     public void testResetLockout() throws RemoteException {
-        mAidlToHidlAdapter.resetLockout(mHardwareAuthToken);
+        mHidlToAidlSessionAdapter.resetLockout(mHardwareAuthToken);
 
         verify(mAidlResponseHandler).onLockoutCleared();
     }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
index edfe1b4..071d571 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -24,10 +24,8 @@
 import static org.mockito.Mockito.when;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.companion.virtual.camera.VirtualCameraCallback;
 import android.companion.virtual.camera.VirtualCameraConfig;
-import android.companion.virtual.camera.VirtualCameraMetadata;
 import android.companion.virtual.camera.VirtualCameraStreamConfig;
 import android.companion.virtualcamera.IVirtualCameraService;
 import android.companion.virtualcamera.VirtualCameraConfiguration;
@@ -156,10 +154,6 @@
                     @NonNull VirtualCameraStreamConfig streamConfig) {}
 
             @Override
-            public void onProcessCaptureRequest(
-                    int streamId, long frameId, @Nullable VirtualCameraMetadata metadata) {}
-
-            @Override
             public void onStreamClosed(int streamId) {}
         };
     }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
new file mode 100644
index 0000000..d9a38eb
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.virtual.camera;
+
+import android.companion.virtual.camera.VirtualCameraStreamConfig;
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class VirtualCameraStreamConfigTest {
+
+    private static final int VGA_WIDTH = 640;
+    private static final int VGA_HEIGHT = 480;
+
+    private static final int QVGA_WIDTH = 320;
+    private static final int QVGA_HEIGHT = 240;
+
+    @Test
+    public void testEquals() {
+        VirtualCameraStreamConfig vgaYuvStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH,
+                VGA_HEIGHT,
+                ImageFormat.YUV_420_888);
+        VirtualCameraStreamConfig qvgaYuvStreamConfig = new VirtualCameraStreamConfig(QVGA_WIDTH,
+                QVGA_HEIGHT, ImageFormat.YUV_420_888);
+        VirtualCameraStreamConfig vgaRgbaStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH,
+                VGA_HEIGHT, PixelFormat.RGBA_8888);
+
+        new EqualsTester()
+                .addEqualityGroup(vgaYuvStreamConfig, reparcel(vgaYuvStreamConfig))
+                .addEqualityGroup(qvgaYuvStreamConfig, reparcel(qvgaYuvStreamConfig))
+                .addEqualityGroup(vgaRgbaStreamConfig, reparcel(vgaRgbaStreamConfig))
+                .testEquals();
+    }
+
+    private static VirtualCameraStreamConfig reparcel(VirtualCameraStreamConfig config) {
+        Parcel parcel = Parcel.obtain();
+        try {
+            config.writeToParcel(parcel, /* flags= */ 0);
+            parcel.setDataPosition(0);
+            return VirtualCameraStreamConfig.CREATOR.createFromParcel(parcel);
+        } finally {
+            parcel.recycle();
+        }
+    }
+
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
index 8487903..89056cc 100644
--- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
+++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
@@ -64,17 +64,18 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.eq;
 import static org.mockito.Matchers.intThat;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
+import android.annotation.DurationMillisLong;
 import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.usage.AppStandbyInfo;
 import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManager;
 import android.app.usage.UsageStatsManagerInternal;
 import android.appwidget.AppWidgetManager;
 import android.content.Context;
@@ -99,7 +100,6 @@
 import android.view.Display;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -110,6 +110,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -125,6 +126,7 @@
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
 import java.util.stream.Collectors;
 
 /**
@@ -175,6 +177,9 @@
     private static final int ASSERT_RETRY_ATTEMPTS = 20;
     private static final int ASSERT_RETRY_DELAY_MILLISECONDS = 500;
 
+    @DurationMillisLong
+    private static final long FLUSH_TIMEOUT_MILLISECONDS = 5_000;
+
     /** Mock variable used in {@link MyInjector#isPackageInstalled(String, int, int)} */
     private static boolean isPackageInstalled = true;
 
@@ -563,6 +568,7 @@
         mInjector = new MyInjector(myContext, Looper.getMainLooper());
         mController = setupController();
         setupInitialUsageHistory();
+        flushHandler(mController);
     }
 
     @After
@@ -571,12 +577,9 @@
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testBoundWidgetPackageExempt() throws Exception {
         assumeTrue(mInjector.getContext().getSystemService(AppWidgetManager.class) != null);
-        assertEquals(STANDBY_BUCKET_ACTIVE,
-                mController.getAppStandbyBucket(PACKAGE_EXEMPTED_1, USER_ID,
-                        mInjector.mElapsedRealtime, false));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_EXEMPTED_1);
     }
 
     @Test
@@ -654,7 +657,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testIsAppIdle_Charging() throws Exception {
         TestParoleListener paroleListener = new TestParoleListener();
         mController.addListener(paroleListener);
@@ -662,7 +664,7 @@
         setChargingState(mController, false);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_FORCED_BY_SYSTEM);
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false));
         assertFalse(mController.isInParole());
@@ -671,7 +673,7 @@
         setChargingState(mController, true);
         paroleListener.awaitOnLatch(2000);
         assertTrue(paroleListener.getParoleState());
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
         assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false));
         assertTrue(mController.isInParole());
@@ -680,14 +682,13 @@
         setChargingState(mController, false);
         paroleListener.awaitOnLatch(2000);
         assertFalse(paroleListener.getParoleState());
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false));
         assertFalse(mController.isInParole());
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testIsAppIdle_Enabled() throws Exception {
         setChargingState(mController, false);
         TestParoleListener paroleListener = new TestParoleListener();
@@ -696,7 +697,7 @@
         setAppIdleEnabled(mController, true);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_FORCED_BY_SYSTEM);
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false));
 
@@ -711,7 +712,7 @@
         setAppIdleEnabled(mController, true);
         paroleListener.awaitOnLatch(2000);
         assertFalse(paroleListener.getParoleState());
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false));
     }
@@ -723,6 +724,7 @@
     private void assertTimeout(AppStandbyController controller, long elapsedTime, int bucket,
             int userId) {
         mInjector.mElapsedRealtime = elapsedTime;
+        flushHandler(controller);
         controller.checkIdleStates(userId);
         assertEquals(bucket,
                 controller.getAppStandbyBucket(PACKAGE_1, userId, mInjector.mElapsedRealtime,
@@ -744,50 +746,85 @@
     }
 
     private int getStandbyBucket(int userId, AppStandbyController controller, String packageName) {
+        flushHandler(controller);
         return controller.getAppStandbyBucket(packageName, userId, mInjector.mElapsedRealtime,
                 true);
     }
 
+    private List<AppStandbyInfo> getStandbyBuckets(int userId) {
+        flushHandler(mController);
+        return mController.getAppStandbyBuckets(userId);
+    }
+
     private int getStandbyBucketReason(String packageName) {
+        flushHandler(mController);
         return mController.getAppStandbyBucketReason(packageName, USER_ID,
                 mInjector.mElapsedRealtime);
     }
 
-    private void assertBucket(int bucket) throws InterruptedException {
-        assertBucket(bucket, PACKAGE_1);
+    private void waitAndAssertBucket(int bucket, String pkg) {
+        waitAndAssertBucket(mController, bucket, pkg);
     }
 
-    private void assertBucket(int bucket, String pkg) throws InterruptedException {
-        int retries = 0;
-        do {
-            if (bucket == getStandbyBucket(mController, pkg)) {
-                // Success
-                return;
-            }
-            Thread.sleep(ASSERT_RETRY_DELAY_MILLISECONDS);
-            retries++;
-        } while(retries < ASSERT_RETRY_ATTEMPTS);
-        // try one last time
-        assertEquals(bucket, getStandbyBucket(mController, pkg));
+    private void waitAndAssertBucket(AppStandbyController controller, int bucket, String pkg) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(pkg);
+        sb.append(" was not in the ");
+        sb.append(UsageStatsManager.standbyBucketToString(bucket));
+        sb.append(" (");
+        sb.append(bucket);
+        sb.append(") bucket.");
+        waitAndAssertBucket(sb.toString(), controller, bucket, pkg);
     }
 
-    private void assertNotBucket(int bucket) throws InterruptedException {
-        final String pkg = PACKAGE_1;
+    private void waitAndAssertBucket(String msg, int bucket, String pkg) {
+        waitAndAssertBucket(msg, mController, bucket, pkg);
+    }
+
+    private void waitAndAssertBucket(String msg, AppStandbyController controller, int bucket,
+            String pkg) {
+        waitAndAssertBucket(msg, controller, bucket, USER_ID, pkg);
+    }
+
+    private void waitAndAssertBucket(String msg, AppStandbyController controller, int bucket,
+            int userId,
+            String pkg) {
+        waitUntil(() -> bucket == getStandbyBucket(userId, controller, pkg));
+        assertEquals(msg, bucket, getStandbyBucket(userId, controller, pkg));
+    }
+
+    private void waitAndAssertNotBucket(int bucket, String pkg) {
+        waitAndAssertNotBucket(mController, bucket, pkg);
+    }
+
+    private void waitAndAssertNotBucket(AppStandbyController controller, int bucket, String pkg) {
+        waitUntil(() -> bucket != getStandbyBucket(controller, pkg));
+        assertNotEquals(bucket, getStandbyBucket(controller, pkg));
+    }
+
+    private void waitAndAssertLastNoteEvent(int event) {
+        waitUntil(() -> {
+            flushHandler(mController);
+            return event == mInjector.mLastNoteEvent;
+        });
+        assertEquals(event, mInjector.mLastNoteEvent);
+    }
+
+    // Waits until condition is true or times out.
+    private void waitUntil(BooleanSupplier resultSupplier) {
         int retries = 0;
         do {
-            if (bucket != getStandbyBucket(mController, pkg)) {
-                // Success
-                return;
+            if (resultSupplier.getAsBoolean()) return;
+            try {
+                Thread.sleep(ASSERT_RETRY_DELAY_MILLISECONDS);
+            } catch (InterruptedException ie) {
+                // Do nothing
             }
-            Thread.sleep(ASSERT_RETRY_DELAY_MILLISECONDS);
             retries++;
-        } while(retries < ASSERT_RETRY_ATTEMPTS);
-        // try one last time
-        assertNotEquals(bucket, getStandbyBucket(mController, pkg));
+        } while (retries < ASSERT_RETRY_ATTEMPTS);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testBuckets() throws Exception {
         assertTimeout(mController, 0, STANDBY_BUCKET_NEVER);
 
@@ -820,14 +857,13 @@
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSetAppStandbyBucket() throws Exception {
         // For a known package, standby bucket should be set properly
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
         mInjector.mElapsedRealtime = HOUR_MS;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_TIMEOUT);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // For an unknown package, standby bucket should not be set, hence NEVER is returned
         // Ensure the unknown package is not already in history by removing it
@@ -836,21 +872,20 @@
         mController.setAppStandbyBucket(PACKAGE_UNKNOWN, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_TIMEOUT);
         isPackageInstalled = true; // Reset mocked variable for other tests
-        assertEquals(STANDBY_BUCKET_NEVER, getStandbyBucket(mController, PACKAGE_UNKNOWN));
+        waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_UNKNOWN);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testAppStandbyBucketOnInstallAndUninstall() throws Exception {
         // On package install, standby bucket should be ACTIVE
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_UNKNOWN);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_UNKNOWN));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_UNKNOWN);
 
         // On uninstall, package should not exist in history and should return a NEVER bucket
         mController.clearAppIdleForPackage(PACKAGE_UNKNOWN, USER_ID);
-        assertEquals(STANDBY_BUCKET_NEVER, getStandbyBucket(mController, PACKAGE_UNKNOWN));
+        waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_UNKNOWN);
         // Ensure uninstalled app is not in history
-        List<AppStandbyInfo> buckets = mController.getAppStandbyBuckets(USER_ID);
+        List<AppStandbyInfo> buckets = getStandbyBuckets(USER_ID);
         for(AppStandbyInfo bucket : buckets) {
             if (bucket.mPackageName.equals(PACKAGE_UNKNOWN)) {
                 fail("packageName found in app idle history after uninstall.");
@@ -859,7 +894,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testScreenTimeAndBuckets() throws Exception {
         mInjector.setDisplayOn(false);
 
@@ -876,22 +910,21 @@
         // RARE bucket, should fail because the screen wasn't ON.
         mInjector.mElapsedRealtime = RARE_THRESHOLD + 1;
         mController.checkIdleStates(USER_ID);
-        assertNotEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertNotBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         mInjector.setDisplayOn(true);
         assertTimeout(mController, RARE_THRESHOLD + 2 * HOUR_MS + 1, STANDBY_BUCKET_RARE);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testForcedIdle() throws Exception {
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
 
         mController.forceIdleState(PACKAGE_1, USER_ID, false);
-        assertEquals(STANDBY_BUCKET_ACTIVE, mController.getAppStandbyBucket(PACKAGE_1, USER_ID, 0,
-                true));
+
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
     }
 
@@ -901,15 +934,15 @@
                 .onPropertiesChanged(mInjector.getDeviceConfigProperties());
 
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime = 1;
         rearmQuotaBumpLatch(PACKAGE_1, USER_ID);
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
         assertFalse(mQuotaBumpLatch.await(1, TimeUnit.SECONDS));
     }
 
@@ -917,9 +950,10 @@
     public void testNotificationEvent_bucketPromotion_changePromotedBucket() throws Exception {
         mInjector.mPropertiesChangedListener
                 .onPropertiesChanged(mInjector.getDeviceConfigProperties());
+        mInjector.mElapsedRealtime += RARE_THRESHOLD + 1;
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         // TODO: Avoid hardcoding these string constants.
         mInjector.mSettingsBuilder.setInt("notification_seen_promoted_bucket",
@@ -928,11 +962,10 @@
                 mInjector.getDeviceConfigProperties());
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testNotificationEvent_quotaBump() throws Exception {
         mInjector.mSettingsBuilder
                 .setBoolean("trigger_quota_bump_on_notification_seen", true);
@@ -942,7 +975,7 @@
                 .onPropertiesChanged(mInjector.getDeviceConfigProperties());
 
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime = RARE_THRESHOLD * 2;
         setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM);
 
@@ -951,83 +984,80 @@
 
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
         assertTrue(mQuotaBumpLatch.await(1, TimeUnit.SECONDS));
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSlicePinnedEvent() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime = 1;
         reportEvent(mController, SLICE_PINNED, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
         reportEvent(mController, SLICE_PINNED, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSlicePinnedPrivEvent() throws Exception {
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
         reportEvent(mController, SLICE_PINNED_PRIV, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testPredictionTimedOut() throws Exception {
         // Set it to timeout or usage, so that prediction can override it
         mInjector.mElapsedRealtime = HOUR_MS;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Fast forward 12 hours
         mInjector.mElapsedRealtime += WORKING_SET_THRESHOLD;
         mController.checkIdleStates(USER_ID);
         // Should still be in predicted bucket, since prediction timeout is 1 day since prediction
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         // Fast forward two more hours
         mInjector.mElapsedRealtime += 2 * HOUR_MS;
         mController.checkIdleStates(USER_ID);
         // Should have now applied prediction timeout
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         // Fast forward RARE bucket
         mInjector.mElapsedRealtime += RARE_THRESHOLD;
         mController.checkIdleStates(USER_ID);
         // Should continue to apply prediction timeout
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
+    @Ignore("b/317086276")
     public void testOverrides() throws Exception {
         // Can force to NEVER
         mInjector.mElapsedRealtime = HOUR_MS;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_NEVER,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(STANDBY_BUCKET_NEVER, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_1);
 
         // Prediction can't override FORCED reasons
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_FORCED_BY_USER);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
 
         // Prediction can't override NEVER
         mInjector.mElapsedRealtime = 2 * HOUR_MS;
@@ -1035,115 +1065,114 @@
                 REASON_MAIN_DEFAULT);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_NEVER, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_1);
 
         // Prediction can't set to NEVER
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_USAGE);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_NEVER,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Prediction can't remove from RESTRICTED
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Force from user can remove from RESTRICTED
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         // Force from system can remove from RESTRICTED if it was put it in due to system
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_FORCED_BY_SYSTEM);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_FORCED_BY_SYSTEM);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_PREDICTED);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_FORCED_BY_SYSTEM);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Non-user usage can't remove from RESTRICTED
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_USAGE);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_USAGE | REASON_SUB_USAGE_SYSTEM_INTERACTION);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_USAGE | REASON_SUB_USAGE_SYNC_ADAPTER);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_USAGE | REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_NON_DOZE);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Explicit user usage can remove from RESTRICTED
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_USAGE | REASON_SUB_USAGE_MOVE_TO_FOREGROUND);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testTimeout() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mElapsedRealtime = 2000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // bucketing works after timeout
         mInjector.mElapsedRealtime = mController.mPredictionTimeoutMillis - 100;
         mController.checkIdleStates(USER_ID);
         // Use recent prediction
-        assertBucket(STANDBY_BUCKET_FREQUENT);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
 
         // Way past prediction timeout, use system thresholds
         mInjector.mElapsedRealtime = RARE_THRESHOLD;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     /** Test that timeouts still work properly even if invalid configuration values are set. */
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testTimeout_InvalidThresholds() throws Exception {
         mInjector.mSettingsBuilder
                 .setLong("screen_threshold_active", -1)
@@ -1161,19 +1190,19 @@
 
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mElapsedRealtime = HOUR_MS;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_FREQUENT);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
 
         mInjector.mElapsedRealtime = 2 * HOUR_MS;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         mInjector.mElapsedRealtime = 4 * HOUR_MS;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     /**
@@ -1181,74 +1210,72 @@
      * timeout has passed.
      */
     @Test
-    @FlakyTest(bugId = 185169504)
+    @Ignore("b/317086276")
     public void testTimeoutBeforeRestricted() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mElapsedRealtime += WORKING_SET_THRESHOLD;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         // Bucket shouldn't change
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // bucketing works after timeout
         mInjector.mElapsedRealtime += DAY_MS;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Way past all timeouts. Make sure timeout processing doesn't raise bucket.
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     /**
      * Test that an app is put into the RESTRICTED bucket after enough time has passed.
      */
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testRestrictedDelay() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mElapsedRealtime += mInjector.getAutoRestrictedBucketDelayMs() - 5000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         // Bucket shouldn't change
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // bucketing works after timeout
         mInjector.mElapsedRealtime += 6000;
 
         Thread.sleep(6000);
         // Enough time has passed. The app should automatically be put into the RESTRICTED bucket.
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     /**
      * Test that an app is put into the RESTRICTED bucket after enough time has passed.
      */
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testRestrictedDelay_DelayChange() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mAutoRestrictedBucketDelayMs = 2 * HOUR_MS;
         mInjector.mElapsedRealtime += 2 * HOUR_MS - 5000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         // Bucket shouldn't change
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // bucketing works after timeout
         mInjector.mElapsedRealtime += 6000;
 
         Thread.sleep(6000);
         // Enough time has passed. The app should automatically be put into the RESTRICTED bucket.
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     /**
@@ -1256,36 +1283,35 @@
      * a low bucket after the RESTRICTED timeout.
      */
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testRestrictedTimeoutOverridesRestoredLowBucketPrediction() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Predict to RARE Not long enough to time out into RESTRICTED.
         mInjector.mElapsedRealtime += RARE_THRESHOLD;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         // Add a short timeout event
         mInjector.mElapsedRealtime += 1000;
         reportEvent(mController, SYSTEM_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime += 1000;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Long enough that it could have timed out into RESTRICTED. Instead of reverting to
         // predicted RARE, should go into RESTRICTED
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Ensure that prediction can still raise it out despite this override.
         mInjector.mElapsedRealtime += 1;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
     }
 
     /**
@@ -1293,7 +1319,6 @@
      * a low bucket after the RESTRICTED timeout.
      */
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testRestrictedTimeoutOverridesPredictionLowBucket() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
 
@@ -1301,7 +1326,7 @@
         mInjector.mElapsedRealtime += RARE_THRESHOLD;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         mInjector.mElapsedRealtime += 1;
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
@@ -1310,10 +1335,10 @@
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     /**
@@ -1321,261 +1346,250 @@
      * interaction.
      */
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSystemInteractionOverridesRestrictedTimeout() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Long enough that it could have timed out into RESTRICTED.
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Report system interaction.
         mInjector.mElapsedRealtime += 1000;
         reportEvent(mController, SYSTEM_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
 
         // Ensure that it's raised out of RESTRICTED for the system interaction elevation duration.
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime += 1000;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Elevation duration over. Should fall back down.
         mInjector.mElapsedRealtime += 10 * MINUTE_MS;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testPredictionRaiseFromRestrictedTimeout_highBucket() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
 
         // Way past all timeouts. App times out into RESTRICTED bucket.
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Since the app timed out into RESTRICTED, prediction should be able to remove from the
         // bucket.
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testPredictionRaiseFromRestrictedTimeout_lowBucket() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
 
         // Way past all timeouts. App times out into RESTRICTED bucket.
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Prediction into a low bucket means no expectation of the app being used, so we shouldn't
         // elevate the app from RESTRICTED.
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testCascadingTimeouts() throws Exception {
         mInjector.mPropertiesChangedListener
                 .onPropertiesChanged(mInjector.getDeviceConfigProperties());
 
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         reportEvent(mController, NOTIFICATION_SEEN, 1000, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mElapsedRealtime = 2000 + mController.mStrongUsageTimeoutMillis;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_WORKING_SET);
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         mInjector.mElapsedRealtime = 2000 + mController.mNotificationSeenTimeoutMillis;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_FREQUENT);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testOverlappingTimeouts() throws Exception {
         mInjector.mPropertiesChangedListener
                 .onPropertiesChanged(mInjector.getDeviceConfigProperties());
 
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         reportEvent(mController, NOTIFICATION_SEEN, 1000, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Overlapping USER_INTERACTION before previous one times out
         reportEvent(mController, USER_INTERACTION, mController.mStrongUsageTimeoutMillis - 1000,
                 PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Still in ACTIVE after first USER_INTERACTION times out
         mInjector.mElapsedRealtime = mController.mStrongUsageTimeoutMillis + 1000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Both timed out, so NOTIFICATION_SEEN timeout should be effective
         mInjector.mElapsedRealtime = mController.mStrongUsageTimeoutMillis * 2 + 2000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_WORKING_SET);
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         mInjector.mElapsedRealtime = mController.mNotificationSeenTimeoutMillis + 2000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSystemInteractionTimeout() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
         // Fast forward to RARE
         mInjector.mElapsedRealtime = RARE_THRESHOLD + 100;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         // Trigger a SYSTEM_INTERACTION and verify bucket
         reportEvent(mController, SYSTEM_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Verify it's still in ACTIVE close to end of timeout
         mInjector.mElapsedRealtime += mController.mSystemInteractionTimeoutMillis - 100;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Verify bucket moves to RARE after timeout
         mInjector.mElapsedRealtime += 200;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testInitialForegroundServiceTimeout() throws Exception {
         mInjector.mElapsedRealtime = 1 * RARE_THRESHOLD + 100;
         // Make sure app is in NEVER bucket
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_NEVER,
                 REASON_MAIN_FORCED_BY_USER);
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_NEVER);
+        waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_1);
 
         mInjector.mElapsedRealtime += 100;
 
         // Trigger a FOREGROUND_SERVICE_START and verify bucket
         reportEvent(mController, FOREGROUND_SERVICE_START, mInjector.mElapsedRealtime, PACKAGE_1);
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Verify it's still in ACTIVE close to end of timeout
         mInjector.mElapsedRealtime += mController.mInitialForegroundServiceStartTimeoutMillis - 100;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Verify bucket moves to RARE after timeout
         mInjector.mElapsedRealtime += 200;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         // Trigger a FOREGROUND_SERVICE_START again
         reportEvent(mController, FOREGROUND_SERVICE_START, mInjector.mElapsedRealtime, PACKAGE_1);
         mController.checkIdleStates(USER_ID);
         // Bucket should not be immediately elevated on subsequent service starts
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testPredictionNotOverridden() throws Exception {
         mInjector.mPropertiesChangedListener
                 .onPropertiesChanged(mInjector.getDeviceConfigProperties());
 
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mElapsedRealtime = WORKING_SET_THRESHOLD - 1000;
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Falls back to WORKING_SET
         mInjector.mElapsedRealtime += 5000;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_WORKING_SET);
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         // Predict to ACTIVE
         mInjector.mElapsedRealtime += 1000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // CheckIdleStates should not change the prediction
         mInjector.mElapsedRealtime += 1000;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testPredictionStrikesBack() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Predict to FREQUENT
         mInjector.mElapsedRealtime = RARE_THRESHOLD;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_FREQUENT);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
 
         // Add a short timeout event
         mInjector.mElapsedRealtime += 1000;
         reportEvent(mController, SYSTEM_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime += 1000;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Verify it reverted to predicted
         mInjector.mElapsedRealtime += WORKING_SET_THRESHOLD / 2;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_FREQUENT);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSystemForcedFlags_NotAddedForUserForce() throws Exception {
         final int expectedReason = REASON_MAIN_FORCED_BY_USER;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         assertEquals(expectedReason, getStandbyBucketReason(PACKAGE_1));
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         assertEquals(expectedReason, getStandbyBucketReason(PACKAGE_1));
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSystemForcedFlags_AddedForSystemForce() throws Exception {
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_DEFAULT);
@@ -1584,13 +1598,13 @@
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE,
                 getStandbyBucketReason(PACKAGE_1));
 
         mController.restrictApp(PACKAGE_1, USER_ID, REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         // Flags should be combined
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM
                 | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE
@@ -1598,7 +1612,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSystemForcedFlags_SystemForceChangesBuckets() throws Exception {
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_DEFAULT);
@@ -1607,14 +1620,14 @@
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_FORCED_BY_SYSTEM
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE);
-        assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE,
                 getStandbyBucketReason(PACKAGE_1));
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE);
-        assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
         // Flags should be combined
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE
@@ -1623,20 +1636,19 @@
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY);
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         // Flags should be combined
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY,
                 getStandbyBucketReason(PACKAGE_1));
 
         mController.restrictApp(PACKAGE_1, USER_ID, REASON_SUB_FORCED_SYSTEM_FLAG_UNDEFINED);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         // Flags should not be combined since the bucket changed.
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_UNDEFINED,
                 getStandbyBucketReason(PACKAGE_1));
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testRestrictApp_MainReason() throws Exception {
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_DEFAULT);
@@ -1644,11 +1656,11 @@
 
         mController.restrictApp(PACKAGE_1, USER_ID, REASON_MAIN_PREDICTED, 0);
         // Call should be ignored.
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mController.restrictApp(PACKAGE_1, USER_ID, REASON_MAIN_FORCED_BY_USER, 0);
         // Call should go through
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     @Test
@@ -1724,15 +1736,15 @@
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testUserInteraction_CrossProfile() throws Exception {
         mInjector.mRunningUsers = new int[] {USER_ID, USER_ID2, USER_ID3};
         mInjector.mCrossProfileTargets = Arrays.asList(USER_HANDLE_USER2);
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertEquals("Cross profile connected package bucket should be elevated on usage",
-                STANDBY_BUCKET_ACTIVE, getStandbyBucket(USER_ID2, mController, PACKAGE_1));
-        assertEquals("Not Cross profile connected package bucket should not be elevated on usage",
-                STANDBY_BUCKET_NEVER, getStandbyBucket(USER_ID3, mController, PACKAGE_1));
+        waitAndAssertBucket("Cross profile connected package bucket should be elevated on usage",
+                mController, STANDBY_BUCKET_ACTIVE, USER_ID2, PACKAGE_1);
+        waitAndAssertBucket(
+                "Not Cross profile connected package bucket should not be elevated on usage",
+                mController, STANDBY_BUCKET_NEVER, USER_ID3, PACKAGE_1);
 
         assertTimeout(mController, WORKING_SET_THRESHOLD - 1, STANDBY_BUCKET_ACTIVE, USER_ID);
         assertTimeout(mController, WORKING_SET_THRESHOLD - 1, STANDBY_BUCKET_ACTIVE, USER_ID2);
@@ -1742,51 +1754,50 @@
 
         mInjector.mCrossProfileTargets = Collections.emptyList();
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertEquals("No longer cross profile connected package bucket should not be "
-                        + "elevated on usage",
-                STANDBY_BUCKET_WORKING_SET, getStandbyBucket(USER_ID2, mController, PACKAGE_1));
+        waitAndAssertBucket("No longer cross profile connected package bucket should not be "
+                        + "elevated on usage", mController, STANDBY_BUCKET_WORKING_SET, USER_ID2,
+                PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testUnexemptedSyncScheduled() throws Exception {
         rearmLatch(PACKAGE_1);
         mController.addListener(mListener);
-        assertEquals("Test package did not start in the Never bucket", STANDBY_BUCKET_NEVER,
-                getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket("Test package did not start in the Never bucket", STANDBY_BUCKET_NEVER,
+                PACKAGE_1);
 
         mController.postReportSyncScheduled(PACKAGE_1, USER_ID, false);
         mStateChangedLatch.await(1000, TimeUnit.MILLISECONDS);
-        assertEquals("Unexempted sync scheduled should bring the package out of the Never bucket",
-                STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(
+                "Unexempted sync scheduled should bring the package out of the Never bucket",
+                STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM);
 
         rearmLatch(PACKAGE_1);
         mController.postReportSyncScheduled(PACKAGE_1, USER_ID, false);
         mStateChangedLatch.await(1000, TimeUnit.MILLISECONDS);
-        assertEquals("Unexempted sync scheduled should not elevate a non Never bucket",
-                STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket("Unexempted sync scheduled should not elevate a non Never bucket",
+                STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testExemptedSyncScheduled() throws Exception {
         setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM);
         mInjector.mDeviceIdleMode = true;
         rearmLatch(PACKAGE_1);
         mController.postReportSyncScheduled(PACKAGE_1, USER_ID, true);
         mStateChangedLatch.await(1000, TimeUnit.MILLISECONDS);
-        assertEquals("Exempted sync scheduled in doze should set bucket to working set",
-                STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket("Exempted sync scheduled in doze should set bucket to working set",
+                STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM);
         mInjector.mDeviceIdleMode = false;
         rearmLatch(PACKAGE_1);
         mController.postReportSyncScheduled(PACKAGE_1, USER_ID, true);
         mStateChangedLatch.await(1000, TimeUnit.MILLISECONDS);
-        assertEquals("Exempted sync scheduled while not in doze should set bucket to active",
-                STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket("Exempted sync scheduled while not in doze should set bucket to active",
+                STANDBY_BUCKET_ACTIVE, PACKAGE_1);
     }
 
     @Test
@@ -1796,14 +1807,14 @@
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Updates shouldn't change bucket if the app was forced by the system for a non-buggy
         // reason.
@@ -1814,11 +1825,11 @@
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE);
 
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Updates should change bucket if the app was forced by the system for a buggy reason.
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
@@ -1827,11 +1838,11 @@
                 REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY);
 
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID);
-        assertNotEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertNotBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Updates shouldn't change bucket if the app was forced by the system for more than just
         // a buggy reason.
@@ -1842,13 +1853,13 @@
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY);
 
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE,
                 getStandbyBucketReason(PACKAGE_1));
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Updates shouldn't change bucket if the app was forced by the user.
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
@@ -1857,11 +1868,11 @@
                 REASON_MAIN_FORCED_BY_USER);
 
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     @Test
@@ -1876,37 +1887,37 @@
 
         mController.setAppStandbyBucket(PACKAGE_SYSTEM_HEADFULL, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_RARE, PACKAGE_SYSTEM_HEADFULL);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_SYSTEM_HEADFULL);
 
         // Make sure headless system apps don't get lowered.
         mController.setAppStandbyBucket(PACKAGE_SYSTEM_HEADLESS, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_SYSTEM_HEADLESS);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_SYSTEM_HEADLESS);
 
         // Package 1 doesn't have activities and is headless, but is not a system app, so it can
         // be lowered.
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
     public void testWellbeingAppElevated() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_WELLBEING);
-        assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_WELLBEING);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_WELLBEING);
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD;
 
         // Make sure the default wellbeing app does not get lowered below WORKING_SET.
         mController.setAppStandbyBucket(PACKAGE_WELLBEING, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_WELLBEING);
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_WELLBEING);
 
         // A non default wellbeing app should be able to fall lower than WORKING_SET.
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
@@ -1914,22 +1925,22 @@
         mInjector.mClockApps.add(Pair.create(PACKAGE_1, UID_1));
 
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_2);
-        assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_2);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_2);
 
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD;
 
         // Make sure a clock app does not get lowered below WORKING_SET.
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         // A non clock app should be able to fall lower than WORKING_SET.
         mController.setAppStandbyBucket(PACKAGE_2, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_RARE, PACKAGE_2);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_2);
     }
 
     @Test
@@ -2067,13 +2078,13 @@
     public void testBackgroundLocationBucket() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime,
                 PACKAGE_BACKGROUND_LOCATION);
-        assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_BACKGROUND_LOCATION);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_BACKGROUND_LOCATION);
 
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD;
         // Make sure PACKAGE_BACKGROUND_LOCATION does not get lowered than STANDBY_BUCKET_FREQUENT.
         mController.setAppStandbyBucket(PACKAGE_BACKGROUND_LOCATION, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_BACKGROUND_LOCATION);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_BACKGROUND_LOCATION);
     }
 
     @Test
@@ -2083,41 +2094,41 @@
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE);
 
         // Since we're staying on the PACKAGE_ACTIVE side, noteEvent shouldn't be called.
         // Reset the last event to confirm the method isn't called.
         mInjector.mLastNoteEvent = BatteryStats.HistoryItem.EVENT_NONE;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_NONE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_NONE);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE);
 
         // Since we're staying on the PACKAGE_ACTIVE side, noteEvent shouldn't be called.
         // Reset the last event to confirm the method isn't called.
         mInjector.mLastNoteEvent = BatteryStats.HistoryItem.EVENT_NONE;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_NONE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_NONE);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_EXEMPTED,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE);
     }
 
     private String getAdminAppsStr(int userId) {
@@ -2187,8 +2198,7 @@
         rearmLatch(pkg);
         mController.setAppStandbyBucket(pkg, user, bucket, reason);
         mStateChangedLatch.await(1, TimeUnit.SECONDS);
-        assertEquals("Failed to set package bucket", bucket,
-                getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket("Failed to set package bucket", bucket, PACKAGE_1);
     }
 
     private void rearmLatch(String pkgName) {
@@ -2205,4 +2215,12 @@
         mLatchUserId = userId;
         mQuotaBumpLatch = new CountDownLatch(1);
     }
+
+    private void flushHandler(AppStandbyController controller) {
+        assertTrue("Failed to flush handler!", controller.flushHandler(FLUSH_TIMEOUT_MILLISECONDS));
+        // Some AppStandbyController handler messages queue another handler message. Flush again
+        // to catch those as well.
+        assertTrue("Failed to flush handler (the second time)!",
+                controller.flushHandler(FLUSH_TIMEOUT_MILLISECONDS));
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
index 260ee396..30843d2 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
@@ -245,8 +245,8 @@
     }
 
     @Test
-    @TestParameters({"{origin: ORIGIN_USER}", "{origin: ORIGIN_INIT}", "{origin: ORIGIN_INIT_USER}",
-            "{origin: ORIGIN_SYSTEM_OR_SYSTEMUI}"})
+    @TestParameters({"{origin: ORIGIN_USER}", "{origin: ORIGIN_INIT}",
+            "{origin: ORIGIN_INIT_USER}"})
     public void apply_nightModeWithScreenOn_appliedImmediatelyBasedOnOrigin(ChangeOrigin origin) {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
 
@@ -263,7 +263,7 @@
 
     @Test
     @TestParameters({"{origin: ORIGIN_APP}", "{origin: ORIGIN_RESTORE_BACKUP}",
-            "{origin: ORIGIN_UNKNOWN}"})
+            "{origin: ORIGIN_SYSTEM_OR_SYSTEMUI}", "{origin: ORIGIN_UNKNOWN}"})
     public void apply_nightModeWithScreenOn_willBeAppliedLaterBasedOnOrigin(ChangeOrigin origin) {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
 
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 56c75b5..e004ca0 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -80,6 +80,9 @@
 import static android.service.notification.Adjustment.KEY_IMPORTANCE;
 import static android.service.notification.Adjustment.KEY_USER_SENTIMENT;
 import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS;
+import static android.service.notification.Condition.SOURCE_CONTEXT;
+import static android.service.notification.Condition.SOURCE_USER_ACTION;
+import static android.service.notification.Condition.STATE_TRUE;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
@@ -224,6 +227,7 @@
 import android.provider.MediaStore;
 import android.provider.Settings;
 import android.service.notification.Adjustment;
+import android.service.notification.Condition;
 import android.service.notification.ConversationChannelWrapper;
 import android.service.notification.DeviceEffectsApplier;
 import android.service.notification.INotificationListener;
@@ -342,6 +346,11 @@
     private static final String SCHEME_TIMEOUT = "timeout";
     private static final String REDACTED_TEXT = "redacted text";
 
+    private static final AutomaticZenRule SOME_ZEN_RULE =
+            new AutomaticZenRule.Builder("rule", Uri.parse("uri"))
+                    .setOwner(new ComponentName("pkg", "cls"))
+                    .build();
+
     private final int mUid = Binder.getCallingUid();
     private final @UserIdInt int mUserId = UserHandle.getUserId(mUid);
 
@@ -9048,7 +9057,7 @@
                 zenPolicy, NotificationManager.INTERRUPTION_FILTER_NONE, isEnabled);
 
         try {
-            mBinderService.addAutomaticZenRule(rule, mContext.getPackageName());
+            mBinderService.addAutomaticZenRule(rule, mContext.getPackageName(), false);
             fail("Zen policy only applies to priority only mode");
         } catch (IllegalArgumentException e) {
             // yay
@@ -9056,11 +9065,11 @@
 
         rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class),
                 zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled);
-        mBinderService.addAutomaticZenRule(rule, mContext.getPackageName());
+        mBinderService.addAutomaticZenRule(rule, mContext.getPackageName(), false);
 
         rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class),
                 null, NotificationManager.INTERRUPTION_FILTER_NONE, isEnabled);
-        mBinderService.addAutomaticZenRule(rule, mContext.getPackageName());
+        mBinderService.addAutomaticZenRule(rule, mContext.getPackageName(), false);
     }
 
     @Test
@@ -9075,7 +9084,7 @@
         boolean isEnabled = true;
         AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class),
                 zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled);
-        mBinderService.addAutomaticZenRule(rule, "com.android.settings");
+        mBinderService.addAutomaticZenRule(rule, "com.android.settings", false);
 
         // verify that zen mode helper gets passed in a package name of "android"
         verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule),
@@ -9097,7 +9106,7 @@
         boolean isEnabled = true;
         AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class),
                 zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled);
-        mBinderService.addAutomaticZenRule(rule, "com.android.settings");
+        mBinderService.addAutomaticZenRule(rule, "com.android.settings", false);
 
         // verify that zen mode helper gets passed in a package name of "android"
         verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule),
@@ -9117,7 +9126,7 @@
         boolean isEnabled = true;
         AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class),
                 zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled);
-        mBinderService.addAutomaticZenRule(rule, "another.package");
+        mBinderService.addAutomaticZenRule(rule, "another.package", false);
 
         // verify that zen mode helper gets passed in the package name from the arg, not the owner
         verify(mockZenModeHelper).addAutomaticZenRule(
@@ -9128,10 +9137,8 @@
     @Test
     @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void testAddAutomaticZenRule_typeManagedCanBeUsedByDeviceOwners() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
         mService.setCallerIsNormalPackage();
-        mService.setZenHelper(mock(ZenModeHelper.class));
-        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
-                .thenReturn(true);
 
         AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri"))
                 .setType(AutomaticZenRule.TYPE_MANAGED)
@@ -9139,8 +9146,9 @@
                 .build();
         when(mDevicePolicyManager.isActiveDeviceOwner(anyInt())).thenReturn(true);
 
-        mBinderService.addAutomaticZenRule(rule, "pkg");
-        // No exception!
+        mBinderService.addAutomaticZenRule(rule, "pkg", /* fromUser= */ false);
+
+        verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(rule), anyInt(), any(), anyInt());
     }
 
     @Test
@@ -9158,7 +9166,144 @@
         when(mDevicePolicyManager.isActiveDeviceOwner(anyInt())).thenReturn(false);
 
         assertThrows(IllegalArgumentException.class,
-                () -> mBinderService.addAutomaticZenRule(rule, "pkg"));
+                () -> mBinderService.addAutomaticZenRule(rule, "pkg", /* fromUser= */ false));
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void addAutomaticZenRule_fromUser_mappedToOriginUser() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.isSystemUid = true;
+
+        mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ true);
+
+        verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE),
+                eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyString(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void addAutomaticZenRule_fromSystemNotUser_mappedToOriginSystem() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.isSystemUid = true;
+
+        mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ false);
+
+        verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE),
+                eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void addAutomaticZenRule_fromApp_mappedToOriginApp() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.setCallerIsNormalPackage();
+
+        mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ false);
+
+        verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE),
+                eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyString(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void addAutomaticZenRule_fromAppFromUser_blocked() throws Exception {
+        setUpMockZenTest();
+        mService.setCallerIsNormalPackage();
+
+        assertThrows(SecurityException.class, () ->
+                mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ true));
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void updateAutomaticZenRule_fromUserFromSystem_allowed() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.isSystemUid = true;
+
+        mBinderService.updateAutomaticZenRule("id", SOME_ZEN_RULE, /* fromUser= */ true);
+
+        verify(zenModeHelper).updateAutomaticZenRule(eq("id"), eq(SOME_ZEN_RULE),
+                eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyString(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void updateAutomaticZenRule_fromUserFromApp_blocked() throws Exception {
+        setUpMockZenTest();
+        mService.setCallerIsNormalPackage();
+
+        assertThrows(SecurityException.class, () ->
+                mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ true));
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void removeAutomaticZenRule_fromUserFromSystem_allowed() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.isSystemUid = true;
+
+        mBinderService.removeAutomaticZenRule("id", /* fromUser= */ true);
+
+        verify(zenModeHelper).removeAutomaticZenRule(eq("id"),
+                eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyString(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void removeAutomaticZenRule_fromUserFromApp_blocked() throws Exception {
+        setUpMockZenTest();
+        mService.setCallerIsNormalPackage();
+
+        assertThrows(SecurityException.class, () ->
+                mBinderService.removeAutomaticZenRule("id", /* fromUser= */ true));
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void setAutomaticZenRuleState_fromUserMatchesConditionSource_okay() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.setCallerIsNormalPackage();
+
+        Condition withSourceContext = new Condition(Uri.parse("uri"), "summary", STATE_TRUE,
+                SOURCE_CONTEXT);
+        mBinderService.setAutomaticZenRuleState("id", withSourceContext, /* fromUser= */ false);
+        verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceContext),
+                eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyInt());
+
+        Condition withSourceUser = new Condition(Uri.parse("uri"), "summary", STATE_TRUE,
+                SOURCE_USER_ACTION);
+        mBinderService.setAutomaticZenRuleState("id", withSourceUser, /* fromUser= */ true);
+        verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceUser),
+                eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void setAutomaticZenRuleState_fromUserDoesNotMatchConditionSource_blocked()
+            throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.setCallerIsNormalPackage();
+
+        Condition withSourceContext = new Condition(Uri.parse("uri"), "summary", STATE_TRUE,
+                SOURCE_CONTEXT);
+        assertThrows(IllegalArgumentException.class,
+                () -> mBinderService.setAutomaticZenRuleState("id", withSourceContext,
+                        /* fromUser= */ true));
+
+        Condition withSourceUser = new Condition(Uri.parse("uri"), "summary", STATE_TRUE,
+                SOURCE_USER_ACTION);
+        assertThrows(IllegalArgumentException.class,
+                () -> mBinderService.setAutomaticZenRuleState("id", withSourceUser,
+                        /* fromUser= */ false));
+    }
+
+    private ZenModeHelper setUpMockZenTest() {
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.setZenHelper(zenModeHelper);
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        return zenModeHelper;
     }
 
     @Test
@@ -9184,7 +9329,7 @@
         });
 
         mService.getBinderService().setZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS, null,
-                "testing!");
+                "testing!", false);
         waitForIdle();
 
         InOrder inOrder = inOrder(mContext);
@@ -12157,7 +12302,9 @@
                 /* isImageBitmap= */ true,
                 /* isExpired= */ true);
         addRecordAndRemoveBitmaps(record);
-        assertThat(record.getNotification().extras.containsKey(EXTRA_PICTURE)).isFalse();
+        assertThat(record.getNotification().extras.containsKey(EXTRA_PICTURE)).isTrue();
+        final Parcelable picture = record.getNotification().extras.getParcelable(EXTRA_PICTURE);
+        assertThat(picture).isNull();
     }
 
     @Test
@@ -12191,7 +12338,10 @@
                 /* isImageBitmap= */ false,
                 /* isExpired= */ true);
         addRecordAndRemoveBitmaps(record);
-        assertThat(record.getNotification().extras.containsKey(EXTRA_PICTURE_ICON)).isFalse();
+        assertThat(record.getNotification().extras.containsKey(EXTRA_PICTURE_ICON)).isTrue();
+        final Parcelable pictureIcon =
+                record.getNotification().extras.getParcelable(EXTRA_PICTURE_ICON);
+        assertThat(pictureIcon).isNull();
     }
 
     @Test
@@ -13441,9 +13591,10 @@
                 .thenReturn(true);
 
         NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
-        mBinderService.setNotificationPolicy("package", policy);
+        mBinderService.setNotificationPolicy("package", policy, false);
 
-        verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy));
+        verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy),
+                eq(ZenModeConfig.UPDATE_ORIGIN_APP));
     }
 
     @Test
@@ -13457,7 +13608,7 @@
         mService.isSystemUid = true;
 
         NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
-        mBinderService.setNotificationPolicy("package", policy);
+        mBinderService.setNotificationPolicy("package", policy, false);
 
         verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt());
     }
@@ -13479,7 +13630,7 @@
                                 .build()));
 
         NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
-        mBinderService.setNotificationPolicy("package", policy);
+        mBinderService.setNotificationPolicy("package", policy, false);
 
         verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt());
     }
@@ -13495,7 +13646,7 @@
                 .thenReturn(true);
 
         NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
-        mBinderService.setNotificationPolicy("package", policy);
+        mBinderService.setNotificationPolicy("package", policy, false);
 
         verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt());
     }
@@ -13525,7 +13676,7 @@
         when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
                 .thenReturn(true);
 
-        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false);
 
         verify(zenHelper).applyGlobalZenModeAsImplicitZenRule(eq("package"), anyInt(),
                 eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS));
@@ -13542,7 +13693,7 @@
                 .thenReturn(true);
         mService.isSystemUid = true;
 
-        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false);
 
         verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
                 eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(), eq("package"),
@@ -13565,7 +13716,7 @@
                                 .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH)
                                 .build()));
 
-        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false);
 
         verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
                 eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyString(), eq("package"), anyInt());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java
index 4a1435f..1fcee06 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java
@@ -99,7 +99,7 @@
     public boolean getFromSystemOrSystemUi(int i) throws IllegalArgumentException {
         // While this isn't a logged output value, it's still helpful to check in tests.
         checkInRange(i);
-        return mChanges.get(i).mFromSystemOrSystemUi;
+        return mChanges.get(i).isFromSystemOrSystemUi();
     }
 
     public boolean getIsUserAction(int i) throws IllegalArgumentException {
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 1aea56c..44f0894 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -43,6 +43,8 @@
 import static android.provider.Settings.Global.ZEN_MODE_ALARMS;
 import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
 import static android.provider.Settings.Global.ZEN_MODE_OFF;
+import static android.service.notification.Condition.SOURCE_SCHEDULE;
+import static android.service.notification.Condition.SOURCE_USER_ACTION;
 import static android.service.notification.Condition.STATE_FALSE;
 import static android.service.notification.Condition.STATE_TRUE;
 import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_APP;
@@ -112,6 +114,7 @@
 import android.os.Parcel;
 import android.os.Process;
 import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.provider.Settings.Global;
@@ -119,12 +122,13 @@
 import android.service.notification.DeviceEffectsApplier;
 import android.service.notification.ZenDeviceEffects;
 import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ConfigChangeOrigin;
 import android.service.notification.ZenModeConfig.ScheduleInfo;
 import android.service.notification.ZenModeConfig.ZenRule;
 import android.service.notification.ZenModeDiff;
 import android.service.notification.ZenPolicy;
 import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
+import android.testing.TestWithLooperRule;
 import android.testing.TestableLooper;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -147,6 +151,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.truth.Correspondence;
 import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -172,7 +178,7 @@
 
 @SmallTest
 @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service.
-@RunWith(AndroidTestingRunner.class)
+@RunWith(TestParameterInjector.class)
 @TestableLooper.RunWithLooper
 public class ZenModeHelperTest extends UiServiceTestCase {
 
@@ -211,7 +217,11 @@
     private static final ZenDeviceEffects NO_EFFECTS = new ZenDeviceEffects.Builder().build();
 
     @Rule
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(
+            SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
+
+    @Rule(order = Integer.MAX_VALUE) // set the highest order so it's the innermost rule
+    public TestWithLooperRule mLooperRule = new TestWithLooperRule();
 
     ConditionProviders mConditionProviders;
     @Mock NotificationManager mNotificationManager;
@@ -2339,15 +2349,38 @@
         assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode);
     }
 
+    private enum ModesApiFlag {
+        ENABLED(true, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_USER),
+        DISABLED(false, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI);
+
+        private final boolean mEnabled;
+        @ConfigChangeOrigin private final int mOriginForUserActionInSystemUi;
+
+        ModesApiFlag(boolean enabled, @ConfigChangeOrigin int originForUserActionInSystemUi) {
+            this.mEnabled = enabled;
+            this.mOriginForUserActionInSystemUi = originForUserActionInSystemUi;
+        }
+
+        void applyFlag(SetFlagsRule setFlagsRule) {
+            if (mEnabled) {
+                setFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+            } else {
+                setFlagsRule.disableFlags(Flags.FLAG_MODES_API);
+            }
+        }
+    }
+
     @Test
-    public void testZenModeEventLog_setManualZenMode() throws IllegalArgumentException {
+    public void testZenModeEventLog_setManualZenMode(@TestParameter ModesApiFlag modesApiFlag)
+            throws IllegalArgumentException {
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
         // Turn zen mode on (to important_interruptions)
         // Need to additionally call the looper in order to finish the post-apply-config process
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID);
+                modesApiFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID);
 
         // Now turn zen mode off, but via a different package UID -- this should get registered as
         // "not an action by the user" because some other app is changing zen mode
@@ -2374,7 +2407,8 @@
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
-        assertTrue(mZenModeEventLogger.getFromSystemOrSystemUi(0));
+        assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isEqualTo(
+                modesApiFlag == ModesApiFlag.DISABLED);
         assertTrue(mZenModeEventLogger.getIsUserAction(0));
         assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0));
         checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0));
@@ -2399,7 +2433,9 @@
     }
 
     @Test
-    public void testZenModeEventLog_automaticRules() throws IllegalArgumentException {
+    public void testZenModeEventLog_automaticRules(@TestParameter ModesApiFlag modesApiFlag)
+            throws IllegalArgumentException {
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -2421,8 +2457,8 @@
 
         // Event 2: "User" turns off the automatic rule (sets it to not enabled)
         zenRule.setEnabled(false);
-        mZenModeHelper.updateAutomaticZenRule(id, zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "",
-                Process.SYSTEM_UID);
+        mZenModeHelper.updateAutomaticZenRule(id, zenRule,
+                modesApiFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID);
 
         // Add a new system rule
         AutomaticZenRule systemRule = new AutomaticZenRule("systemRule",
@@ -2440,8 +2476,8 @@
                 UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
 
         // Event 4: "User" deletes the rule
-        mZenModeHelper.removeAutomaticZenRule(systemId, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "",
-                Process.SYSTEM_UID);
+        mZenModeHelper.removeAutomaticZenRule(systemId, modesApiFlag.mOriginForUserActionInSystemUi,
+                "", Process.SYSTEM_UID);
 
         // In total, this represents 4 events
         assertEquals(4, mZenModeEventLogger.numLoggedChanges());
@@ -2497,20 +2533,109 @@
     }
 
     @Test
-    public void testZenModeEventLog_policyChanges() throws IllegalArgumentException {
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testZenModeEventLog_automaticRuleActivatedFromAppByAppAndUser()
+            throws IllegalArgumentException {
+        mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
+        setupZenConfig();
+
+        // Ann app adds an automatic zen rule
+        AutomaticZenRule zenRule = new AutomaticZenRule("name",
+                null,
+                new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
+                ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+                null,
+                NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+        String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
+                UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID);
+
+        // Event 1: Mimic the rule coming on manually when the user turns it on in the app
+        // ("Turn on bedtime now" because user goes to bed earlier).
+        mZenModeHelper.setAutomaticZenRuleState(id,
+                new Condition(zenRule.getConditionId(), "", STATE_TRUE, SOURCE_USER_ACTION),
+                UPDATE_ORIGIN_USER, CUSTOM_PKG_UID);
+
+        // Event 2: App deactivates the rule automatically (it's 8 AM, bedtime schedule ends)
+        mZenModeHelper.setAutomaticZenRuleState(id,
+                new Condition(zenRule.getConditionId(), "", STATE_FALSE, SOURCE_SCHEDULE),
+                UPDATE_ORIGIN_APP, CUSTOM_PKG_UID);
+
+        // Event 3: App activates the rule automatically (it's now 11 PM, bedtime schedule starts)
+        mZenModeHelper.setAutomaticZenRuleState(id,
+                new Condition(zenRule.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE),
+                UPDATE_ORIGIN_APP, CUSTOM_PKG_UID);
+
+        // Event 4: User deactivates the rule manually (they get up before 8 AM on the next day)
+        mZenModeHelper.setAutomaticZenRuleState(id,
+                new Condition(zenRule.getConditionId(), "", STATE_FALSE, SOURCE_USER_ACTION),
+                UPDATE_ORIGIN_USER, CUSTOM_PKG_UID);
+
+        // In total, this represents 4 events
+        assertEquals(4, mZenModeEventLogger.numLoggedChanges());
+
+        // Automatic rule turning on manually:
+        //   - event ID: DND_TURNED_ON
+        //   - 1 rule (newly) active
+        //   - is a user action
+        //   - package UID is the calling package
+        assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
+                mZenModeEventLogger.getEventId(0));
+        assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
+        assertTrue(mZenModeEventLogger.getIsUserAction(0));
+        assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0));
+
+        // Automatic rule turned off automatically by app:
+        //   - event ID: DND_TURNED_OFF
+        //   - 0 rules active
+        //   - is not a user action
+        //   - package UID is the calling package
+        assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
+                mZenModeEventLogger.getEventId(1));
+        assertEquals(0, mZenModeEventLogger.getNumRulesActive(1));
+        assertFalse(mZenModeEventLogger.getIsUserAction(1));
+        assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1));
+
+        // Automatic rule turned on automatically by app:
+        //   - event ID: DND_TURNED_ON
+        //   - 1 rule (newly) active
+        //   - is not a user action
+        //   - package UID is the calling package
+        assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
+                mZenModeEventLogger.getEventId(2));
+        assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(2));
+        assertEquals(1, mZenModeEventLogger.getNumRulesActive(2));
+        assertFalse(mZenModeEventLogger.getIsUserAction(2));
+        assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(2));
+
+        // Automatic rule turned off automatically by the user:
+        //   - event ID: DND_TURNED_ON
+        //   - 0 rules active
+        //   - is a user action
+        //   - package UID is the calling package
+        assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
+                mZenModeEventLogger.getEventId(3));
+        assertEquals(0, mZenModeEventLogger.getNumRulesActive(3));
+        assertTrue(mZenModeEventLogger.getIsUserAction(3));
+        assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(3));
+    }
+
+    @Test
+    public void testZenModeEventLog_policyChanges(@TestParameter ModesApiFlag modesApiFlag)
+            throws IllegalArgumentException {
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
         // First just turn zen mode on
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID);
+                modesApiFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID);
 
         // Now change the policy slightly; want to confirm that this'll be reflected in the logs
         ZenModeConfig newConfig = mZenModeHelper.mConfig.copy();
         newConfig.allowAlarms = true;
         newConfig.allowRepeatCallers = false;
         mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(),
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+                modesApiFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID);
 
         // Turn zen mode off; we want to make sure policy changes do not get logged when zen mode
         // is off.
@@ -2521,7 +2646,7 @@
         newConfig.allowMessages = false;
         newConfig.allowRepeatCallers = true;
         mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(),
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+                modesApiFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID);
 
         // Total events: we only expect ones for turning on, changing policy, and turning off
         assertEquals(3, mZenModeEventLogger.numLoggedChanges());
@@ -2554,7 +2679,9 @@
     }
 
     @Test
-    public void testZenModeEventLog_ruleCounts() throws IllegalArgumentException {
+    public void testZenModeEventLog_ruleCounts(@TestParameter ModesApiFlag modesApiFlag)
+            throws IllegalArgumentException {
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -2657,8 +2784,10 @@
     }
 
     @Test
-    public void testZenModeEventLog_noLogWithNoConfigChange() throws IllegalArgumentException {
+    public void testZenModeEventLog_noLogWithNoConfigChange(
+            @TestParameter ModesApiFlag modesApiFlag) throws IllegalArgumentException {
         // If evaluateZenMode is called independently of a config change, don't log.
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -2675,9 +2804,11 @@
     }
 
     @Test
-    public void testZenModeEventLog_reassignUid() throws IllegalArgumentException {
+    public void testZenModeEventLog_reassignUid(@TestParameter ModesApiFlag modesApiFlag)
+            throws IllegalArgumentException {
         // Test that, only in specific cases, we reassign the calling UID to one associated with
         // the automatic rule owner.
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -2689,7 +2820,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+                UPDATE_ORIGIN_APP, "test", Process.SYSTEM_UID);
 
         // Rule 2, same as rule 1 but owned by the system
         AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
@@ -2699,11 +2830,11 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2,
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+                modesApiFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID);
 
         // Turn on rule 1; call looks like it's from the system. Because setting a condition is
         // typically an automatic (non-user-initiated) action, expect the calling UID to be
-        // re-evaluated to the one associat.d with CUSTOM_PKG_NAME.
+        // re-evaluated to the one associated with CUSTOM_PKG_NAME.
         mZenModeHelper.setAutomaticZenRuleState(id,
                 new Condition(zenRule.getConditionId(), "", STATE_TRUE),
                 UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
@@ -2717,8 +2848,8 @@
         // Disable rule 1. Because this looks like a user action, the UID should not be modified
         // from the system-provided one.
         zenRule.setEnabled(false);
-        mZenModeHelper.updateAutomaticZenRule(id, zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "",
-                Process.SYSTEM_UID);
+        mZenModeHelper.updateAutomaticZenRule(id, zenRule,
+                modesApiFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID);
 
         // Add a manual rule. Any manual rule changes should not get calling uids reassigned.
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, UPDATE_ORIGIN_APP,
@@ -2775,8 +2906,10 @@
     }
 
     @Test
-    public void testZenModeEventLog_channelsBypassingChanges() {
+    public void testZenModeEventLog_channelsBypassingChanges(
+            @TestParameter ModesApiFlag modesApiFlag) {
         // Verify that the right thing happens when the canBypassDnd value changes.
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -2872,14 +3005,11 @@
         // Second message where we change the policy:
         //   - DND_POLICY_CHANGED (indicates only the policy changed and nothing else)
         //   - rule type: unknown (it's a policy change, not a rule change)
-        //   - user action (because it comes from a "system" uid)
         //   - change is in allow channels, and final policy
         assertThat(mZenModeEventLogger.getEventId(1))
                 .isEqualTo(ZenModeEventLogger.ZenStateChangedEvent.DND_POLICY_CHANGED.getId());
         assertThat(mZenModeEventLogger.getChangedRuleType(1))
                 .isEqualTo(DNDProtoEnums.UNKNOWN_RULE);
-        assertThat(mZenModeEventLogger.getIsUserAction(1)).isTrue();
-        assertThat(mZenModeEventLogger.getPackageUid(1)).isEqualTo(Process.SYSTEM_UID);
         DNDPolicyProto dndProto = mZenModeEventLogger.getPolicyProto(1);
         assertThat(dndProto.getAllowChannels().getNumber())
                 .isEqualTo(DNDProtoEnums.CHANNEL_TYPE_NONE);
@@ -3372,6 +3502,29 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void removeAutomaticZenRule_propagatesOriginToEffectsApplier() {
+        mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
+        reset(mDeviceEffectsApplier);
+
+        String ruleId = addRuleWithEffects(new ZenDeviceEffects.Builder()
+                .setShouldSuppressAmbientDisplay(true)
+                .setShouldDimWallpaper(true)
+                .build());
+        mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP,
+                CUSTOM_PKG_UID);
+        mTestableLooper.processAllMessages();
+        verify(mDeviceEffectsApplier).apply(any(), eq(UPDATE_ORIGIN_APP));
+
+        // Now delete the (currently active!) rule. For example, assume this is done from settings.
+        mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_USER, "remove",
+                Process.SYSTEM_UID);
+        mTestableLooper.processAllMessages();
+
+        verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(UPDATE_ORIGIN_USER));
+    }
+
+    @Test
     public void testDeviceEffects_applied() {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
@@ -3511,8 +3664,8 @@
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
                 .setDeviceEffects(effects)
                 .build();
-        return mZenModeHelper.addAutomaticZenRule("pkg", rule, UPDATE_ORIGIN_APP, "",
-                CUSTOM_PKG_UID);
+        return mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "reasons", CUSTOM_PKG_UID);
     }
 
     @Test
@@ -3619,7 +3772,8 @@
         Policy policy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
                 PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
                 Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
-        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy,
+                UPDATE_ORIGIN_APP);
 
         ZenPolicy expectedZenPolicy = new ZenPolicy.Builder()
                 .disallowAllSounds()
@@ -3643,13 +3797,14 @@
                 PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
                 Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
         mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
-                original);
+                original, UPDATE_ORIGIN_APP);
 
         // Change priorityCallSenders: contacts -> starred.
         Policy updated = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
                 PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED,
                 Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
-        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated,
+                UPDATE_ORIGIN_APP);
 
         ZenPolicy expectedZenPolicy = new ZenPolicy.Builder()
                 .disallowAllSounds()
@@ -3671,7 +3826,7 @@
 
         withoutWtfCrash(
                 () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME,
-                        CUSTOM_PKG_UID, new Policy(0, 0, 0)));
+                        CUSTOM_PKG_UID, new Policy(0, 0, 0), UPDATE_ORIGIN_APP));
 
         assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
     }
@@ -3684,7 +3839,7 @@
                 Policy.getAllSuppressedVisualEffects(), STATE_FALSE,
                 CONVERSATION_SENDERS_IMPORTANT);
         mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
-                writtenPolicy);
+                writtenPolicy, UPDATE_ORIGIN_APP);
 
         Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
                 CUSTOM_PKG_NAME);
diff --git a/services/tests/wmtests/src/com/android/server/wm/CompatModePackagesTests.java b/services/tests/wmtests/src/com/android/server/wm/CompatModePackagesTests.java
index 1f7b65e..c187263 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CompatModePackagesTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CompatModePackagesTests.java
@@ -20,10 +20,13 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 
 import android.app.GameManagerInternal;
+import android.content.pm.ApplicationInfo;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.filters.SmallTest;
@@ -90,6 +93,14 @@
     public void testGetCompatScale_noGameManager() {
         assertEquals(mAtm.mCompatModePackages.getCompatScale(TEST_PACKAGE, TEST_USER_ID), 1f,
                 0.01f);
-    }
 
+        final ApplicationInfo info = new ApplicationInfo();
+        // Any non-zero value without FLAG_SUPPORTS_*_SCREENS.
+        info.flags = ApplicationInfo.FLAG_HAS_CODE;
+        info.packageName = info.sourceDir = "legacy.app";
+        mAtm.mCompatModePackages.compatibilityInfoForPackageLocked(info);
+        assertTrue(mAtm.mCompatModePackages.useLegacyScreenCompatMode(info.packageName));
+        mAtm.mCompatModePackages.handlePackageUninstalledLocked(info.packageName);
+        assertFalse(mAtm.mCompatModePackages.useLegacyScreenCompatMode(info.packageName));
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceControlViewHostTests.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceControlViewHostTests.java
index ef427bb..a8b2178 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceControlViewHostTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceControlViewHostTests.java
@@ -16,7 +16,7 @@
 
 package com.android.server.wm;
 
-import static android.server.wm.CtsWindowInfoUtils.dumpWindowsOnScreen;
+import static android.server.wm.CtsWindowInfoUtils.assertAndDumpWindowState;
 import static android.server.wm.CtsWindowInfoUtils.waitForWindowFocus;
 import static android.server.wm.CtsWindowInfoUtils.waitForWindowVisible;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
@@ -129,37 +129,22 @@
             mScvh2.setView(mView2, lp2);
         });
 
-        boolean wasVisible = waitForWindowVisible(mView1);
-        if (!wasVisible) {
-            dumpWindowsOnScreen(TAG, "requestFocusWithMultipleWindows");
-        }
-        assertTrue("Failed to wait for view1", wasVisible);
-
-        wasVisible = waitForWindowVisible(mView2);
-        if (!wasVisible) {
-            dumpWindowsOnScreen(TAG, "requestFocusWithMultipleWindows-not visible");
-        }
-        assertTrue("Failed to wait for view2", wasVisible);
+        assertAndDumpWindowState(TAG, "Failed to wait for view1", waitForWindowVisible(mView1));
+        assertAndDumpWindowState(TAG, "Failed to wait for view2", waitForWindowVisible(mView2));
 
         IWindow window = IWindow.Stub.asInterface(mSurfaceView.getWindowToken());
 
         WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(window,
                 mScvh1.getInputTransferToken(), true);
 
-        boolean gainedFocus = waitForWindowFocus(mView1, true);
-        if (!gainedFocus) {
-            dumpWindowsOnScreen(TAG, "requestFocusWithMultipleWindows-view1 not focus");
-        }
-        assertTrue("Failed to gain focus for view1", gainedFocus);
+        assertAndDumpWindowState(TAG, "Failed to wait for view1 focus",
+                waitForWindowFocus(mView1, true));
 
         WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(window,
                 mScvh2.getInputTransferToken(), true);
 
-        gainedFocus = waitForWindowFocus(mView2, true);
-        if (!gainedFocus) {
-            dumpWindowsOnScreen(TAG, "requestFocusWithMultipleWindows-view2 not focus");
-        }
-        assertTrue("Failed to gain focus for view2", gainedFocus);
+        assertAndDumpWindowState(TAG, "Failed to wait for view2 focus",
+                waitForWindowFocus(mView2, true));
     }
 
     private static class TestWindowlessWindowManager extends WindowlessWindowManager {
diff --git a/services/tests/wmtests/src/com/android/server/wm/TrustedOverlayTests.java b/services/tests/wmtests/src/com/android/server/wm/TrustedOverlayTests.java
index ac49839..6a15b05 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TrustedOverlayTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TrustedOverlayTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.server.wm.CtsWindowInfoUtils.assertAndDumpWindowState;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
 
@@ -138,11 +139,8 @@
                     return false;
                 }, TIMEOUT_S, TimeUnit.SECONDS);
 
-        if (!foundTrusted[0]) {
-            CtsWindowInfoUtils.dumpWindowsOnScreen(TAG, mName.getMethodName());
-        }
-
-        assertTrue("Failed to find window or was not marked trusted", foundTrusted[0]);
+        assertAndDumpWindowState(TAG, "Failed to find window or was not marked trusted",
+                foundTrusted[0]);
     }
 
     private void testTrustedOverlayChildHelper(boolean expectedTrustedChild)
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index df4af11..616a23e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -294,7 +294,7 @@
      */
     static void suppressInsetsAnimation(InsetsControlTarget target) {
         spyOn(target);
-        Mockito.doNothing().when(target).notifyInsetsControlChanged();
+        Mockito.doNothing().when(target).notifyInsetsControlChanged(anyInt());
     }
 
     @After
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
index 4720d27..f9fa9b7 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
@@ -106,96 +106,104 @@
             @Override
             public void onAttentionGained() {
                 Slog.v(TAG, "BinderCallback#onAttentionGained");
-                mEgressingData = true;
-                if (mAttentionListener == null) {
-                    return;
-                }
-                try {
-                    mAttentionListener.onAttentionGained();
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Error delivering attention gained event.", e);
-                    try {
-                        callback.onVisualQueryDetectionServiceFailure(
-                                new VisualQueryDetectionServiceFailure(
-                                        ERROR_CODE_ILLEGAL_ATTENTION_STATE,
-                                        "Attention listener failed to switch to GAINED state."));
-                    } catch (RemoteException ex) {
-                        Slog.v(TAG, "Fail to call onVisualQueryDetectionServiceFailure");
+                synchronized (mLock) {
+                    mEgressingData = true;
+                    if (mAttentionListener == null) {
+                        return;
                     }
-                    return;
+                    try {
+                        mAttentionListener.onAttentionGained();
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Error delivering attention gained event.", e);
+                        try {
+                            callback.onVisualQueryDetectionServiceFailure(
+                                    new VisualQueryDetectionServiceFailure(
+                                            ERROR_CODE_ILLEGAL_ATTENTION_STATE,
+                                            "Attention listener fails to switch to GAINED state."));
+                        } catch (RemoteException ex) {
+                            Slog.v(TAG, "Fail to call onVisualQueryDetectionServiceFailure");
+                        }
+                    }
                 }
             }
 
             @Override
             public void onAttentionLost() {
                 Slog.v(TAG, "BinderCallback#onAttentionLost");
-                mEgressingData = false;
-                if (mAttentionListener == null) {
-                    return;
-                }
-                try {
-                    mAttentionListener.onAttentionLost();
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Error delivering attention lost event.", e);
-                    try {
-                        callback.onVisualQueryDetectionServiceFailure(
-                                new VisualQueryDetectionServiceFailure(
-                                        ERROR_CODE_ILLEGAL_ATTENTION_STATE,
-                                        "Attention listener failed to switch to LOST state."));
-                    } catch (RemoteException ex) {
-                        Slog.v(TAG, "Fail to call onVisualQueryDetectionServiceFailure");
+                synchronized (mLock) {
+                    mEgressingData = false;
+                    if (mAttentionListener == null) {
+                        return;
                     }
-                    return;
+                    try {
+                        mAttentionListener.onAttentionLost();
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Error delivering attention lost event.", e);
+                        try {
+                            callback.onVisualQueryDetectionServiceFailure(
+                                    new VisualQueryDetectionServiceFailure(
+                                            ERROR_CODE_ILLEGAL_ATTENTION_STATE,
+                                            "Attention listener fails to switch to LOST state."));
+                        } catch (RemoteException ex) {
+                            Slog.v(TAG, "Fail to call onVisualQueryDetectionServiceFailure");
+                        }
+                    }
                 }
             }
 
             @Override
             public void onQueryDetected(@NonNull String partialQuery) throws RemoteException {
-                Objects.requireNonNull(partialQuery);
                 Slog.v(TAG, "BinderCallback#onQueryDetected");
-                if (!mEgressingData) {
-                    Slog.v(TAG, "Query should not be egressed within the unattention state.");
-                    callback.onVisualQueryDetectionServiceFailure(
-                            new VisualQueryDetectionServiceFailure(
-                                    ERROR_CODE_ILLEGAL_STREAMING_STATE,
-                                    "Cannot stream queries without attention signals."));
-                    return;
+                synchronized (mLock) {
+                    Objects.requireNonNull(partialQuery);
+                    if (!mEgressingData) {
+                        Slog.v(TAG, "Query should not be egressed within the unattention state.");
+                        callback.onVisualQueryDetectionServiceFailure(
+                                new VisualQueryDetectionServiceFailure(
+                                        ERROR_CODE_ILLEGAL_STREAMING_STATE,
+                                        "Cannot stream queries without attention signals."));
+                        return;
+                    }
+                    mQueryStreaming = true;
+                    callback.onQueryDetected(partialQuery);
+                    Slog.i(TAG, "Egressed from visual query detection process.");
                 }
-                mQueryStreaming = true;
-                callback.onQueryDetected(partialQuery);
-                Slog.i(TAG, "Egressed from visual query detection process.");
             }
 
             @Override
             public void onQueryFinished() throws RemoteException {
                 Slog.v(TAG, "BinderCallback#onQueryFinished");
-                if (!mQueryStreaming) {
-                    Slog.v(TAG, "Query streaming state signal FINISHED is block since there is"
-                            + " no active query being streamed.");
-                    callback.onVisualQueryDetectionServiceFailure(
-                            new VisualQueryDetectionServiceFailure(
-                                    ERROR_CODE_ILLEGAL_STREAMING_STATE,
-                                    "Cannot send FINISHED signal with no query streamed."));
-                    return;
+                synchronized (mLock) {
+                    if (!mQueryStreaming) {
+                        Slog.v(TAG, "Query streaming state signal FINISHED is block since there is"
+                                + " no active query being streamed.");
+                        callback.onVisualQueryDetectionServiceFailure(
+                                new VisualQueryDetectionServiceFailure(
+                                        ERROR_CODE_ILLEGAL_STREAMING_STATE,
+                                        "Cannot send FINISHED signal with no query streamed."));
+                        return;
+                    }
+                    callback.onQueryFinished();
+                    mQueryStreaming = false;
                 }
-                callback.onQueryFinished();
-                mQueryStreaming = false;
             }
 
             @Override
             public void onQueryRejected() throws RemoteException {
                 Slog.v(TAG, "BinderCallback#onQueryRejected");
-                if (!mQueryStreaming) {
-                    Slog.v(TAG, "Query streaming state signal REJECTED is block since there is"
-                            + " no active query being streamed.");
-                    callback.onVisualQueryDetectionServiceFailure(
-                            new VisualQueryDetectionServiceFailure(
-                                    ERROR_CODE_ILLEGAL_STREAMING_STATE,
-                                    "Cannot send REJECTED signal with no query streamed."));
-                    return;
+                synchronized (mLock) {
+                    if (!mQueryStreaming) {
+                        Slog.v(TAG, "Query streaming state signal REJECTED is block since there is"
+                                + " no active query being streamed.");
+                        callback.onVisualQueryDetectionServiceFailure(
+                                new VisualQueryDetectionServiceFailure(
+                                        ERROR_CODE_ILLEGAL_STREAMING_STATE,
+                                        "Cannot send REJECTED signal with no query streamed."));
+                        return;
+                    }
+                    callback.onQueryRejected();
+                    mQueryStreaming = false;
                 }
-                callback.onQueryRejected();
-                mQueryStreaming = false;
             }
         };
         return mRemoteDetectionService.run(
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
index def52a5..874c10c 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -210,100 +210,6 @@
             "android.telecom.extra.SILENT_RINGING_REQUESTED";
 
     /**
-     * Call event sent from a {@link Call} via {@link #sendCallEvent(String, Bundle)} to inform
-     * Telecom that the user has requested that the current {@link Call} should be handed over
-     * to another {@link ConnectionService}.
-     * <p>
-     * The caller must specify the {@link #EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE} to indicate to
-     * Telecom which {@link PhoneAccountHandle} the {@link Call} should be handed over to.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EVENT_REQUEST_HANDOVER =
-            "android.telecom.event.REQUEST_HANDOVER";
-
-    /**
-     * Extra key used with the {@link #EVENT_REQUEST_HANDOVER} call event.  Specifies the
-     * {@link PhoneAccountHandle} to which a call should be handed over to.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE =
-            "android.telecom.extra.HANDOVER_PHONE_ACCOUNT_HANDLE";
-
-    /**
-     * Integer extra key used with the {@link #EVENT_REQUEST_HANDOVER} call event.  Specifies the
-     * video state of the call when it is handed over to the new {@link PhoneAccount}.
-     * <p>
-     * Valid values: {@link VideoProfile#STATE_AUDIO_ONLY},
-     * {@link VideoProfile#STATE_BIDIRECTIONAL}, {@link VideoProfile#STATE_RX_ENABLED}, and
-     * {@link VideoProfile#STATE_TX_ENABLED}.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EXTRA_HANDOVER_VIDEO_STATE =
-            "android.telecom.extra.HANDOVER_VIDEO_STATE";
-
-    /**
-     * Extra key used with the {@link #EVENT_REQUEST_HANDOVER} call event.  Used by the
-     * {@link InCallService} initiating a handover to provide a {@link Bundle} with extra
-     * information to the handover {@link ConnectionService} specified by
-     * {@link #EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE}.
-     * <p>
-     * This {@link Bundle} is not interpreted by Telecom, but passed as-is to the
-     * {@link ConnectionService} via the request extras when
-     * {@link ConnectionService#onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)}
-     * is called to initate the handover.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EXTRA_HANDOVER_EXTRAS = "android.telecom.extra.HANDOVER_EXTRAS";
-
-    /**
-     * Call event sent from Telecom to the handover {@link ConnectionService} via
-     * {@link Connection#onCallEvent(String, Bundle)} to inform a {@link Connection} that a handover
-     * to the {@link ConnectionService} has completed successfully.
-     * <p>
-     * A handover is initiated with the {@link #EVENT_REQUEST_HANDOVER} call event.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EVENT_HANDOVER_COMPLETE =
-            "android.telecom.event.HANDOVER_COMPLETE";
-
-    /**
-     * Call event sent from Telecom to the handover destination {@link ConnectionService} via
-     * {@link Connection#onCallEvent(String, Bundle)} to inform the handover destination that the
-     * source connection has disconnected.  The {@link Bundle} parameter for the call event will be
-     * {@code null}.
-     * <p>
-     * A handover is initiated with the {@link #EVENT_REQUEST_HANDOVER} call event.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EVENT_HANDOVER_SOURCE_DISCONNECTED =
-            "android.telecom.event.HANDOVER_SOURCE_DISCONNECTED";
-
-    /**
-     * Call event sent from Telecom to the handover {@link ConnectionService} via
-     * {@link Connection#onCallEvent(String, Bundle)} to inform a {@link Connection} that a handover
-     * to the {@link ConnectionService} has failed.
-     * <p>
-     * A handover is initiated with the {@link #EVENT_REQUEST_HANDOVER} call event.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EVENT_HANDOVER_FAILED =
-            "android.telecom.event.HANDOVER_FAILED";
-
-    /**
      * Event reported from the Telecom stack to report an in-call diagnostic message which the
      * dialer app may opt to display to the user.  A diagnostic message is used to communicate
      * scenarios the device has detected which may impact the quality of the ongoing call.
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 4a541da..ee9bf898 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -961,28 +961,6 @@
             "android.telecom.event.CALL_REMOTELY_UNHELD";
 
     /**
-     * Connection event used to inform an {@link InCallService} which initiated a call handover via
-     * {@link Call#EVENT_REQUEST_HANDOVER} that the handover from this {@link Connection} has
-     * successfully completed.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EVENT_HANDOVER_COMPLETE =
-            "android.telecom.event.HANDOVER_COMPLETE";
-
-    /**
-     * Connection event used to inform an {@link InCallService} which initiated a call handover via
-     * {@link Call#EVENT_REQUEST_HANDOVER} that the handover from this {@link Connection} has failed
-     * to complete.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EVENT_HANDOVER_FAILED =
-            "android.telecom.event.HANDOVER_FAILED";
-
-    /**
      * String Connection extra key used to store SIP invite fields for an incoming call for IMS call
      */
     public static final String EXTRA_SIP_INVITE = "android.telecom.extra.SIP_INVITE";
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index bcd9929..101d285 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -525,6 +525,12 @@
     public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool";
 
     /**
+     * Used in the Preferred Network Types menu to determine if the 3G option is displayed.
+     */
+    @FlaggedApi(Flags.FLAG_HIDE_PREFER_3G_ITEM)
+    public static final String KEY_PREFER_3G_VISIBILITY_BOOL = "prefer_3g_visibility_bool";
+
+    /**
      * Used in Cellular Network Settings for preferred network type to show 4G only mode.
      * @hide
      */
@@ -10144,6 +10150,7 @@
         sDefaults.putBoolean(KEY_MDN_IS_ADDITIONAL_VOICEMAIL_NUMBER_BOOL, false);
         sDefaults.putBoolean(KEY_OPERATOR_SELECTION_EXPAND_BOOL, true);
         sDefaults.putBoolean(KEY_PREFER_2G_BOOL, false);
+        sDefaults.putBoolean(KEY_PREFER_3G_VISIBILITY_BOOL, true);
         sDefaults.putBoolean(KEY_4G_ONLY_BOOL, false);
         sDefaults.putBoolean(KEY_SHOW_APN_SETTING_CDMA_BOOL, false);
         sDefaults.putBoolean(KEY_SHOW_CDMA_CHOICES_BOOL, false);
diff --git a/tests/ActivityManagerPerfTests/tests/Android.bp b/tests/ActivityManagerPerfTests/tests/Android.bp
index e5813ae..cce40f3a 100644
--- a/tests/ActivityManagerPerfTests/tests/Android.bp
+++ b/tests/ActivityManagerPerfTests/tests/Android.bp
@@ -30,6 +30,12 @@
         "ActivityManagerPerfTestsUtils",
         "collector-device-lib-platform",
     ],
+    data: [
+        ":ActivityManagerPerfTestsTestApp",
+        ":ActivityManagerPerfTestsStubApp1",
+        ":ActivityManagerPerfTestsStubApp2",
+        ":ActivityManagerPerfTestsStubApp3",
+    ],
     platform_apis: true,
     min_sdk_version: "25",
     // For android.permission.FORCE_STOP_PACKAGES permission
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
index c49f8fe..be47ec7 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
@@ -19,12 +19,12 @@
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
+import android.tools.common.flicker.subject.layers.LayersTraceSubject.Companion.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.traces.parsers.toFlickerComponent
-import androidx.test.filters.FlakyTest
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
@@ -58,9 +58,10 @@
         }
         transitions {
             broadcastActionTrigger.doAction(ACTION_FINISH_ACTIVITY)
-            wmHelper.StateSyncBuilder()
-                    .withActivityRemoved(ActivityOptions.Ime.Default.COMPONENT.toFlickerComponent())
-                    .waitForAndVerify()
+            wmHelper
+                .StateSyncBuilder()
+                .withActivityRemoved(ActivityOptions.Ime.Default.COMPONENT.toFlickerComponent())
+                .waitForAndVerify()
         }
         teardown { simpleApp.exit(wmHelper) }
     }
@@ -69,10 +70,16 @@
 
     @Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible()
 
-    @FlakyTest(bugId = 246284124)
+    @Presubmit
     @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+        flicker.assertLayers {
+            this.visibleLayersShownMoreThanOneConsecutiveEntry(
+                VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS.toMutableList().also {
+                    it.add(simpleApp.componentMatcher)
+                }
+            )
+        }
     }
 
     @Presubmit
diff --git a/tests/testables/src/android/testing/TestWithLooperRule.java b/tests/testables/src/android/testing/TestWithLooperRule.java
index 99b303e..37b39c3 100644
--- a/tests/testables/src/android/testing/TestWithLooperRule.java
+++ b/tests/testables/src/android/testing/TestWithLooperRule.java
@@ -19,7 +19,6 @@
 import android.testing.TestableLooper.LooperFrameworkMethod;
 import android.testing.TestableLooper.RunWithLooper;
 
-import org.junit.internal.runners.statements.InvokeMethod;
 import org.junit.rules.MethodRule;
 import org.junit.runner.RunWith;
 import org.junit.runners.model.FrameworkMethod;
@@ -79,13 +78,11 @@
             while (next != null) {
                 switch (next.getClass().getSimpleName()) {
                     case "RunAfters":
-                        this.<List<FrameworkMethod>>wrapFieldMethodFor(next,
-                                next.getClass(), "afters", method, target);
+                        this.wrapFieldMethodFor(next, "afters", method, target);
                         next = getNextStatement(next, "next");
                         break;
                     case "RunBefores":
-                        this.<List<FrameworkMethod>>wrapFieldMethodFor(next,
-                                next.getClass(), "befores", method, target);
+                        this.wrapFieldMethodFor(next, "befores", method, target);
                         next = getNextStatement(next, "next");
                         break;
                     case "FailOnTimeout":
@@ -95,8 +92,10 @@
                         next = getNextStatement(next, "originalStatement");
                         break;
                     case "InvokeMethod":
-                        this.<FrameworkMethod>wrapFieldMethodFor(next,
-                                InvokeMethod.class, "testMethod", method, target);
+                        this.wrapFieldMethodFor(next, "testMethod", method, target);
+                        return;
+                    case "InvokeParameterizedMethod":
+                        this.wrapFieldMethodFor(next, "frameworkMethod", method, target);
                         return;
                     default:
                         throw new Exception(
@@ -112,12 +111,11 @@
 
     // Wrapping the befores, afters, and InvokeMethods with LooperFrameworkMethod
     // within the statement.
-    private <T> void wrapFieldMethodFor(Statement base, Class<?> targetClass, String fieldStr,
-            FrameworkMethod method, Object target)
-            throws NoSuchFieldException, IllegalAccessException {
-        Field field = targetClass.getDeclaredField(fieldStr);
+    private void wrapFieldMethodFor(Statement base, String fieldStr, FrameworkMethod method,
+            Object target) throws NoSuchFieldException, IllegalAccessException {
+        Field field = base.getClass().getDeclaredField(fieldStr);
         field.setAccessible(true);
-        T fieldInstance = (T) field.get(base);
+        Object fieldInstance = field.get(base);
         if (fieldInstance instanceof FrameworkMethod) {
             field.set(base, looperWrap(method, target, (FrameworkMethod) fieldInstance));
         } else {
diff --git a/tools/hoststubgen/README.md b/tools/hoststubgen/README.md
index 3455b0a..1a895dc 100644
--- a/tools/hoststubgen/README.md
+++ b/tools/hoststubgen/README.md
@@ -34,11 +34,6 @@
 
   - `test-tiny-framework/` See `README.md` in it.
 
-  - `test-framework`
-    This directory was used during the prototype phase, but now that we have real ravenwood tests,
-    this directory is obsolete and should be deleted.
-
-
 - `scripts`
   - `dump-jar.sh`
 
diff --git a/tools/hoststubgen/hoststubgen/Android.bp b/tools/hoststubgen/hoststubgen/Android.bp
index 4eac361..57bcc04 100644
--- a/tools/hoststubgen/hoststubgen/Android.bp
+++ b/tools/hoststubgen/hoststubgen/Android.bp
@@ -9,7 +9,7 @@
 
 // Visibility only for ravenwood prototype uses.
 genrule_defaults {
-    name: "hoststubgen-for-prototype-only-genrule",
+    name: "ravenwood-internal-only-visibility-genrule",
     visibility: [
         ":__subpackages__",
         "//frameworks/base",
@@ -19,7 +19,7 @@
 
 // Visibility only for ravenwood prototype uses.
 java_defaults {
-    name: "hoststubgen-for-prototype-only-java",
+    name: "ravenwood-internal-only-visibility-java",
     visibility: [
         ":__subpackages__",
         "//frameworks/base",
@@ -29,7 +29,7 @@
 
 // Visibility only for ravenwood prototype uses.
 filegroup_defaults {
-    name: "hoststubgen-for-prototype-only-filegroup",
+    name: "ravenwood-internal-only-visibility-filegroup",
     visibility: [
         ":__subpackages__",
         "//frameworks/base",
@@ -41,7 +41,7 @@
 // This is only for the prototype. The productionized version is "ravenwood-annotations".
 java_library {
     name: "hoststubgen-annotations",
-    defaults: ["hoststubgen-for-prototype-only-java"],
+    defaults: ["ravenwood-internal-only-visibility-java"],
     srcs: [
         "annotations-src/**/*.java",
     ],
@@ -115,7 +115,7 @@
 // This is only for the prototype. The productionized version is "ravenwood-standard-options".
 filegroup {
     name: "hoststubgen-standard-options",
-    defaults: ["hoststubgen-for-prototype-only-filegroup"],
+    defaults: ["ravenwood-internal-only-visibility-filegroup"],
     srcs: [
         "hoststubgen-standard-options.txt",
     ],
@@ -153,149 +153,25 @@
     ],
 }
 
-// Generate the stub/impl from framework-all, with hidden APIs.
-java_genrule_host {
-    name: "framework-all-hidden-api-host",
-    defaults: ["hoststubgen-command-defaults"],
-    cmd: hoststubgen_common_options +
-        "--in-jar $(location :framework-all) " +
-        "--policy-override-file $(location framework-policy-override.txt) ",
-    srcs: [
-        ":framework-all",
-        "framework-policy-override.txt",
-    ],
-    visibility: ["//visibility:private"],
-}
-
-// Extract the stub jar from "framework-all-host" for subsequent build rules.
-// This is only for the prototype. Do not use it in "productionized" build rules.
-java_genrule_host {
-    name: "framework-all-hidden-api-host-stub",
-    defaults: ["hoststubgen-for-prototype-only-genrule"],
-    cmd: "cp $(in) $(out)",
-    srcs: [
-        ":framework-all-hidden-api-host{host_stub.jar}",
-    ],
-    out: [
-        "host_stub.jar",
-    ],
-}
-
-// Extract the impl jar from "framework-all-host" for subsequent build rules.
-// This is only for the prototype. Do not use it in "productionized" build rules.
-java_genrule_host {
-    name: "framework-all-hidden-api-host-impl",
-    defaults: ["hoststubgen-for-prototype-only-genrule"],
-    cmd: "cp $(in) $(out)",
-    srcs: [
-        ":framework-all-hidden-api-host{host_impl.jar}",
-    ],
-    out: [
-        "host_impl.jar",
-    ],
-}
-
-// Generate the stub/impl from framework-all, with only public/system/test APIs, without
-// hidden APIs.
-// This is only for the prototype. Do not use it in "productionized" build rules.
-java_genrule_host {
-    name: "framework-all-test-api-host",
-    defaults: ["hoststubgen-command-defaults"],
-    cmd: hoststubgen_common_options +
-        "--intersect-stub-jar $(location :android_test_stubs_current{.jar}) " +
-        "--in-jar $(location :framework-all) " +
-        "--policy-override-file $(location framework-policy-override.txt) ",
-    srcs: [
-        ":framework-all",
-        ":android_test_stubs_current{.jar}",
-        "framework-policy-override.txt",
-    ],
-    visibility: ["//visibility:private"],
-}
-
-// Extract the stub jar from "framework-all-test-api-host" for subsequent build rules.
-// This is only for the prototype. Do not use it in "productionized" build rules.
-java_genrule_host {
-    name: "framework-all-test-api-host-stub",
-    defaults: ["hoststubgen-for-prototype-only-genrule"],
-    cmd: "cp $(in) $(out)",
-    srcs: [
-        ":framework-all-test-api-host{host_stub.jar}",
-    ],
-    out: [
-        "host_stub.jar",
-    ],
-}
-
-// Extract the impl jar from "framework-all-test-api-host" for subsequent build rules.
-// This is only for the prototype. Do not use it in "productionized" build rules.
-java_genrule_host {
-    name: "framework-all-test-api-host-impl",
-    defaults: ["hoststubgen-for-prototype-only-genrule"],
-    cmd: "cp $(in) $(out)",
-    srcs: [
-        ":framework-all-test-api-host{host_impl.jar}",
-    ],
-    out: [
-        "host_impl.jar",
-    ],
-}
-
-// This library contains helper classes to build hostside tests/targets.
-// This essentially contains dependencies from tests that we can't actually use the real ones.
-// For example, the actual AndroidTestCase and AndroidJUnit4 don't run on the host side (yet),
-// so we pup "fake" implementations here.
-// Ideally this library should be empty.
-java_library_host {
-    name: "hoststubgen-helper-framework-buildtime",
-    defaults: ["hoststubgen-for-prototype-only-java"],
-    srcs: [
-        "helper-framework-buildtime-src/**/*.java",
-    ],
-    libs: [
-        // We need it to pull in some of the framework classes used in this library,
-        // such as Context.java.
-        "framework-all-hidden-api-host-impl",
-        "junit",
-    ],
-}
-
-// This module contains "fake" libcore/dalvik classes, framework native substitution, etc,
-// that are needed at runtime.
-java_library_host {
-    name: "hoststubgen-helper-framework-runtime",
-    defaults: ["hoststubgen-for-prototype-only-java"],
-    srcs: [
-        "helper-framework-runtime-src/**/*.java",
-    ],
-    exclude_srcs: [
-        "helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java",
-    ],
-    libs: [
-        "hoststubgen-helper-runtime",
-        "framework-all-hidden-api-host-impl",
-    ],
-}
-
 java_library_host {
     name: "hoststubgen-helper-libcore-runtime",
-    defaults: ["hoststubgen-for-prototype-only-java"],
     srcs: [
         "helper-framework-runtime-src/libcore-fake/**/*.java",
     ],
+    visibility: ["//visibility:private"],
 }
 
 java_host_for_device {
     name: "hoststubgen-helper-libcore-runtime.ravenwood",
-    defaults: ["hoststubgen-for-prototype-only-java"],
     libs: [
         "hoststubgen-helper-libcore-runtime",
     ],
+    visibility: ["//visibility:private"],
 }
 
 java_library {
     name: "hoststubgen-helper-framework-runtime.ravenwood",
-    defaults: ["hoststubgen-for-prototype-only-java"],
+    defaults: ["ravenwood-internal-only-visibility-java"],
     srcs: [
         "helper-framework-runtime-src/framework/**/*.java",
     ],
@@ -308,88 +184,3 @@
         "hoststubgen-helper-libcore-runtime.ravenwood",
     ],
 }
-
-// Defaults for host side test modules.
-// We need two rules for each test.
-// 1. A "-test-lib" jar, which compiles the test against the stub jar.
-//    This one is only used by the second rule, so it should be "private.
-// 2. A "-test" jar, which includes 1 + the runtime (impl) jars.
-
-// This and next ones are for tests using framework-app, with hidden APIs.
-java_defaults {
-    name: "hosttest-with-framework-all-hidden-api-test-lib-defaults",
-    installable: false,
-    libs: [
-        "framework-all-hidden-api-host-stub",
-    ],
-    static_libs: [
-        "hoststubgen-helper-framework-buildtime",
-        "framework-annotations-lib",
-    ],
-    visibility: ["//visibility:private"],
-}
-
-// Default rules to include `libandroid_runtime`. For now, it's empty, but we'll use it
-// once we start using JNI.
-java_defaults {
-    name: "hosttest-with-libandroid_runtime",
-    jni_libs: [
-        // "libandroid_runtime",
-
-        // TODO: Figure out how to build them automatically.
-        // Following ones are depended by libandroid_runtime.
-        // Without listing them here, not only we won't get them under
-        // $ANDROID_HOST_OUT/testcases/*/lib64, but also not under
-        // $ANDROID_HOST_OUT/lib64, so we'd fail to load them at runtime.
-        // ($ANDROID_HOST_OUT/lib/ gets all of them though.)
-        // "libcutils",
-        // "libharfbuzz_ng",
-        // "libminikin",
-        // "libz",
-        // "libbinder",
-        // "libhidlbase",
-        // "libvintf",
-        // "libicu",
-        // "libutils",
-        // "libtinyxml2",
-    ],
-}
-
-java_defaults {
-    name: "hosttest-with-framework-all-hidden-api-test-defaults",
-    defaults: ["hosttest-with-libandroid_runtime"],
-    installable: false,
-    test_config: "AndroidTest-host.xml",
-    static_libs: [
-        "hoststubgen-helper-runtime",
-        "hoststubgen-helper-framework-runtime",
-        "framework-all-hidden-api-host-impl",
-    ],
-}
-
-// This and next ones are for tests using framework-app, with public/system/test APIs,
-// without hidden APIs.
-java_defaults {
-    name: "hosttest-with-framework-all-test-api-test-lib-defaults",
-    installable: false,
-    libs: [
-        "framework-all-test-api-host-stub",
-    ],
-    static_libs: [
-        "hoststubgen-helper-framework-buildtime",
-        "framework-annotations-lib",
-    ],
-    visibility: ["//visibility:private"],
-}
-
-java_defaults {
-    name: "hosttest-with-framework-all-test-api-test-defaults",
-    defaults: ["hosttest-with-libandroid_runtime"],
-    installable: false,
-    test_config: "AndroidTest-host.xml",
-    static_libs: [
-        "hoststubgen-helper-runtime",
-        "hoststubgen-helper-framework-runtime",
-        "framework-all-test-api-host-impl",
-    ],
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/android/test/AndroidTestCase.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/android/test/AndroidTestCase.java
deleted file mode 100644
index e6d3866..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/android/test/AndroidTestCase.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.test;
-
-import android.content.Context;
-
-import junit.framework.TestCase;
-
-public class AndroidTestCase extends TestCase {
-    protected Context mContext;
-    public Context getContext() {
-        throw new RuntimeException("[ravenwood] Class Context is not supported yet.");
-    }
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/NonNull.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/NonNull.java
deleted file mode 100644
index 51c5d9a..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/NonNull.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.annotation;
-
-// [ravenwood] TODO: Find the actual androidx jar containing it.s
-
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-/**
- * Denotes that a parameter, field or method return value can never be null.
- * <p>
- * This is a marker annotation and it has no specific attributes.
- *
- * @paramDoc This value cannot be {@code null}.
- * @returnDoc This value cannot be {@code null}.
- * @hide
- */
-@Retention(SOURCE)
-@Target({METHOD, PARAMETER, FIELD})
-public @interface NonNull {
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/Nullable.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/Nullable.java
deleted file mode 100644
index f1f0e8b..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/Nullable.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.annotation;
-
-// [ravenwood] TODO: Find the actual androidx jar containing it.s
-
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-/**
- * Denotes that a parameter, field or method return value can be null.
- * <p>
- * When decorating a method call parameter, this denotes that the parameter can
- * legitimately be null and the method will gracefully deal with it. Typically
- * used on optional parameters.
- * <p>
- * When decorating a method, this denotes the method might legitimately return
- * null.
- * <p>
- * This is a marker annotation and it has no specific attributes.
- *
- * @paramDoc This value may be {@code null}.
- * @returnDoc This value may be {@code null}.
- * @hide
- */
-@Retention(SOURCE)
-@Target({METHOD, PARAMETER, FIELD})
-public @interface Nullable {
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/ext/junit/runners/AndroidJUnit4.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/ext/junit/runners/AndroidJUnit4.java
deleted file mode 100644
index 0c82e4e..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/ext/junit/runners/AndroidJUnit4.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.test.ext.junit.runners;
-
-import org.junit.runners.BlockJUnit4ClassRunner;
-import org.junit.runners.model.InitializationError;
-
-// TODO: We need to simulate the androidx test runner.
-// https://source.corp.google.com/piper///depot/google3/third_party/android/androidx_test/ext/junit/java/androidx/test/ext/junit/runners/AndroidJUnit4.java
-// https://source.corp.google.com/piper///depot/google3/third_party/android/androidx_test/runner/android_junit_runner/java/androidx/test/internal/runner/junit4/AndroidJUnit4ClassRunner.java
-
-public class AndroidJUnit4 extends BlockJUnit4ClassRunner {
-    public AndroidJUnit4(Class<?> testClass) throws InitializationError {
-        super(testClass);
-    }
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/FlakyTest.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/FlakyTest.java
deleted file mode 100644
index 2470d839..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/FlakyTest.java
+++ /dev/null
@@ -1,46 +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 androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Designates a test as being flaky (non-deterministic).
- *
- * <p>Can then be used to filter tests on execution using -e annotation or -e notAnnotation as
- * desired.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
-public @interface FlakyTest {
-  /**
-   * An optional bug number associated with the test. -1 Means that no bug number is associated with
-   * the flaky annotation.
-   *
-   * @return int
-   */
-  int bugId() default -1;
-
-  /**
-   * Details, such as the reason of why the test is flaky.
-   *
-   * @return String
-   */
-  String detail() default "";
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/LargeTest.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/LargeTest.java
deleted file mode 100644
index 578d7dc..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/LargeTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Annotation to assign a large test size qualifier to a test. This annotation can be used at a
- * method or class level.
- *
- * <p>Test size qualifiers are a great way to structure test code and are used to assign a test to a
- * test suite of similar run time.
- *
- * <p>Execution time: &gt;1000ms
- *
- * <p>Large tests should be focused on testing integration of all application components. These
- * tests fully participate in the system and may make use of all resources such as databases, file
- * systems and network. As a rule of thumb most functional UI tests are large tests.
- *
- * <p>Note: This class replaces the deprecated Android platform size qualifier <a
- * href="{@docRoot}reference/android/test/suitebuilder/annotation/LargeTest.html"><code>
- * android.test.suitebuilder.annotation.LargeTest</code></a> and is the recommended way to annotate
- * tests written with the AndroidX Test Library.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
-public @interface LargeTest {}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/MediumTest.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/MediumTest.java
deleted file mode 100644
index dfdaa53..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/MediumTest.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Annotation to assign a medium test size qualifier to a test. This annotation can be used at a
- * method or class level.
- *
- * <p>Test size qualifiers are a great way to structure test code and are used to assign a test to a
- * test suite of similar run time.
- *
- * <p>Execution time: &lt;1000ms
- *
- * <p>Medium tests should be focused on a very limited subset of components or a single component.
- * Resource access to the file system through well defined interfaces like databases,
- * ContentProviders, or Context is permitted. Network access should be restricted, (long-running)
- * blocking operations should be avoided and use mock objects instead.
- *
- * <p>Note: This class replaces the deprecated Android platform size qualifier <a
- * href="{@docRoot}reference/android/test/suitebuilder/annotation/MediumTest.html"><code>
- * android.test.suitebuilder.annotation.MediumTest</code></a> and is the recommended way to annotate
- * tests written with the AndroidX Test Library.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
-public @interface MediumTest {}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/RequiresDevice.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/RequiresDevice.java
deleted file mode 100644
index 3d3ee33..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/RequiresDevice.java
+++ /dev/null
@@ -1,30 +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 androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Indicates that a specific test should not be run on emulator.
- *
- * <p>It will be executed only if the test is running on the physical android device.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.TYPE, ElementType.METHOD})
-public @interface RequiresDevice {}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SdkSuppress.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SdkSuppress.java
deleted file mode 100644
index dd65ddb..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SdkSuppress.java
+++ /dev/null
@@ -1,47 +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 androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Indicates that a specific test or class requires a minimum or maximum API Level to execute.
- *
- * <p>Test(s) will be skipped when executed on android platforms less/more than specified level
- * (inclusive).
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.TYPE, ElementType.METHOD})
-public @interface SdkSuppress {
-  /** The minimum API level to execute (inclusive) */
-  int minSdkVersion() default 1;
-  /** The maximum API level to execute (inclusive) */
-  int maxSdkVersion() default Integer.MAX_VALUE;
-  /**
-   * The {@link android.os.Build.VERSION.CODENAME} to execute on. This is intended to be used to run
-   * on a pre-release SDK, where the {@link android.os.Build.VERSION.SDK_INT} has not yet been
-   * finalized. This is treated as an OR operation with respect to the minSdkVersion and
-   * maxSdkVersion attributes.
-   *
-   * <p>For example, to filter a test so it runs on only the prerelease R SDK: <code>
-   * {@literal @}SdkSuppress(minSdkVersion = Build.VERSION_CODES.R, codeName = "R")
-   * </code>
-   */
-  String codeName() default "unset";
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SmallTest.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SmallTest.java
deleted file mode 100644
index dd32df4..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SmallTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Annotation to assign a small test size qualifier to a test. This annotation can be used at a
- * method or class level.
- *
- * <p>Test size qualifiers are a great way to structure test code and are used to assign a test to a
- * test suite of similar run time.
- *
- * <p>Execution time: &lt;200ms
- *
- * <p>Small tests should be run very frequently. Focused on units of code to verify specific logical
- * conditions. These tests should runs in an isolated environment and use mock objects for external
- * dependencies. Resource access (such as file system, network, or databases) are not permitted.
- * Tests that interact with hardware, make binder calls, or that facilitate android instrumentation
- * should not use this annotation.
- *
- * <p>Note: This class replaces the deprecated Android platform size qualifier <a
- * href="http://developer.android.com/reference/android/test/suitebuilder/annotation/SmallTest.html">
- * android.test.suitebuilder.annotation.SmallTest</a> and is the recommended way to annotate tests
- * written with the AndroidX Test Library.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
-public @interface SmallTest {}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/Suppress.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/Suppress.java
deleted file mode 100644
index 88e636c..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/Suppress.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Use this annotation on test classes or test methods that should not be included in a test suite.
- * If the annotation appears on the class then no tests in that class will be included. If the
- * annotation appears only on a test method then only that method will be excluded.
- *
- * <p>Note: This class replaces the deprecated Android platform annotation <a
- * href="http://developer.android.com/reference/android/test/suitebuilder/annotation/Suppress.html">
- * android.test.suitebuilder.annotation.Suppress</a> and is the recommended way to suppress tests
- * written with the AndroidX Test Library.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
-public @interface Suppress {}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/runner/AndroidJUnit4.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/runner/AndroidJUnit4.java
deleted file mode 100644
index e137939..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/runner/AndroidJUnit4.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.test.runner;
-
-import org.junit.runners.model.InitializationError;
-
-public class AndroidJUnit4 extends androidx.test.ext.junit.runners.AndroidJUnit4 {
-    public AndroidJUnit4(Class<?> testClass) throws InitializationError {
-        super(testClass);
-    }
-}
diff --git a/tools/hoststubgen/hoststubgen/test-framework/Android.bp b/tools/hoststubgen/hoststubgen/test-framework/Android.bp
deleted file mode 100644
index 2b91cc1..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/Android.bp
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-build = ["AndroidHostTest.bp"]
diff --git a/tools/hoststubgen/hoststubgen/test-framework/AndroidHostTest.bp b/tools/hoststubgen/hoststubgen/test-framework/AndroidHostTest.bp
deleted file mode 100644
index 1f8382a..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/AndroidHostTest.bp
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// Add `build = ["AndroidHostTest.bp"]` to Android.bp to include this file.
-
-// Compile the test jar, using 2 rules.
-// 1. Build the test against the stub.
-java_library_host {
-    name: "HostStubGenTest-framework-test-host-test-lib",
-    defaults: ["hosttest-with-framework-all-hidden-api-test-lib-defaults"],
-    srcs: [
-        "src/**/*.java",
-    ],
-    static_libs: [
-        "junit",
-        "truth",
-        "mockito",
-
-        // http://cs/h/googleplex-android/platform/superproject/main/+/main:platform_testing/libraries/annotations/src/android/platform/test/annotations/
-        "platform-test-annotations",
-        "hoststubgen-annotations",
-    ],
-}
-
-// 2. Link the above module with necessary runtime dependencies, so it can be executed stand-alone.
-java_test_host {
-    name: "HostStubGenTest-framework-all-test-host-test",
-    defaults: ["hosttest-with-framework-all-hidden-api-test-defaults"],
-    static_libs: [
-        "HostStubGenTest-framework-test-host-test-lib",
-    ],
-    test_suites: ["general-tests"],
-}
-
-// "Productionized" build rule.
-android_ravenwood_test {
-    name: "HostStubGenTest-framework-test",
-    srcs: [
-        "src/**/*.java",
-    ],
-    static_libs: [
-        "junit",
-        "truth",
-        "mockito",
-    ],
-}
diff --git a/tools/hoststubgen/hoststubgen/test-framework/AndroidTest-host.xml b/tools/hoststubgen/hoststubgen/test-framework/AndroidTest-host.xml
deleted file mode 100644
index f35dcf6..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/AndroidTest-host.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2023 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<!-- [Ravenwood] Copied from $ANDROID_BUILD_TOP/cts/hostsidetests/devicepolicy/AndroidTest.xml  -->
-<configuration description="CtsContentTestCases host-side test">
-    <option name="test-suite-tag" value="ravenwood" />
-    <option name="config-descriptor:metadata" key="component" value="framework" />
-    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
-    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
-    <option name="config-descriptor:metadata" key="parameter" value="not_secondary_user" />
-    <option name="config-descriptor:metadata" key="parameter" value="no_foldable_states" />
-
-    <test class="com.android.tradefed.testtype.IsolatedHostTest" >
-        <option name="jar" value="HostStubGenTest-framework-all-test-host-test.jar" />
-    </test>
-</configuration>
diff --git a/tools/hoststubgen/hoststubgen/test-framework/README.md b/tools/hoststubgen/hoststubgen/test-framework/README.md
deleted file mode 100644
index 26a9ad1..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/README.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# HostStubGen: (obsolete) real framework test
-
-This directory contains tests against the actual framework.jar code. The tests were
-copied from somewhere else in the android tree. We use this directory to quickly run existing
-tests.
-
-This directory was used during the prototype phase, but now that we have real ravenwood tests,
-this directory is obsolete and should be deleted.
-
-## How to run
-
-- With `atest`. This is the proper way to run it, but it may fail due to atest's known problems.
-
-```
-$ atest HostStubGenTest-framework-all-test-host-test
-```
-
-- Advanced option: `run-test-without-atest.sh` runs the test without using `atest`
-
-```
-$ ./run-test-without-atest.sh
-```
\ No newline at end of file
diff --git a/tools/hoststubgen/hoststubgen/test-framework/run-test-without-atest.sh b/tools/hoststubgen/hoststubgen/test-framework/run-test-without-atest.sh
deleted file mode 100755
index cfc06a1..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/run-test-without-atest.sh
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/bin/bash
-# Copyright (C) 2023 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Run HostStubGenTest-framework-test-host-test directly with JUnit.
-# (without using atest.)
-
-source "${0%/*}"/../../common.sh
-
-
-# Options:
-# -v enable verbose log
-# -d enable debugger
-
-verbose=0
-debug=0
-while getopts "vd" opt; do
-  case "$opt" in
-    v) verbose=1 ;;
-    d) debug=1 ;;
-  esac
-done
-shift $(($OPTIND - 1))
-
-
-if (( $verbose )) ; then
-  JAVA_OPTS="$JAVA_OPTS -verbose:class"
-fi
-
-if (( $debug )) ; then
-  JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8700"
-fi
-
-#=======================================
-module=HostStubGenTest-framework-all-test-host-test
-module_jar=$ANDROID_BUILD_TOP/out/host/linux-x86/testcases/$module/$module.jar
-run m $module
-
-out=out
-
-rm -fr $out
-mkdir -p $out
-
-
-# Copy and extract the relevant jar files so we can look into them.
-run cp \
-    $module_jar \
-    $SOONG_INT/frameworks/base/tools/hoststubgen/hoststubgen/framework-all-hidden-api-host/linux_glibc_common/gen/*.jar \
-    $out
-
-run extract $out/*.jar
-
-# Result is the number of failed tests.
-result=0
-
-
-# This suite runs all tests in the JAR.
-tests=(com.android.hoststubgen.hosthelper.HostTestSuite)
-
-# Uncomment this to run a specific test.
-# tests=(com.android.hoststubgen.frameworktest.LogTest)
-
-
-for class in ${tests[@]} ; do
-  echo "Running $class ..."
-
-  run cd "${module_jar%/*}"
-  run $JAVA $JAVA_OPTS \
-      -cp $module_jar \
-      org.junit.runner.JUnitCore \
-      $class || result=$(( $result + 1 ))
-done
-
-exit $result
diff --git a/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/ArrayMapTest.java b/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/ArrayMapTest.java
deleted file mode 100644
index 2c5949c..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/ArrayMapTest.java
+++ /dev/null
@@ -1,472 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.hoststubgen.frameworktest;
-
-// [ravewnwood] Copied from cts/, and commented out unsupported stuff.
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.util.ArrayMap;
-import android.util.Log;
-
-import org.junit.Test;
-
-import java.util.AbstractMap;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.NoSuchElementException;
-import java.util.Set;
-import java.util.function.BiFunction;
-
-/**
- * Some basic tests for {@link android.util.ArrayMap}.
- */
-public class ArrayMapTest {
-    static final boolean DEBUG = false;
-
-    private static boolean compare(Object v1, Object v2) {
-        if (v1 == null) {
-            return v2 == null;
-        }
-        if (v2 == null) {
-            return false;
-        }
-        return v1.equals(v2);
-    }
-
-    private static void compareMaps(HashMap map, ArrayMap array) {
-        if (map.size() != array.size()) {
-            fail("Bad size: expected " + map.size() + ", got " + array.size());
-        }
-
-        Set<Entry> mapSet = map.entrySet();
-        for (Map.Entry entry : mapSet) {
-            Object expValue = entry.getValue();
-            Object gotValue = array.get(entry.getKey());
-            if (!compare(expValue, gotValue)) {
-                fail("Bad value: expected " + expValue + ", got " + gotValue
-                        + " at key " + entry.getKey());
-            }
-        }
-
-        for (int i = 0; i < array.size(); i++) {
-            Object gotValue = array.valueAt(i);
-            Object key = array.keyAt(i);
-            Object expValue = map.get(key);
-            if (!compare(expValue, gotValue)) {
-                fail("Bad value: expected " + expValue + ", got " + gotValue
-                        + " at key " + key);
-            }
-        }
-
-        if (map.entrySet().hashCode() != array.entrySet().hashCode()) {
-            fail("Entry set hash codes differ: map=0x"
-                    + Integer.toHexString(map.entrySet().hashCode()) + " array=0x"
-                    + Integer.toHexString(array.entrySet().hashCode()));
-        }
-
-        if (!map.entrySet().equals(array.entrySet())) {
-            fail("Failed calling equals on map entry set against array set");
-        }
-
-        if (!array.entrySet().equals(map.entrySet())) {
-            fail("Failed calling equals on array entry set against map set");
-        }
-
-        if (map.keySet().hashCode() != array.keySet().hashCode()) {
-            fail("Key set hash codes differ: map=0x"
-                    + Integer.toHexString(map.keySet().hashCode()) + " array=0x"
-                    + Integer.toHexString(array.keySet().hashCode()));
-        }
-
-        if (!map.keySet().equals(array.keySet())) {
-            fail("Failed calling equals on map key set against array set");
-        }
-
-        if (!array.keySet().equals(map.keySet())) {
-            fail("Failed calling equals on array key set against map set");
-        }
-
-        if (!map.keySet().containsAll(array.keySet())) {
-            fail("Failed map key set contains all of array key set");
-        }
-
-        if (!array.keySet().containsAll(map.keySet())) {
-            fail("Failed array key set contains all of map key set");
-        }
-
-        if (!array.containsAll(map.keySet())) {
-            fail("Failed array contains all of map key set");
-        }
-
-        if (!map.entrySet().containsAll(array.entrySet())) {
-            fail("Failed map entry set contains all of array entry set");
-        }
-
-        if (!array.entrySet().containsAll(map.entrySet())) {
-            fail("Failed array entry set contains all of map entry set");
-        }
-    }
-
-    private static void validateArrayMap(ArrayMap array) {
-        Set<Map.Entry> entrySet = array.entrySet();
-        int index = 0;
-        Iterator<Entry> entryIt = entrySet.iterator();
-        while (entryIt.hasNext()) {
-            Map.Entry entry = entryIt.next();
-            Object value = entry.getKey();
-            Object realValue = array.keyAt(index);
-            if (!compare(realValue, value)) {
-                fail("Bad array map entry set: expected key " + realValue
-                        + ", got " + value + " at index " + index);
-            }
-            value = entry.getValue();
-            realValue = array.valueAt(index);
-            if (!compare(realValue, value)) {
-                fail("Bad array map entry set: expected value " + realValue
-                        + ", got " + value + " at index " + index);
-            }
-            index++;
-        }
-
-        index = 0;
-        Set keySet = array.keySet();
-        Iterator keyIt = keySet.iterator();
-        while (keyIt.hasNext()) {
-            Object value = keyIt.next();
-            Object realValue = array.keyAt(index);
-            if (!compare(realValue, value)) {
-                fail("Bad array map key set: expected key " + realValue
-                        + ", got " + value + " at index " + index);
-            }
-            index++;
-        }
-
-        index = 0;
-        Collection valueCol = array.values();
-        Iterator valueIt = valueCol.iterator();
-        while (valueIt.hasNext()) {
-            Object value = valueIt.next();
-            Object realValue = array.valueAt(index);
-            if (!compare(realValue, value)) {
-                fail("Bad array map value col: expected value " + realValue
-                        + ", got " + value + " at index " + index);
-            }
-            index++;
-        }
-    }
-
-    private static void dump(Map map, ArrayMap array) {
-        Log.e("test", "HashMap of " + map.size() + " entries:");
-        Set<Map.Entry> mapSet = map.entrySet();
-        for (Map.Entry entry : mapSet) {
-            Log.e("test", "    " + entry.getKey() + " -> " + entry.getValue());
-        }
-        Log.e("test", "ArrayMap of " + array.size() + " entries:");
-        for (int i = 0; i < array.size(); i++) {
-            Log.e("test", "    " + array.keyAt(i) + " -> " + array.valueAt(i));
-        }
-    }
-
-    private static void dump(ArrayMap map1, ArrayMap map2) {
-        Log.e("test", "ArrayMap of " + map1.size() + " entries:");
-        for (int i = 0; i < map1.size(); i++) {
-            Log.e("test", "    " + map1.keyAt(i) + " -> " + map1.valueAt(i));
-        }
-        Log.e("test", "ArrayMap of " + map2.size() + " entries:");
-        for (int i = 0; i < map2.size(); i++) {
-            Log.e("test", "    " + map2.keyAt(i) + " -> " + map2.valueAt(i));
-        }
-    }
-
-    @Test
-    public void testCopyArrayMap() {
-        // map copy constructor test
-        ArrayMap newMap = new ArrayMap<Integer, String>();
-        for (int i = 0; i < 10; ++i) {
-            newMap.put(i, String.valueOf(i));
-        }
-        ArrayMap mapCopy = new ArrayMap(newMap);
-        if (!compare(mapCopy, newMap)) {
-            String msg = "ArrayMap copy constructor failure: expected " +
-                    newMap + ", got " + mapCopy;
-            Log.e("test", msg);
-            dump(newMap, mapCopy);
-            fail(msg);
-            return;
-        }
-    }
-
-    @Test
-    public void testEqualsArrayMap() {
-        ArrayMap<Integer, String> map1 = new ArrayMap<>();
-        ArrayMap<Integer, String> map2 = new ArrayMap<>();
-        HashMap<Integer, String> map3 = new HashMap<>();
-        if (!compare(map1, map2) || !compare(map1, map3) || !compare(map3, map2)) {
-            fail("ArrayMap equals failure for empty maps " + map1 + ", " +
-                    map2 + ", " + map3);
-        }
-
-        for (int i = 0; i < 10; ++i) {
-            String value = String.valueOf(i);
-            map1.put(i, value);
-            map2.put(i, value);
-            map3.put(i, value);
-        }
-        if (!compare(map1, map2) || !compare(map1, map3) || !compare(map3, map2)) {
-            fail("ArrayMap equals failure for populated maps " + map1 + ", " +
-                    map2 + ", " + map3);
-        }
-
-        map1.remove(0);
-        if (compare(map1, map2) || compare(map1, map3) || compare(map3, map1)) {
-            fail("ArrayMap equals failure for map size " + map1 + ", " +
-                    map2 + ", " + map3);
-        }
-
-        map1.put(0, "-1");
-        if (compare(map1, map2) || compare(map1, map3) || compare(map3, map1)) {
-            fail("ArrayMap equals failure for map contents " + map1 + ", " +
-                    map2 + ", " + map3);
-        }
-    }
-
-    private static void checkEntrySetToArray(ArrayMap<?, ?> testMap) {
-        try {
-            testMap.entrySet().toArray();
-            fail();
-        } catch (UnsupportedOperationException expected) {
-        }
-
-        try {
-            Map.Entry<?, ?>[] entries = new Map.Entry[20];
-            testMap.entrySet().toArray(entries);
-            fail();
-        } catch (UnsupportedOperationException expected) {
-        }
-    }
-
-    // http://b/32294038, Test ArrayMap.entrySet().toArray()
-    @Test
-    public void testEntrySetArray() {
-        // Create
-        ArrayMap<Integer, String> testMap = new ArrayMap<>();
-
-        // Test empty
-        checkEntrySetToArray(testMap);
-
-        // Test non-empty
-        for (int i = 0; i < 10; ++i) {
-            testMap.put(i, String.valueOf(i));
-        }
-        checkEntrySetToArray(testMap);
-    }
-
-    @Test
-    public void testCanNotIteratePastEnd_entrySetIterator() {
-        Map<String, String> map = new ArrayMap<>();
-        map.put("key 1", "value 1");
-        map.put("key 2", "value 2");
-        Set<Map.Entry<String, String>> expectedEntriesToIterate = new HashSet<>(Arrays.asList(
-                entryOf("key 1", "value 1"),
-                entryOf("key 2", "value 2")
-        ));
-        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
-
-        // Assert iteration over the expected two entries in any order
-        assertTrue(iterator.hasNext());
-        Map.Entry<String, String> firstEntry = copyOf(iterator.next());
-        assertTrue(expectedEntriesToIterate.remove(firstEntry));
-
-        assertTrue(iterator.hasNext());
-        Map.Entry<String, String> secondEntry = copyOf(iterator.next());
-        assertTrue(expectedEntriesToIterate.remove(secondEntry));
-
-        assertFalse(iterator.hasNext());
-
-        try {
-            iterator.next();
-            fail();
-        } catch (NoSuchElementException expected) {
-        }
-    }
-
-    private static <K, V> Map.Entry<K, V> entryOf(K key, V value) {
-        return new AbstractMap.SimpleEntry<>(key, value);
-    }
-
-    private static <K, V> Map.Entry<K, V> copyOf(Map.Entry<K, V> entry) {
-        return entryOf(entry.getKey(), entry.getValue());
-    }
-
-    @Test
-    public void testCanNotIteratePastEnd_keySetIterator() {
-        Map<String, String> map = new ArrayMap<>();
-        map.put("key 1", "value 1");
-        map.put("key 2", "value 2");
-        Set<String> expectedKeysToIterate = new HashSet<>(Arrays.asList("key 1", "key 2"));
-        Iterator<String> iterator = map.keySet().iterator();
-
-        // Assert iteration over the expected two keys in any order
-        assertTrue(iterator.hasNext());
-        String firstKey = iterator.next();
-        assertTrue(expectedKeysToIterate.remove(firstKey));
-
-        assertTrue(iterator.hasNext());
-        String secondKey = iterator.next();
-        assertTrue(expectedKeysToIterate.remove(secondKey));
-
-        assertFalse(iterator.hasNext());
-
-        try {
-            iterator.next();
-            fail();
-        } catch (NoSuchElementException expected) {
-        }
-    }
-
-    @Test
-    public void testCanNotIteratePastEnd_valuesIterator() {
-        Map<String, String> map = new ArrayMap<>();
-        map.put("key 1", "value 1");
-        map.put("key 2", "value 2");
-        Set<String> expectedValuesToIterate = new HashSet<>(Arrays.asList("value 1", "value 2"));
-        Iterator<String> iterator = map.values().iterator();
-
-        // Assert iteration over the expected two values in any order
-        assertTrue(iterator.hasNext());
-        String firstValue = iterator.next();
-        assertTrue(expectedValuesToIterate.remove(firstValue));
-
-        assertTrue(iterator.hasNext());
-        String secondValue = iterator.next();
-        assertTrue(expectedValuesToIterate.remove(secondValue));
-
-        assertFalse(iterator.hasNext());
-
-        try {
-            iterator.next();
-            fail();
-        } catch (NoSuchElementException expected) {
-        }
-    }
-
-    @Test
-    public void testForEach() {
-        ArrayMap<String, Integer> map = new ArrayMap<>();
-
-        for (int i = 0; i < 50; ++i) {
-            map.put(Integer.toString(i), i * 10);
-        }
-
-        // Make sure forEach goes through all of the elements.
-        HashMap<String, Integer> seen = new HashMap<>();
-        map.forEach(seen::put);
-        compareMaps(seen, map);
-    }
-
-    /**
-     * The entrySet Iterator returns itself from each call to {@code next()}. This is unusual
-     * behavior for {@link Iterator#next()}; this test ensures that any future change to this
-     * behavior is deliberate.
-     */
-    @Test
-    public void testUnusualBehavior_eachEntryIsSameAsIterator_entrySetIterator() {
-        Map<String, String> map = new ArrayMap<>();
-        map.put("key 1", "value 1");
-        map.put("key 2", "value 2");
-        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
-
-        assertSame(iterator, iterator.next());
-        assertSame(iterator, iterator.next());
-    }
-
-    @SuppressWarnings("SelfEquals")
-    @Test
-    public void testUnusualBehavior_equalsThrowsAfterRemove_entrySetIterator() {
-        Map<String, String> map = new ArrayMap<>();
-        map.put("key 1", "value 1");
-        map.put("key 2", "value 2");
-        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
-        iterator.next();
-        iterator.remove();
-        try {
-            iterator.equals(iterator);
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-    }
-
-    private static <T> void assertEqualsBothWays(T a, T b) {
-        assertEquals(a, b);
-        assertEquals(b, a);
-        assertEquals(a.hashCode(), b.hashCode());
-    }
-
-    @Test
-    public void testRemoveAll() {
-        final ArrayMap<Integer, String> map = new ArrayMap<>();
-        for (Integer i : Arrays.asList(0, 1, 2, 3, 4, 5)) {
-            map.put(i, i.toString());
-        }
-
-        final ArrayMap<Integer, String> expectedMap = new ArrayMap<>();
-        for (Integer i : Arrays.asList(2, 4)) {
-            expectedMap.put(i, String.valueOf(i));
-        }
-        map.removeAll(Arrays.asList(0, 1, 3, 5, 6));
-        if (!compare(map, expectedMap)) {
-            fail("ArrayMap removeAll failure, expect " + expectedMap + ", but " + map);
-        }
-
-        map.removeAll(Collections.emptyList());
-        if (!compare(map, expectedMap)) {
-            fail("ArrayMap removeAll failure for empty maps, expect " + expectedMap + ", but " +
-                    map);
-        }
-
-        map.removeAll(Arrays.asList(2, 4));
-        if (!map.isEmpty()) {
-            fail("ArrayMap removeAll failure, expect empty, but " + map);
-        }
-    }
-
-    @Test
-    public void testReplaceAll() {
-        final ArrayMap<Integer, Integer> map = new ArrayMap<>();
-        final ArrayMap<Integer, Integer> expectedMap = new ArrayMap<>();
-        final BiFunction<Integer, Integer, Integer> function = (k, v) -> 2 * v;
-        for (Integer i : Arrays.asList(0, 1, 2, 3, 4, 5)) {
-            map.put(i, i);
-            expectedMap.put(i, 2 * i);
-        }
-
-        map.replaceAll(function);
-        if (!compare(map, expectedMap)) {
-            fail("ArrayMap replaceAll failure, expect " + expectedMap + ", but " + map);
-        }
-    }
-}
diff --git a/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/LogTest.java b/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/LogTest.java
deleted file mode 100644
index 3e33b54..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/LogTest.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.hoststubgen.frameworktest;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-/**
- * Some basic tests for {@link android.util.Log}.
- */
-public class LogTest {
-    @Test
-    public void testBasicLogging() {
-        Log.v("TAG", "Test v log");
-        Log.d("TAG", "Test d log");
-        Log.i("TAG", "Test i log");
-        Log.w("TAG", "Test w log");
-        Log.e("TAG", "Test e log");
-    }
-
-    @Test
-    public void testNativeMethods() {
-        assertThat(Log.isLoggable("mytag", Log.INFO)).isTrue();
-    }
-}
diff --git a/tools/hoststubgen/scripts/build-framework-hostside-jars-and-extract.sh b/tools/hoststubgen/scripts/build-framework-hostside-jars-and-extract.sh
deleted file mode 100755
index 7268123..0000000
--- a/tools/hoststubgen/scripts/build-framework-hostside-jars-and-extract.sh
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/bin/bash
-# Copyright (C) 2023 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-
-# Script to build `framework-host-stub` and `framework-host-impl`, and copy the
-# generated jars to $out, and unzip them. Useful for looking into the generated files.
-
-source "${0%/*}"/../common.sh
-
-out=framework-all-stub-out
-
-rm -fr $out
-mkdir -p $out
-
-# Build the jars with `m`.
-run m framework-all-hidden-api-host
-
-# Copy the jar to out/ and extract them.
-run cp \
-    $SOONG_INT/frameworks/base/tools/hoststubgen/hoststubgen/framework-all-hidden-api-host/linux_glibc_common/gen/* \
-    $out
-
-extract $out/*.jar
diff --git a/tools/hoststubgen/scripts/run-all-tests.sh b/tools/hoststubgen/scripts/run-all-tests.sh
index 222c874..a6847ae 100755
--- a/tools/hoststubgen/scripts/run-all-tests.sh
+++ b/tools/hoststubgen/scripts/run-all-tests.sh
@@ -22,7 +22,6 @@
 
 # These tests are known to pass.
 READY_TEST_MODULES=(
-  HostStubGenTest-framework-all-test-host-test
   hoststubgen-test-tiny-test
   CtsUtilTestCasesRavenwood
   CtsOsTestCasesRavenwood # This one uses native sustitution, so let's run it too.
@@ -30,7 +29,6 @@
 
 MUST_BUILD_MODULES=(
     "${NOT_READY_TEST_MODULES[*]}"
-    HostStubGenTest-framework-test
 )
 
 # First, build all the test / etc modules. This shouldn't fail.
@@ -44,11 +42,8 @@
 # files, and they may fail when something changes in the build system.
 run ./hoststubgen/test-tiny-framework/diff-and-update-golden.sh
 
-run ./hoststubgen/test-framework/run-test-without-atest.sh
-
 run ./hoststubgen/test-tiny-framework/run-test-manually.sh
 run atest $ATEST_ARGS tiny-framework-dump-test
-run ./scripts/build-framework-hostside-jars-and-extract.sh
 
 # This script is already broken on goog/master
 # run ./scripts/build-framework-hostside-jars-without-genrules.sh