Merge "Remove CommunalContainer touch forwarding" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 9e30843..de1a0d7 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -71,6 +71,7 @@
     ":android.appwidget.flags-aconfig-java{.generated_srcjars}",
     ":android.webkit.flags-aconfig-java{.generated_srcjars}",
     ":android.provider.flags-aconfig-java{.generated_srcjars}",
+    ":android.chre.flags-aconfig-java{.generated_srcjars}",
 ]
 
 filegroup {
@@ -905,3 +906,10 @@
     aconfig_declarations: "android.provider.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// ContextHub
+java_aconfig_library {
+    name: "android.chre.flags-aconfig-java",
+    aconfig_declarations: "chre_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/api/ApiDocs.bp b/api/ApiDocs.bp
index bcfb68f..6b8c02f 100644
--- a/api/ApiDocs.bp
+++ b/api/ApiDocs.bp
@@ -183,6 +183,7 @@
         "-federationapi AndroidX $(location :current-androidx-api)",
         // doclava contains checks for a few issues that are have been migrated to metalava.
         // disable them in doclava, to avoid mistriggering or double triggering.
+        "-hide 101", // TODO: turn Lint 101 back into an error again
         "-hide 111", // HIDDEN_SUPERCLASS
         "-hide 113", // DEPRECATION_MISMATCH
         "-hide 125", // REQUIRES_PERMISSION
diff --git a/api/api.go b/api/api.go
index 2668999..43713aa 100644
--- a/api/api.go
+++ b/api/api.go
@@ -64,6 +64,7 @@
 
 type CombinedApis struct {
 	android.ModuleBase
+	android.DefaultableModuleBase
 
 	properties CombinedApisProperties
 }
@@ -74,6 +75,7 @@
 
 func registerBuildComponents(ctx android.RegistrationContext) {
 	ctx.RegisterModuleType("combined_apis", combinedApisModuleFactory)
+	ctx.RegisterModuleType("combined_apis_defaults", CombinedApisModuleDefaultsFactory)
 }
 
 var PrepareForCombinedApisTest = android.FixtureRegisterWithContext(registerBuildComponents)
@@ -409,6 +411,7 @@
 	module := &CombinedApis{}
 	module.AddProperties(&module.properties)
 	android.InitAndroidModule(module)
+	android.InitDefaultableModule(module)
 	android.AddLoadHook(module, func(ctx android.LoadHookContext) { module.createInternalModules(ctx) })
 	return module
 }
@@ -445,3 +448,16 @@
 	}
 	return s2
 }
+
+// Defaults
+type CombinedApisModuleDefaults struct {
+	android.ModuleBase
+	android.DefaultsModuleBase
+}
+
+func CombinedApisModuleDefaultsFactory() android.Module {
+	module := &CombinedApisModuleDefaults{}
+	module.AddProperties(&CombinedApisProperties{})
+	android.InitDefaultsModule(module)
+	return module
+}
diff --git a/core/api/current.txt b/core/api/current.txt
index af821b6..7c34812 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -49,6 +49,7 @@
     field public static final String BIND_SCREENING_SERVICE = "android.permission.BIND_SCREENING_SERVICE";
     field public static final String BIND_TELECOM_CONNECTION_SERVICE = "android.permission.BIND_TELECOM_CONNECTION_SERVICE";
     field public static final String BIND_TEXT_SERVICE = "android.permission.BIND_TEXT_SERVICE";
+    field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String BIND_TV_AD_SERVICE = "android.permission.BIND_TV_AD_SERVICE";
     field public static final String BIND_TV_INPUT = "android.permission.BIND_TV_INPUT";
     field public static final String BIND_TV_INTERACTIVE_APP = "android.permission.BIND_TV_INTERACTIVE_APP";
     field public static final String BIND_VISUAL_VOICEMAIL_SERVICE = "android.permission.BIND_VISUAL_VOICEMAIL_SERVICE";
@@ -1601,7 +1602,6 @@
     field public static final int switchTextOff = 16843628; // 0x101036c
     field public static final int switchTextOn = 16843627; // 0x101036b
     field public static final int syncable = 16842777; // 0x1010019
-    field @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") public static final int systemUserOnly;
     field public static final int tabStripEnabled = 16843453; // 0x10102bd
     field public static final int tabStripLeft = 16843451; // 0x10102bb
     field public static final int tabStripRight = 16843452; // 0x10102bc
@@ -19734,7 +19734,7 @@
   @FlaggedApi("com.android.internal.camera.flags.concert_mode") public final class LensIntrinsicsSample {
     ctor @FlaggedApi("com.android.internal.camera.flags.concert_mode") public LensIntrinsicsSample(long, @NonNull float[]);
     method @FlaggedApi("com.android.internal.camera.flags.concert_mode") @NonNull public float[] getLensIntrinsics();
-    method @FlaggedApi("com.android.internal.camera.flags.concert_mode") public long getTimestamp();
+    method @FlaggedApi("com.android.internal.camera.flags.concert_mode") public long getTimestampNanos();
   }
 
   public final class LensShadingMap {
@@ -33401,7 +33401,7 @@
 
   @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public final class PowerMonitorReadings {
     method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public long getConsumedEnergy(@NonNull android.os.PowerMonitor);
-    method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public long getTimestamp(@NonNull android.os.PowerMonitor);
+    method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public long getTimestampMillis(@NonNull android.os.PowerMonitor);
     field @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public static final int ENERGY_UNAVAILABLE = -1; // 0xffffffff
   }
 
@@ -33996,8 +33996,8 @@
   }
 
   public class SystemHealthManager {
-    method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getPowerMonitorReadings(@NonNull java.util.List<android.os.PowerMonitor>, @Nullable android.os.Handler, @NonNull java.util.function.Consumer<android.os.PowerMonitorReadings>, @NonNull java.util.function.Consumer<java.lang.RuntimeException>);
-    method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getSupportedPowerMonitors(@Nullable android.os.Handler, @NonNull java.util.function.Consumer<java.util.List<android.os.PowerMonitor>>);
+    method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getPowerMonitorReadings(@NonNull java.util.List<android.os.PowerMonitor>, @Nullable java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.PowerMonitorReadings,java.lang.RuntimeException>);
+    method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getSupportedPowerMonitors(@Nullable java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.os.PowerMonitor>>);
     method public android.os.health.HealthStats takeMyUidSnapshot();
     method public android.os.health.HealthStats takeUidSnapshot(int);
     method public android.os.health.HealthStats[] takeUidSnapshots(int[]);
@@ -47295,6 +47295,7 @@
     method public int getLineForOffset(int);
     method public int getLineForVertical(int);
     method public float getLineLeft(int);
+    method @FlaggedApi("com.android.text.flags.inter_character_justification") @IntRange(from=0) public int getLineLetterSpacingUnitCount(@IntRange(from=0) int, boolean);
     method public float getLineMax(int);
     method public float getLineRight(int);
     method @FlaggedApi("com.android.text.flags.use_bounds_for_width") public final float getLineSpacingAmount();
@@ -56500,6 +56501,7 @@
     field public static final String TYPE_EMAIL = "email";
     field public static final String TYPE_FLIGHT_NUMBER = "flight";
     field public static final String TYPE_OTHER = "other";
+    field @FlaggedApi("android.service.notification.redact_sensitive_notifications_from_untrusted_listeners") public static final String TYPE_OTP_CODE = "otp_code";
     field public static final String TYPE_PHONE = "phone";
     field public static final String TYPE_UNKNOWN = "";
     field public static final String TYPE_URL = "url";
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 2898705..a532cdb 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -1596,12 +1596,14 @@
     method public int getDensityLevel();
     method @NonNull public java.time.Instant getEndTime();
     method public int getEventType();
+    method @FlaggedApi("android.app.ambient_heart_rate") @IntRange(from=0xffffffff) public int getRatePerMinute();
     method @NonNull public java.time.Instant getStartTime();
     method @NonNull public android.os.PersistableBundle getVendorData();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.ambientcontext.AmbientContextEvent> CREATOR;
     field public static final int EVENT_BACK_DOUBLE_TAP = 3; // 0x3
     field public static final int EVENT_COUGH = 1; // 0x1
+    field @FlaggedApi("android.app.ambient_heart_rate") public static final int EVENT_HEART_RATE = 4; // 0x4
     field public static final int EVENT_SNORE = 2; // 0x2
     field public static final int EVENT_UNKNOWN = 0; // 0x0
     field public static final int EVENT_VENDOR_WEARABLE_START = 100000; // 0x186a0
@@ -1621,6 +1623,7 @@
     method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setDensityLevel(int);
     method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setEndTime(@NonNull java.time.Instant);
     method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setEventType(int);
+    method @FlaggedApi("android.app.ambient_heart_rate") @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setRatePerMinute(@IntRange(from=0xffffffff) int);
     method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setStartTime(@NonNull java.time.Instant);
     method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setVendorData(@NonNull android.os.PersistableBundle);
   }
@@ -14681,6 +14684,7 @@
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isLteCdmaEvdoGsmWcdmaEnabled();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isMobileDataPolicyEnabled(int);
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isNrDualConnectivityEnabled();
+    method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isNullCipherNotificationsEnabled();
     method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public boolean isOffhook();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isOpportunisticNetworkEnabled();
     method @Deprecated @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isPotentialEmergencyNumber(@NonNull String);
@@ -14720,6 +14724,7 @@
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataEnabled(int, boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataRoamingEnabled(boolean);
     method @FlaggedApi("com.android.internal.telephony.flags.enable_identifier_disclosure_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableCellularIdentifierDisclosureNotifications(boolean);
+    method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableNullCipherNotifications(boolean);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.telephony.PinResult setIccLockEnabled(boolean, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMobileDataPolicyEnabled(int, boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMultiSimCarrierRestriction(boolean);
@@ -15127,7 +15132,7 @@
     method public final void notifyDataCallListChanged(java.util.List<android.telephony.data.DataCallResponse>);
     method public final void notifyDataProfileUnthrottled(@NonNull android.telephony.data.DataProfile);
     method public void requestDataCallList(@NonNull android.telephony.data.DataServiceCallback);
-    method @FlaggedApi("com.android.internal.telephony.flags.network_validation") public void requestValidation(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+    method @FlaggedApi("com.android.internal.telephony.flags.network_validation") public void requestNetworkValidation(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method public void setDataProfile(@NonNull java.util.List<android.telephony.data.DataProfile>, boolean, @NonNull android.telephony.data.DataServiceCallback);
     method public void setInitialAttachApn(@NonNull android.telephony.data.DataProfile, boolean, @NonNull android.telephony.data.DataServiceCallback);
     method public void setupDataCall(int, @NonNull android.telephony.data.DataProfile, boolean, boolean, int, @Nullable android.net.LinkProperties, @NonNull android.telephony.data.DataServiceCallback);
@@ -16334,7 +16339,7 @@
 
   public interface RegistrationManager {
     field public static final int SUGGESTED_ACTION_NONE = 0; // 0x0
-    field @FlaggedApi("com.android.internal.telephony.flags.add_rat_related_suggested_action_to_ims_registration") public static final int SUGGESTED_ACTION_TRIGGER_CLEAR_RAT_BLOCK = 4; // 0x4
+    field @FlaggedApi("com.android.internal.telephony.flags.add_rat_related_suggested_action_to_ims_registration") public static final int SUGGESTED_ACTION_TRIGGER_CLEAR_RAT_BLOCKS = 4; // 0x4
     field public static final int SUGGESTED_ACTION_TRIGGER_PLMN_BLOCK = 1; // 0x1
     field public static final int SUGGESTED_ACTION_TRIGGER_PLMN_BLOCK_WITH_TIMEOUT = 2; // 0x2
     field @FlaggedApi("com.android.internal.telephony.flags.add_rat_related_suggested_action_to_ims_registration") public static final int SUGGESTED_ACTION_TRIGGER_RAT_BLOCK = 3; // 0x3
@@ -17032,8 +17037,8 @@
 
   @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public final class PointingInfo implements android.os.Parcelable {
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public int describeContents();
-    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public float getSatelliteAzimuthDegrees();
-    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public float getSatelliteElevationDegrees();
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @FloatRange(from=0xffffff4c, to=180) public float getSatelliteAzimuthDegrees();
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @FloatRange(from=0xffffffa6, to=90) public float getSatelliteElevationDegrees();
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void writeToParcel(@NonNull android.os.Parcel, int);
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public static final android.os.Parcelable.Creator<android.telephony.satellite.PointingInfo> CREATOR;
   }
@@ -17072,7 +17077,7 @@
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void registerForNtnSignalStrengthChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.NtnSignalStrengthCallback) throws android.telephony.satellite.SatelliteManager.SatelliteException;
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteCapabilitiesChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCapabilitiesCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteDatagram(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteDatagramCallback);
-    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteModemStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteStateCallback);
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteModemStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteModemStateCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteProvisionStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteProvisionStateCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void removeSatelliteAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsDemoModeEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
@@ -17093,7 +17098,7 @@
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForNtnSignalStrengthChanged(@NonNull android.telephony.satellite.NtnSignalStrengthCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteCapabilitiesChanged(@NonNull android.telephony.satellite.SatelliteCapabilitiesCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteDatagram(@NonNull android.telephony.satellite.SatelliteDatagramCallback);
-    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteModemStateChanged(@NonNull android.telephony.satellite.SatelliteStateCallback);
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteModemStateChanged(@NonNull android.telephony.satellite.SatelliteModemStateCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteProvisionStateChanged(@NonNull android.telephony.satellite.SatelliteProvisionStateCallback);
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int DATAGRAM_TYPE_LOCATION_SHARING = 2; // 0x2
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int DATAGRAM_TYPE_SOS_MESSAGE = 1; // 0x1
@@ -17163,12 +17168,12 @@
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public int getErrorCode();
   }
 
-  @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public interface SatelliteProvisionStateCallback {
-    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void onSatelliteProvisionStateChanged(boolean);
+  @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public interface SatelliteModemStateCallback {
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void onSatelliteModemStateChanged(int);
   }
 
-  @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public interface SatelliteStateCallback {
-    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void onSatelliteModemStateChanged(int);
+  @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public interface SatelliteProvisionStateCallback {
+    method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void onSatelliteProvisionStateChanged(boolean);
   }
 
   @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public interface SatelliteTransmissionUpdateCallback {
diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt
index b2a28b2..0505af4 100644
--- a/core/api/system-lint-baseline.txt
+++ b/core/api/system-lint-baseline.txt
@@ -2359,8 +2359,8 @@
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.provisionSatelliteService(String,byte[],android.os.CancellationSignal,java.util.concurrent.Executor,java.util.function.Consumer<java.lang.Integer>)
 UnflaggedApi: android.telephony.satellite.SatelliteManager#registerForSatelliteDatagram(java.util.concurrent.Executor, android.telephony.satellite.SatelliteDatagramCallback):
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.registerForSatelliteDatagram(java.util.concurrent.Executor,android.telephony.satellite.SatelliteDatagramCallback)
-UnflaggedApi: android.telephony.satellite.SatelliteManager#registerForSatelliteModemStateChanged(java.util.concurrent.Executor, android.telephony.satellite.SatelliteStateCallback):
-    New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.registerForSatelliteModemStateChanged(java.util.concurrent.Executor,android.telephony.satellite.SatelliteStateCallback)
+UnflaggedApi: android.telephony.satellite.SatelliteManager#registerForSatelliteModemStateChanged(java.util.concurrent.Executor, android.telephony.satellite.SatelliteModemStateCallback):
+    New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.registerForSatelliteModemStateChanged(java.util.concurrent.Executor,android.telephony.satellite.SatelliteModemStateCallback)
 UnflaggedApi: android.telephony.satellite.SatelliteManager#registerForSatelliteProvisionStateChanged(java.util.concurrent.Executor, android.telephony.satellite.SatelliteProvisionStateCallback):
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.registerForSatelliteProvisionStateChanged(java.util.concurrent.Executor,android.telephony.satellite.SatelliteProvisionStateCallback)
 UnflaggedApi: android.telephony.satellite.SatelliteManager#requestIsDemoModeEnabled(java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>):
@@ -2389,8 +2389,8 @@
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.stopSatelliteTransmissionUpdates(android.telephony.satellite.SatelliteTransmissionUpdateCallback,java.util.concurrent.Executor,java.util.function.Consumer<java.lang.Integer>)
 UnflaggedApi: android.telephony.satellite.SatelliteManager#unregisterForSatelliteDatagram(android.telephony.satellite.SatelliteDatagramCallback):
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.unregisterForSatelliteDatagram(android.telephony.satellite.SatelliteDatagramCallback)
-UnflaggedApi: android.telephony.satellite.SatelliteManager#unregisterForSatelliteModemStateChanged(android.telephony.satellite.SatelliteStateCallback):
-    New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.unregisterForSatelliteModemStateChanged(android.telephony.satellite.SatelliteStateCallback)
+UnflaggedApi: android.telephony.satellite.SatelliteManager#unregisterForSatelliteModemStateChanged(android.telephony.satellite.SatelliteModemStateCallback):
+    New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.unregisterForSatelliteModemStateChanged(android.telephony.satellite.SatelliteModemStateCallback)
 UnflaggedApi: android.telephony.satellite.SatelliteManager#unregisterForSatelliteProvisionStateChanged(android.telephony.satellite.SatelliteProvisionStateCallback):
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.unregisterForSatelliteProvisionStateChanged(android.telephony.satellite.SatelliteProvisionStateCallback)
 UnflaggedApi: android.telephony.satellite.SatelliteManager.SatelliteException:
@@ -2403,10 +2403,10 @@
     New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteProvisionStateCallback
 UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback#onSatelliteProvisionStateChanged(boolean):
     New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteProvisionStateCallback.onSatelliteProvisionStateChanged(boolean)
-UnflaggedApi: android.telephony.satellite.SatelliteStateCallback:
-    New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteStateCallback
-UnflaggedApi: android.telephony.satellite.SatelliteStateCallback#onSatelliteModemStateChanged(int):
-    New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteStateCallback.onSatelliteModemStateChanged(int)
+UnflaggedApi: android.telephony.satellite.SatelliteModemStateCallback:
+    New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteModemStateCallback
+UnflaggedApi: android.telephony.satellite.SatelliteModemStateCallback#onSatelliteModemStateChanged(int):
+    New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteModemStateCallback.onSatelliteModemStateChanged(int)
 UnflaggedApi: android.telephony.satellite.SatelliteTransmissionUpdateCallback:
     New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteTransmissionUpdateCallback
 UnflaggedApi: android.telephony.satellite.SatelliteTransmissionUpdateCallback#onReceiveDatagramStateChanged(int, int, int):
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index f9583d2..be2582f 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -3075,7 +3075,7 @@
     }
 
     /**
-     * Request to put the freeform activity into fullscreen. The requester has to be the top-most
+     * Request to put the activity into fullscreen. The requester must be pinned or the top-most
      * activity of the focused display which can be verified using
      * {@link #onTopResumedActivityChanged(boolean)}. The request should also be a response to a
      * user input. When getting fullscreen and receiving corresponding
diff --git a/core/java/android/app/ambient_context.aconfig b/core/java/android/app/ambient_context.aconfig
new file mode 100644
index 0000000..3f73da2
--- /dev/null
+++ b/core/java/android/app/ambient_context.aconfig
@@ -0,0 +1,8 @@
+package: "android.app"
+
+flag {
+  namespace: "biometrics_integration"
+  name: "ambient_heart_rate"
+  description: "Feature flag for adding heart rate api to ambient context."
+  bug: "318309481"
+}
diff --git a/core/java/android/app/ambientcontext/AmbientContextEvent.java b/core/java/android/app/ambientcontext/AmbientContextEvent.java
index b5c66ff..f94987e 100644
--- a/core/java/android/app/ambientcontext/AmbientContextEvent.java
+++ b/core/java/android/app/ambientcontext/AmbientContextEvent.java
@@ -16,7 +16,9 @@
 
 package android.app.ambientcontext;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.os.Parcelable;
@@ -68,6 +70,14 @@
     public static final int EVENT_BACK_DOUBLE_TAP = 3;
 
     /**
+     * The integer indicating a heart rate measurement was done.
+     *
+     * @see #getRatePerMinute
+     */
+    @Event @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE)
+    public static final int EVENT_HEART_RATE = 4;
+
+    /**
      * Integer indicating the start of wearable vendor defined events that can be detected.
      * These depend on the vendor implementation.
      */
@@ -79,12 +89,16 @@
      */
     public static final String KEY_VENDOR_WEARABLE_EVENT_NAME = "wearable_event_name";
 
+    /** Default value for the rate per minute data field. */
+    private static final int RATE_PER_MINUTE_UNKNOWN = -1;
+
     /** @hide */
     @IntDef(prefix = { "EVENT_" }, value = {
             EVENT_UNKNOWN,
             EVENT_COUGH,
             EVENT_SNORE,
             EVENT_BACK_DOUBLE_TAP,
+            EVENT_HEART_RATE,
             EVENT_VENDOR_WEARABLE_START,
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -170,6 +184,16 @@
         return new PersistableBundle();
     }
 
+    /**
+     * Rate per minute of the event during the start to end time.
+     *
+     * @return the rate per minute, or {@link #RATE_PER_MINUTE_UNKNOWN} if the rate is unknown.
+     */
+    private final @IntRange(from = -1) int mRatePerMinute;
+    private static int defaultRatePerMinute() {
+        return RATE_PER_MINUTE_UNKNOWN;
+    }
+
 
 
     // Code below generated by codegen v1.0.23.
@@ -179,6 +203,8 @@
     //
     // To regenerate run:
     // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/app/ambientcontext/AmbientContextEvent.java
+    // then manually add @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE) back to flagged
+    // APIs.
     //
     // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
     //   Settings > Editor > Code Style > Formatter Control
@@ -191,6 +217,7 @@
         EVENT_COUGH,
         EVENT_SNORE,
         EVENT_BACK_DOUBLE_TAP,
+        EVENT_HEART_RATE,
         EVENT_VENDOR_WEARABLE_START
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -209,6 +236,8 @@
                     return "EVENT_SNORE";
             case EVENT_BACK_DOUBLE_TAP:
                     return "EVENT_BACK_DOUBLE_TAP";
+            case EVENT_HEART_RATE:
+                    return "EVENT_HEART_RATE";
             case EVENT_VENDOR_WEARABLE_START:
                     return "EVENT_VENDOR_WEARABLE_START";
             default: return Integer.toHexString(value);
@@ -255,7 +284,8 @@
             @NonNull Instant endTime,
             @LevelValue int confidenceLevel,
             @LevelValue int densityLevel,
-            @NonNull PersistableBundle vendorData) {
+            @NonNull PersistableBundle vendorData,
+            @IntRange(from = -1) int ratePerMinute) {
         this.mEventType = eventType;
         com.android.internal.util.AnnotationValidations.validate(
                 EventCode.class, null, mEventType);
@@ -274,6 +304,10 @@
         this.mVendorData = vendorData;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mVendorData);
+        this.mRatePerMinute = ratePerMinute;
+        com.android.internal.util.AnnotationValidations.validate(
+                IntRange.class, null, mRatePerMinute,
+                "from", -1);
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -330,6 +364,17 @@
         return mVendorData;
     }
 
+    /**
+     * Rate per minute of the event during the start to end time.
+     *
+     * @return the rate per minute, or {@link #RATE_PER_MINUTE_UNKNOWN} if the rate is unknown.
+     */
+    @DataClass.Generated.Member
+    @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE)
+    public @IntRange(from = -1) int getRatePerMinute() {
+        return mRatePerMinute;
+    }
+
     @Override
     @DataClass.Generated.Member
     public String toString() {
@@ -342,7 +387,8 @@
                 "endTime = " + mEndTime + ", " +
                 "confidenceLevel = " + mConfidenceLevel + ", " +
                 "densityLevel = " + mDensityLevel + ", " +
-                "vendorData = " + mVendorData +
+                "vendorData = " + mVendorData + ", " +
+                "ratePerMinute = " + mRatePerMinute +
         " }";
     }
 
@@ -380,6 +426,7 @@
         dest.writeInt(mConfidenceLevel);
         dest.writeInt(mDensityLevel);
         dest.writeTypedObject(mVendorData, flags);
+        dest.writeInt(mRatePerMinute);
     }
 
     @Override
@@ -399,6 +446,7 @@
         int confidenceLevel = in.readInt();
         int densityLevel = in.readInt();
         PersistableBundle vendorData = (PersistableBundle) in.readTypedObject(PersistableBundle.CREATOR);
+        int ratePerMinute = in.readInt();
 
         this.mEventType = eventType;
         com.android.internal.util.AnnotationValidations.validate(
@@ -418,6 +466,10 @@
         this.mVendorData = vendorData;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mVendorData);
+        this.mRatePerMinute = ratePerMinute;
+        com.android.internal.util.AnnotationValidations.validate(
+                IntRange.class, null, mRatePerMinute,
+                "from", -1);
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -449,6 +501,7 @@
         private @LevelValue int mConfidenceLevel;
         private @LevelValue int mDensityLevel;
         private @NonNull PersistableBundle mVendorData;
+        private @IntRange(from = -1) int mRatePerMinute;
 
         private long mBuilderFieldsSet = 0L;
 
@@ -525,10 +578,22 @@
             return this;
         }
 
+        /**
+         * Rate per minute of the event during the start to end time.
+         */
+        @DataClass.Generated.Member
+        @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE)
+        public @NonNull Builder setRatePerMinute(@IntRange(from = -1) int value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x40;
+            mRatePerMinute = value;
+            return this;
+        }
+
         /** Builds the instance. This builder should not be touched after calling this! */
         public @NonNull AmbientContextEvent build() {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x40; // Mark builder used
+            mBuilderFieldsSet |= 0x80; // Mark builder used
 
             if ((mBuilderFieldsSet & 0x1) == 0) {
                 mEventType = defaultEventType();
@@ -548,18 +613,22 @@
             if ((mBuilderFieldsSet & 0x20) == 0) {
                 mVendorData = defaultVendorData();
             }
+            if ((mBuilderFieldsSet & 0x40) == 0) {
+                mRatePerMinute = defaultRatePerMinute();
+            }
             AmbientContextEvent o = new AmbientContextEvent(
                     mEventType,
                     mStartTime,
                     mEndTime,
                     mConfidenceLevel,
                     mDensityLevel,
-                    mVendorData);
+                    mVendorData,
+                    mRatePerMinute);
             return o;
         }
 
         private void checkNotUsed() {
-            if ((mBuilderFieldsSet & 0x40) != 0) {
+            if ((mBuilderFieldsSet & 0x80) != 0) {
                 throw new IllegalStateException(
                         "This Builder should not be reused. Use a new Builder instance instead");
             }
@@ -567,10 +636,10 @@
     }
 
     @DataClass.Generated(
-            time = 1671217108067L,
+            time = 1704895515931L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/app/ambientcontext/AmbientContextEvent.java",
-            inputSignatures = "public static final  int EVENT_UNKNOWN\npublic static final  int EVENT_COUGH\npublic static final  int EVENT_SNORE\npublic static final  int EVENT_BACK_DOUBLE_TAP\npublic static final  int EVENT_VENDOR_WEARABLE_START\npublic static final  java.lang.String KEY_VENDOR_WEARABLE_EVENT_NAME\npublic static final  int LEVEL_UNKNOWN\npublic static final  int LEVEL_LOW\npublic static final  int LEVEL_MEDIUM_LOW\npublic static final  int LEVEL_MEDIUM\npublic static final  int LEVEL_MEDIUM_HIGH\npublic static final  int LEVEL_HIGH\nprivate final @android.app.ambientcontext.AmbientContextEvent.EventCode int mEventType\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mStartTime\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mEndTime\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mConfidenceLevel\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mDensityLevel\nprivate final @android.annotation.NonNull android.os.PersistableBundle mVendorData\nprivate static  int defaultEventType()\nprivate static @android.annotation.NonNull java.time.Instant defaultStartTime()\nprivate static @android.annotation.NonNull java.time.Instant defaultEndTime()\nprivate static  int defaultConfidenceLevel()\nprivate static  int defaultDensityLevel()\nprivate static  android.os.PersistableBundle defaultVendorData()\nclass AmbientContextEvent extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=false, genHiddenConstDefs=true, genParcelable=true, genToString=true)")
+            inputSignatures = "public static final  int EVENT_UNKNOWN\npublic static final  int EVENT_COUGH\npublic static final  int EVENT_SNORE\npublic static final  int EVENT_BACK_DOUBLE_TAP\npublic static final @android.app.ambientcontext.AmbientContextEvent.Event @android.annotation.FlaggedApi int EVENT_HEART_RATE\npublic static final  int EVENT_VENDOR_WEARABLE_START\npublic static final  java.lang.String KEY_VENDOR_WEARABLE_EVENT_NAME\nprivate static final  int RATE_PER_MINUTE_UNKNOWN\npublic static final  int LEVEL_UNKNOWN\npublic static final  int LEVEL_LOW\npublic static final  int LEVEL_MEDIUM_LOW\npublic static final  int LEVEL_MEDIUM\npublic static final  int LEVEL_MEDIUM_HIGH\npublic static final  int LEVEL_HIGH\nprivate final @android.app.ambientcontext.AmbientContextEvent.EventCode int mEventType\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mStartTime\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mEndTime\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mConfidenceLevel\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mDensityLevel\nprivate final @android.annotation.NonNull android.os.PersistableBundle mVendorData\nprivate final @android.annotation.IntRange int mRatePerMinute\nprivate static  int defaultEventType()\nprivate static @android.annotation.NonNull java.time.Instant defaultStartTime()\nprivate static @android.annotation.NonNull java.time.Instant defaultEndTime()\nprivate static  int defaultConfidenceLevel()\nprivate static  int defaultDensityLevel()\nprivate static  android.os.PersistableBundle defaultVendorData()\nprivate static  int defaultRatePerMinute()\nclass AmbientContextEvent extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=false, genHiddenConstDefs=true, genParcelable=true, genToString=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java
index 6371871..a41cb1f 100644
--- a/core/java/android/app/backup/FullBackup.java
+++ b/core/java/android/app/backup/FullBackup.java
@@ -239,7 +239,7 @@
                 Log.e(TAG, "Unable to create/open file " + outFile.getPath(), e);
             }
 
-            byte[] buffer = new byte[32 * 1024];
+            byte[] buffer = new byte[64 * 1024];
             final long origSize = size;
             FileInputStream in = new FileInputStream(data.getFileDescriptor());
             while (size > 0) {
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index e9b94c9..c7a75ed 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -41,7 +41,6 @@
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.SQLException;
-import android.multiuser.Flags;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Binder;
@@ -147,7 +146,6 @@
     private boolean mExported;
     private boolean mNoPerms;
     private boolean mSingleUser;
-    private boolean mSystemUserOnly;
     private SparseBooleanArray mUsersRedirectedToOwnerForMedia = new SparseBooleanArray();
 
     private ThreadLocal<AttributionSource> mCallingAttributionSource;
@@ -379,9 +377,7 @@
                             != PermissionChecker.PERMISSION_GRANTED
                             && getContext().checkUriPermission(userUri, Binder.getCallingPid(),
                             callingUid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
-                            != PackageManager.PERMISSION_GRANTED
-                            && !deniedAccessSystemUserOnlyProvider(callingUserId,
-                            mSystemUserOnly)) {
+                            != PackageManager.PERMISSION_GRANTED) {
                         FrameworkStatsLog.write(GET_TYPE_ACCESSED_WITHOUT_PERMISSION,
                                 enumCheckUriPermission,
                                 callingUid, uri.getAuthority(), type);
@@ -869,10 +865,6 @@
     boolean checkUser(int pid, int uid, Context context) {
         final int callingUserId = UserHandle.getUserId(uid);
 
-        if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) {
-            return false;
-        }
-
         if (callingUserId == context.getUserId() || mSingleUser) {
             return true;
         }
@@ -995,9 +987,6 @@
 
         // last chance, check against any uri grants
         final int callingUserId = UserHandle.getUserId(uid);
-        if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) {
-            return PermissionChecker.PERMISSION_HARD_DENIED;
-        }
         final Uri userUri = (mSingleUser && !UserHandle.isSameUser(mMyUid, uid))
                 ? maybeAddUserId(uri, callingUserId) : uri;
         if (context.checkUriPermission(userUri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
@@ -2634,7 +2623,6 @@
                 setPathPermissions(info.pathPermissions);
                 mExported = info.exported;
                 mSingleUser = (info.flags & ProviderInfo.FLAG_SINGLE_USER) != 0;
-                mSystemUserOnly = (info.flags & ProviderInfo.FLAG_SYSTEM_USER_ONLY) != 0;
                 setAuthorities(info.authority);
             }
             if (Build.IS_DEBUGGABLE) {
@@ -2768,11 +2756,6 @@
         String auth = uri.getAuthority();
         if (!mSingleUser) {
             int userId = getUserIdFromAuthority(auth, UserHandle.USER_CURRENT);
-            if (deniedAccessSystemUserOnlyProvider(mContext.getUserId(),
-                    mSystemUserOnly)) {
-                throw new SecurityException("Trying to query a SYSTEM user only content"
-                        + " provider from user:" + mContext.getUserId());
-            }
             if (userId != UserHandle.USER_CURRENT
                     && userId != mContext.getUserId()
                     // Since userId specified in content uri, the provider userId would be
@@ -2946,16 +2929,4 @@
             Trace.traceBegin(traceTag, methodName + subInfo);
         }
     }
-    /**
-     * Return true if access to content provider is denied because it's a SYSTEM user only
-     * provider and the calling user is not the SYSTEM user.
-     *
-     * @param callingUserId UserId of the caller accessing the content provider.
-     * @param systemUserOnly true when the content provider is only available for the SYSTEM user.
-     */
-    private static boolean deniedAccessSystemUserOnlyProvider(int callingUserId,
-            boolean systemUserOnly) {
-        return Flags.enableSystemUserOnlyForServicesAndProviders()
-                && (callingUserId != UserHandle.USER_SYSTEM && systemUserOnly);
-    }
 }
diff --git a/core/java/android/content/OWNERS b/core/java/android/content/OWNERS
index 90c3d04..a37408b 100644
--- a/core/java/android/content/OWNERS
+++ b/core/java/android/content/OWNERS
@@ -4,6 +4,7 @@
 per-file *Content* = file:/services/core/java/com/android/server/am/OWNERS
 per-file *Sync* = file:/services/core/java/com/android/server/am/OWNERS
 per-file IntentFilter.java = file:/PACKAGE_MANAGER_OWNERS
+per-file UriRelativeFilter* = file:/PACKAGE_MANAGER_OWNERS
 per-file IntentFilter.java = file:/services/core/java/com/android/server/am/OWNERS
 per-file Intent.java = file:/INTENT_OWNERS
 per-file AutofillOptions* = file:/core/java/android/service/autofill/OWNERS
diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java
index de33fa8..9e553db 100644
--- a/core/java/android/content/pm/ProviderInfo.java
+++ b/core/java/android/content/pm/ProviderInfo.java
@@ -89,15 +89,6 @@
     public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000;
 
     /**
-     * Bit in {@link #flags}: If set, this provider will only be available
-     * for the system user.
-     * Set from the android.R.attr#systemUserOnly attribute.
-     * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY}
-     * @hide
-     */
-    public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY;
-
-    /**
      * Bit in {@link #flags}: If set, a single instance of the provider will
      * run for all users on the device.  Set from the
      * {@link android.R.attr#singleUser} attribute.
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index 2b378b1..ae46c027 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -101,14 +101,6 @@
     public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000;
 
     /**
-     * @hide Bit in {@link #flags}: If set, this service will only be available
-     * for the system user.
-     * Set from the android.R.attr#systemUserOnly attribute.
-     * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY}
-     */
-    public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY;
-
-    /**
      * Bit in {@link #flags}: If set, a single instance of the service will
      * run for all users on the device.  Set from the
      * {@link android.R.attr#singleUser} attribute.
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 1036865..c7797c7 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -70,11 +70,4 @@
     namespace: "profile_experiences"
     description: "Add support for Private Space in resolver sheet"
     bug: "307515485"
-}
-flag {
-    name: "enable_system_user_only_for_services_and_providers"
-    namespace: "multiuser"
-    description: "Enable systemUserOnly manifest attribute for services and providers."
-    bug: "302354856"
-    is_fixed_read_only: true
-}
+}
\ No newline at end of file
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index 3affb73..0cd1c8c 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -79,6 +79,8 @@
 import android.util.Range;
 import android.util.Size;
 
+import com.android.internal.camera.flags.Flags;
+
 import dalvik.annotation.optimization.FastNative;
 import dalvik.system.VMRuntime;
 
@@ -1795,49 +1797,57 @@
             return false;
         }
 
-        long[] tsArray = new long[samples.length];
-        float[] intrinsicsArray = new float[samples.length * 5];
-        for (int i = 0; i < samples.length; i++) {
-            tsArray[i] = samples[i].getTimestamp();
-            System.arraycopy(samples[i].getLensIntrinsics(), 0, intrinsicsArray, 5*i, 5);
+        if (Flags.concertMode()) {
+            long[] tsArray = new long[samples.length];
+            float[] intrinsicsArray = new float[samples.length * 5];
+            for (int i = 0; i < samples.length; i++) {
+                tsArray[i] = samples[i].getTimestampNanos();
+                System.arraycopy(samples[i].getLensIntrinsics(), 0, intrinsicsArray, 5 * i, 5);
 
+            }
+            setBase(CaptureResult.STATISTICS_LENS_INTRINSIC_SAMPLES, intrinsicsArray);
+            setBase(CaptureResult.STATISTICS_LENS_INTRINSIC_TIMESTAMPS, tsArray);
+
+            return true;
+        } else {
+            return false;
         }
-        setBase(CaptureResult.STATISTICS_LENS_INTRINSIC_SAMPLES, intrinsicsArray);
-        setBase(CaptureResult.STATISTICS_LENS_INTRINSIC_TIMESTAMPS, tsArray);
-
-        return true;
     }
 
     private LensIntrinsicsSample[] getLensIntrinsicSamples() {
-        long[] timestamps = getBase(CaptureResult.STATISTICS_LENS_INTRINSIC_TIMESTAMPS);
-        float[] intrinsics = getBase(CaptureResult.STATISTICS_LENS_INTRINSIC_SAMPLES);
+        if (Flags.concertMode()) {
+            long[] timestamps = getBase(CaptureResult.STATISTICS_LENS_INTRINSIC_TIMESTAMPS);
+            float[] intrinsics = getBase(CaptureResult.STATISTICS_LENS_INTRINSIC_SAMPLES);
 
-        if (timestamps == null) {
-            if (intrinsics != null) {
-                throw new AssertionError("timestamps is null but intrinsics is not");
+            if (timestamps == null) {
+                if (intrinsics != null) {
+                    throw new AssertionError("timestamps is null but intrinsics is not");
+                }
+
+                return null;
             }
 
+            if (intrinsics == null) {
+                throw new AssertionError("timestamps is not null but intrinsics is");
+            } else if ((intrinsics.length % 5) != 0) {
+                throw new AssertionError("intrinsics are not multiple of 5");
+            }
+
+            if ((intrinsics.length / 5) != timestamps.length) {
+                throw new AssertionError(String.format(
+                        "timestamps has %d entries but intrinsics has %d", timestamps.length,
+                        intrinsics.length / 5));
+            }
+
+            LensIntrinsicsSample[] samples = new LensIntrinsicsSample[timestamps.length];
+            for (int i = 0; i < timestamps.length; i++) {
+                float[] currentIntrinsic = Arrays.copyOfRange(intrinsics, 5 * i, 5 * i + 5);
+                samples[i] = new LensIntrinsicsSample(timestamps[i], currentIntrinsic);
+            }
+            return samples;
+        } else {
             return null;
         }
-
-        if (intrinsics == null) {
-            throw new AssertionError("timestamps is not null but intrinsics is");
-        } else if((intrinsics.length % 5) != 0) {
-            throw new AssertionError("intrinsics are not multiple of 5");
-        }
-
-        if ((intrinsics.length / 5) != timestamps.length) {
-            throw new AssertionError(String.format(
-                    "timestamps has %d entries but intrinsics has %d", timestamps.length,
-                    intrinsics.length / 5));
-        }
-
-        LensIntrinsicsSample[] samples = new LensIntrinsicsSample[timestamps.length];
-        for (int i = 0; i < timestamps.length; i++) {
-            float[] currentIntrinsic = Arrays.copyOfRange(intrinsics, 5*i, 5*i + 5);
-            samples[i] = new LensIntrinsicsSample(timestamps[i], currentIntrinsic);
-        }
-        return samples;
     }
 
     private Capability[] getExtendedSceneModeCapabilities() {
diff --git a/core/java/android/hardware/camera2/params/LensIntrinsicsSample.java b/core/java/android/hardware/camera2/params/LensIntrinsicsSample.java
index 575cbfa..9a4ec5c 100644
--- a/core/java/android/hardware/camera2/params/LensIntrinsicsSample.java
+++ b/core/java/android/hardware/camera2/params/LensIntrinsicsSample.java
@@ -37,16 +37,18 @@
      * Create a new {@link LensIntrinsicsSample}.
      *
      * <p>{@link LensIntrinsicsSample} contains the timestamp and the
-     * {@link CaptureResult#LENS_INTRINSIC_CALIBRATION} sample.
+     * {@link CaptureResult#LENS_INTRINSIC_CALIBRATION} sample.</p>
      *
-     * @param timestamp timestamp of the lens intrinsics sample.
-     * @param lensIntrinsics the lens intrinsic calibration for the sample.
+     * @param timestampNs timestamp in nanoseconds of the lens intrinsics sample. This uses the
+     *                  same time basis as {@link CaptureResult#SENSOR_TIMESTAMP}.
+     * @param lensIntrinsics the lens {@link CaptureResult#LENS_INTRINSIC_CALIBRATION intrinsic}
+     *                      calibration for the sample.
      *
      * @throws IllegalArgumentException if lensIntrinsics length is different from 5
      */
     @FlaggedApi(Flags.FLAG_CONCERT_MODE)
-    public LensIntrinsicsSample(final long timestamp, @NonNull final float[] lensIntrinsics) {
-        mTimestampNs = timestamp;
+    public LensIntrinsicsSample(final long timestampNs, @NonNull final float[] lensIntrinsics) {
+        mTimestampNs = timestampNs;
         Preconditions.checkArgument(lensIntrinsics.length == 5);
         mLensIntrinsics = lensIntrinsics;
     }
@@ -54,18 +56,18 @@
     /**
      * Get the timestamp in nanoseconds.
      *
-     *<p>The timestamps are in the same timebase as and comparable to
+     *<p>The timestamps are in the same time basis as and comparable to
      *{@link CaptureResult#SENSOR_TIMESTAMP android.sensor.timestamp}.</p>
      *
      * @return a long value (guaranteed to be finite)
      */
     @FlaggedApi(Flags.FLAG_CONCERT_MODE)
-    public long getTimestamp() {
+    public long getTimestampNanos() {
         return mTimestampNs;
     }
 
     /**
-     * Get the lens intrinsics calibration
+     * Get the lens {@link CaptureResult#LENS_INTRINSIC_CALIBRATION intrinsics} calibration
      *
      * @return a floating point value (guaranteed to be finite)
      * @see CaptureResult#LENS_INTRINSIC_CALIBRATION
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 134a510..8f0e0c9 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -1831,15 +1831,6 @@
         String KEY_POWER_THROTTLING_DATA = "power_throttling_data";
 
         /**
-         * Key for new power controller feature flag. If enabled new DisplayPowerController will
-         * be used.
-         * Read value via {@link android.provider.DeviceConfig#getBoolean(String, String, boolean)}
-         * with {@link android.provider.DeviceConfig#NAMESPACE_DISPLAY_MANAGER} as the namespace.
-         * @hide
-         */
-        String KEY_NEW_POWER_CONTROLLER = "use_newly_structured_display_power_controller";
-
-        /**
          * Key for normal brightness mode controller feature flag.
          * It enables NormalBrightnessModeController.
          * Read value via {@link android.provider.DeviceConfig#getBoolean(String, String, boolean)}
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 70cf973..561db9c 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -102,6 +102,24 @@
     public static final String VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY =
             "vcn_network_selection_wifi_exit_rssi_threshold";
 
+    /**
+     * Key for the interval to poll IpSecTransformState for packet loss monitoring
+     *
+     * @hide
+     */
+    @NonNull
+    public static final String VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY =
+            "vcn_network_selection_poll_ipsec_state_interval_seconds";
+
+    /**
+     * Key for the threshold of IPSec packet loss rate
+     *
+     * @hide
+     */
+    @NonNull
+    public static final String VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY =
+            "vcn_network_selection_ipsec_packet_loss_percent_threshold";
+
     // TODO: Add separate signal strength thresholds for 2.4 GHz and 5GHz
 
     /**
@@ -115,6 +133,20 @@
             "vcn_restricted_transports";
 
     /**
+     * Key for number of seconds to wait before entering safe mode
+     *
+     * <p>A VcnGatewayConnection will enter safe mode when it takes over the configured timeout to
+     * enter {@link ConnectedState}.
+     *
+     * <p>Defaults to 30, unless overridden by carrier config
+     *
+     * @hide
+     */
+    @NonNull
+    public static final String VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY =
+            "vcn_safe_mode_timeout_seconds_key";
+
+    /**
      * Key for maximum number of parallel SAs for tunnel aggregation
      *
      * <p>If set to a value > 1, multiple tunnels will be set up, and inbound traffic will be
@@ -134,7 +166,10 @@
             new String[] {
                 VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
                 VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
+                VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY,
+                VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY,
                 VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY,
+                VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY,
                 VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY,
             };
 
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
index 6956916..7afd721 100644
--- a/core/java/android/net/vcn/flags.aconfig
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -5,4 +5,18 @@
     namespace: "vcn"
     description: "Feature flag for safe mode configurability"
     bug: "276358140"
+}
+
+flag {
+    name: "safe_mode_timeout_config"
+    namespace: "vcn"
+    description: "Feature flag for adjustable safe mode timeout"
+    bug: "317406085"
+}
+
+flag{
+    name: "network_metric_monitor"
+    namespace: "vcn"
+    description: "Feature flag for enabling network metric monitor"
+    bug: "282996138"
 }
\ No newline at end of file
diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java
index c60f949..fbec518 100644
--- a/core/java/android/os/MessageQueue.java
+++ b/core/java/android/os/MessageQueue.java
@@ -57,6 +57,7 @@
 
     @UnsupportedAppUsage
     Message mMessages;
+    private Message mLast;
     @UnsupportedAppUsage
     private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
     private SparseArray<FileDescriptorRecord> mFileDescriptorRecords;
@@ -66,6 +67,10 @@
     // Indicates whether next() is blocked waiting in pollOnce() with a non-zero timeout.
     private boolean mBlocked;
 
+    // Tracks the number of async message. We use this in enqueueMessage() to avoid searching the
+    // queue for async messages when inserting a message at the tail.
+    private int mAsyncMessageCount;
+
     // The next barrier token.
     // Barriers are indicated by messages with a null target whose arg1 field carries the token.
     @UnsupportedAppUsage
@@ -364,12 +369,21 @@
                         mBlocked = false;
                         if (prevMsg != null) {
                             prevMsg.next = msg.next;
+                            if (prevMsg.next == null) {
+                                mLast = prevMsg;
+                            }
                         } else {
                             mMessages = msg.next;
+                            if (msg.next == null) {
+                                mLast = null;
+                            }
                         }
                         msg.next = null;
                         if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                         msg.markInUse();
+                        if (msg.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         return msg;
                     }
                 } else {
@@ -492,6 +506,14 @@
             msg.when = when;
             msg.arg1 = token;
 
+            if (Flags.messageQueueTailTracking() && mLast != null && mLast.when <= when) {
+                /* Message goes to tail of list */
+                mLast.next = msg;
+                mLast = msg;
+                msg.next = null;
+                return token;
+            }
+
             Message prev = null;
             Message p = mMessages;
             if (when != 0) {
@@ -500,6 +522,12 @@
                     p = p.next;
                 }
             }
+
+            if (p == null) {
+                /* We reached the tail of the list, or list is empty. */
+                mLast = msg;
+            }
+
             if (prev != null) { // invariant: p == prev.next
                 msg.next = p;
                 prev.next = msg;
@@ -540,9 +568,15 @@
             final boolean needWake;
             if (prev != null) {
                 prev.next = p.next;
+                if (prev.next == null) {
+                    mLast = prev;
+                }
                 needWake = false;
             } else {
                 mMessages = p.next;
+                if (mMessages == null) {
+                    mLast = null;
+                }
                 needWake = mMessages == null || mMessages.target != null;
             }
             p.recycleUnchecked();
@@ -582,24 +616,77 @@
                 msg.next = p;
                 mMessages = msg;
                 needWake = mBlocked;
-            } else {
-                // Inserted within the middle of the queue.  Usually we don't have to wake
-                // up the event queue unless there is a barrier at the head of the queue
-                // and the message is the earliest asynchronous message in the queue.
-                needWake = mBlocked && p.target == null && msg.isAsynchronous();
-                Message prev;
-                for (;;) {
-                    prev = p;
-                    p = p.next;
-                    if (p == null || when < p.when) {
-                        break;
-                    }
-                    if (needWake && p.isAsynchronous()) {
-                        needWake = false;
-                    }
+                if (p == null) {
+                    mLast = mMessages;
                 }
-                msg.next = p; // invariant: p == prev.next
-                prev.next = msg;
+            } else {
+                // Message is to be inserted at tail or middle of queue. Usually we don't have to
+                // wake up the event queue unless there is a barrier at the head of the queue and
+                // the message is the earliest asynchronous message in the queue.
+                //
+                // For readability, we split this portion of the function into two blocks based on
+                // whether tail tracking is enabled. This has a minor implication for the case
+                // where tail tracking is disabled. See the comment below.
+                if (Flags.messageQueueTailTracking()) {
+                    needWake = mBlocked && p.target == null && msg.isAsynchronous()
+                        && mAsyncMessageCount == 0;
+                    if (when >= mLast.when) {
+                        msg.next = null;
+                        mLast.next = msg;
+                        mLast = msg;
+                    } else {
+                        // Inserted within the middle of the queue.
+                        Message prev;
+                        for (;;) {
+                            prev = p;
+                            p = p.next;
+                            if (p == null || when < p.when) {
+                                break;
+                            }
+                        }
+                        if (p == null) {
+                            /* Inserting at tail of queue */
+                            mLast = msg;
+                        }
+                        msg.next = p; // invariant: p == prev.next
+                        prev.next = msg;
+                    }
+                } else {
+                    needWake = mBlocked && p.target == null && msg.isAsynchronous();
+                    Message prev;
+                    for (;;) {
+                        prev = p;
+                        p = p.next;
+                        if (p == null || when < p.when) {
+                            break;
+                        }
+                        if (needWake && p.isAsynchronous()) {
+                            needWake = false;
+                        }
+                    }
+                    msg.next = p; // invariant: p == prev.next
+                    prev.next = msg;
+
+                    /*
+                     * If this block is executing then we have a build without tail tracking -
+                     * specifically: Flags.messageQueueTailTracking() == false. This is determined
+                     * at build time so the flag won't change on us during runtime.
+                     *
+                     * Since we don't want to pepper the code with extra checks, we only check
+                     * for tail tracking when we might use mLast. Otherwise, we continue to update
+                     * mLast as the tail of the list.
+                     *
+                     * In this case however we are not maintaining mLast correctly. Since we never
+                     * use it, this is fine. However, we run the risk of leaking a reference.
+                     * So set mLast to null in this case to avoid any Message leaks. The other
+                     * sites will never use the value so we are safe against null pointer derefs.
+                     */
+                    mLast = null;
+                }
+            }
+
+            if (msg.isAsynchronous()) {
+                mAsyncMessageCount++;
             }
 
             // We can assume mPtr != 0 because mQuitting is false.
@@ -692,10 +779,17 @@
                    && (object == null || p.obj == object)) {
                 Message n = p.next;
                 mMessages = n;
+                if (p.isAsynchronous()) {
+                    mAsyncMessageCount--;
+                }
                 p.recycleUnchecked();
                 p = n;
             }
 
+            if (p == null) {
+                mLast = mMessages;
+            }
+
             // Remove all messages after front.
             while (p != null) {
                 Message n = p.next;
@@ -703,8 +797,14 @@
                     if (n.target == h && n.what == what
                         && (object == null || n.obj == object)) {
                         Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         n.recycleUnchecked();
                         p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
                         continue;
                     }
                 }
@@ -726,10 +826,17 @@
                    && (object == null || object.equals(p.obj))) {
                 Message n = p.next;
                 mMessages = n;
+                if (p.isAsynchronous()) {
+                    mAsyncMessageCount--;
+                }
                 p.recycleUnchecked();
                 p = n;
             }
 
+            if (p == null) {
+                mLast = mMessages;
+            }
+
             // Remove all messages after front.
             while (p != null) {
                 Message n = p.next;
@@ -737,8 +844,14 @@
                     if (n.target == h && n.what == what
                         && (object == null || object.equals(n.obj))) {
                         Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         n.recycleUnchecked();
                         p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
                         continue;
                     }
                 }
@@ -760,10 +873,17 @@
                    && (object == null || p.obj == object)) {
                 Message n = p.next;
                 mMessages = n;
+                if (p.isAsynchronous()) {
+                    mAsyncMessageCount--;
+                }
                 p.recycleUnchecked();
                 p = n;
             }
 
+            if (p == null) {
+                mLast = mMessages;
+            }
+
             // Remove all messages after front.
             while (p != null) {
                 Message n = p.next;
@@ -771,8 +891,14 @@
                     if (n.target == h && n.callback == r
                         && (object == null || n.obj == object)) {
                         Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         n.recycleUnchecked();
                         p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
                         continue;
                     }
                 }
@@ -794,10 +920,17 @@
                    && (object == null || object.equals(p.obj))) {
                 Message n = p.next;
                 mMessages = n;
+                if (p.isAsynchronous()) {
+                    mAsyncMessageCount--;
+                }
                 p.recycleUnchecked();
                 p = n;
             }
 
+            if (p == null) {
+                mLast = mMessages;
+            }
+
             // Remove all messages after front.
             while (p != null) {
                 Message n = p.next;
@@ -805,8 +938,14 @@
                     if (n.target == h && n.callback == r
                         && (object == null || object.equals(n.obj))) {
                         Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         n.recycleUnchecked();
                         p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
                         continue;
                     }
                 }
@@ -829,18 +968,31 @@
                     && (object == null || p.obj == object)) {
                 Message n = p.next;
                 mMessages = n;
+                if (p.isAsynchronous()) {
+                    mAsyncMessageCount--;
+                }
                 p.recycleUnchecked();
                 p = n;
             }
 
+            if (p == null) {
+                mLast = mMessages;
+            }
+
             // Remove all messages after front.
             while (p != null) {
                 Message n = p.next;
                 if (n != null) {
                     if (n.target == h && (object == null || n.obj == object)) {
                         Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         n.recycleUnchecked();
                         p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
                         continue;
                     }
                 }
@@ -862,18 +1014,31 @@
                     && (object == null || object.equals(p.obj))) {
                 Message n = p.next;
                 mMessages = n;
+                if (p.isAsynchronous()) {
+                    mAsyncMessageCount--;
+                }
                 p.recycleUnchecked();
                 p = n;
             }
 
+            if (p == null) {
+                mLast = mMessages;
+            }
+
             // Remove all messages after front.
             while (p != null) {
                 Message n = p.next;
                 if (n != null) {
                     if (n.target == h && (object == null || object.equals(n.obj))) {
                         Message nn = n.next;
+                        if (n.isAsynchronous()) {
+                            mAsyncMessageCount--;
+                        }
                         n.recycleUnchecked();
                         p.next = nn;
+                        if (p.next == null) {
+                            mLast = p;
+                        }
                         continue;
                     }
                 }
@@ -890,6 +1055,8 @@
             p = n;
         }
         mMessages = null;
+        mLast = null;
+        mAsyncMessageCount = 0;
     }
 
     private void removeAllFutureMessagesLocked() {
@@ -911,9 +1078,14 @@
                     p = n;
                 }
                 p.next = null;
+                mLast = p;
+
                 do {
                     p = n;
                     n = p.next;
+                    if (p.isAsynchronous()) {
+                        mAsyncMessageCount--;
+                    }
                     p.recycleUnchecked();
                 } while (n != null);
             }
diff --git a/core/java/android/os/PowerMonitorReadings.java b/core/java/android/os/PowerMonitorReadings.java
index bb677d5..a0ab066 100644
--- a/core/java/android/os/PowerMonitorReadings.java
+++ b/core/java/android/os/PowerMonitorReadings.java
@@ -71,7 +71,7 @@
      */
     @FlaggedApi("com.android.server.power.optimization.power_monitor_api")
     @ElapsedRealtimeLong
-    public long getTimestamp(@NonNull PowerMonitor powerMonitor) {
+    public long getTimestampMillis(@NonNull PowerMonitor powerMonitor) {
         int offset = Arrays.binarySearch(mPowerMonitors, powerMonitor, POWER_MONITOR_COMPARATOR);
         if (offset >= 0) {
             return mTimestampsMs[offset];
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index d646146..0db90bf 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -98,3 +98,11 @@
     description: "Guards StrictMode APIs for detecting restricted network access."
     bug: "317250784"
 }
+
+flag {
+    name: "message_queue_tail_tracking"
+    namespace: "system_performance"
+    description: "track tail of message queue."
+    bug: "305311707"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java
index 2d53341..322a8e6 100644
--- a/core/java/android/os/health/SystemHealthManager.java
+++ b/core/java/android/os/health/SystemHealthManager.java
@@ -25,8 +25,8 @@
 import android.os.BatteryStats;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IPowerStatsService;
+import android.os.OutcomeReceiver;
 import android.os.PowerMonitor;
 import android.os.PowerMonitorReadings;
 import android.os.Process;
@@ -39,6 +39,7 @@
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.List;
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
@@ -161,12 +162,12 @@
      * (on-device power rail monitor) rails and modeled energy consumers.  If ODPM is unsupported
      * on this device this method delivers an empty list.
      *
-     * @param handler  optional Handler to deliver the callback. If not supplied, the callback
+     * @param executor optional Handler to deliver the callback. If not supplied, the callback
      *                 may be invoked on an arbitrary thread.
      * @param onResult callback for the result
      */
     @FlaggedApi("com.android.server.power.optimization.power_monitor_api")
-    public void getSupportedPowerMonitors(@Nullable Handler handler,
+    public void getSupportedPowerMonitors(@Nullable Executor executor,
             @NonNull Consumer<List<PowerMonitor>> onResult) {
         final List<PowerMonitor> result;
         synchronized (mPowerMonitorsLock) {
@@ -180,15 +181,15 @@
             }
         }
         if (result != null) {
-            if (handler != null) {
-                handler.post(() -> onResult.accept(result));
+            if (executor != null) {
+                executor.execute(() -> onResult.accept(result));
             } else {
                 onResult.accept(result);
             }
             return;
         }
         try {
-            mPowerStats.getSupportedPowerMonitors(new ResultReceiver(handler) {
+            mPowerStats.getSupportedPowerMonitors(new ResultReceiver(null) {
                 @Override
                 protected void onReceiveResult(int resultCode, Bundle resultData) {
                     PowerMonitor[] array = resultData.getParcelableArray(
@@ -197,7 +198,11 @@
                     synchronized (mPowerMonitorsLock) {
                         mPowerMonitorsInfo = result;
                     }
-                    onResult.accept(result);
+                    if (executor != null) {
+                        executor.execute(()-> onResult.accept(result));
+                    } else {
+                        onResult.accept(result);
+                    }
                 }
             });
         } catch (RemoteException e) {
@@ -213,17 +218,22 @@
      * monitors.
      *
      * @param powerMonitors power monitors to be retrieved.
-     * @param handler       optional Handler to deliver the callbacks. If not supplied, the callback
-     *                      may be invoked on an arbitrary thread.
-     * @param onSuccess     callback for the result
-     * @param onError       callback invoked in case of an error
+     * @param executor      optional Executor to deliver the callbacks. If not supplied, the
+     *                      callback may be invoked on an arbitrary thread.
+     * @param onResult      callback for the result
      */
     @FlaggedApi("com.android.server.power.optimization.power_monitor_api")
     public void getPowerMonitorReadings(@NonNull List<PowerMonitor> powerMonitors,
-            @Nullable Handler handler, @NonNull Consumer<PowerMonitorReadings> onSuccess,
-            @NonNull Consumer<RuntimeException> onError) {
+            @Nullable Executor executor,
+            @NonNull OutcomeReceiver<PowerMonitorReadings, RuntimeException> onResult) {
         if (mPowerStats == null) {
-            onError.accept(new IllegalArgumentException("Unsupported power monitor"));
+            IllegalArgumentException error =
+                    new IllegalArgumentException("Unsupported power monitor");
+            if (executor != null) {
+                executor.execute(() -> onResult.onError(error));
+            } else {
+                onResult.onError(error);
+            }
             return;
         }
 
@@ -235,18 +245,31 @@
             indices[i] = powerMonitorsArray[i].index;
         }
         try {
-            mPowerStats.getPowerMonitorReadings(indices, new ResultReceiver(handler) {
+            mPowerStats.getPowerMonitorReadings(indices, new ResultReceiver(null) {
                 @Override
                 protected void onReceiveResult(int resultCode, Bundle resultData) {
                     if (resultCode == IPowerStatsService.RESULT_SUCCESS) {
-                        onSuccess.accept(new PowerMonitorReadings(powerMonitorsArray,
+                        PowerMonitorReadings result = new PowerMonitorReadings(powerMonitorsArray,
                                 resultData.getLongArray(IPowerStatsService.KEY_ENERGY),
-                                resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS)));
-                    } else if (resultCode == IPowerStatsService.RESULT_UNSUPPORTED_POWER_MONITOR) {
-                        onError.accept(new IllegalArgumentException("Unsupported power monitor"));
+                                resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS));
+                        if (executor != null) {
+                            executor.execute(() -> onResult.onResult(result));
+                        } else {
+                            onResult.onResult(result);
+                        }
                     } else {
-                        onError.accept(new IllegalStateException(
-                                "Unrecognized result code " + resultCode));
+                        RuntimeException error;
+                        if (resultCode == IPowerStatsService.RESULT_UNSUPPORTED_POWER_MONITOR) {
+                            error = new IllegalArgumentException("Unsupported power monitor");
+                        } else {
+                            error = new IllegalStateException(
+                                    "Unrecognized result code " + resultCode);
+                        }
+                        if (executor != null) {
+                            executor.execute(() -> onResult.onError(error));
+                        } else {
+                            onResult.onError(error);
+                        }
                     }
                 }
             });
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 7d9c0a3..2d657c2 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -445,6 +445,19 @@
     }
 
     /**
+     * Retrieves the current {@link android.app.Activity} associated with the dream.
+     * This method behaves similarly to calling {@link android.app.Activity#getActivity()}.
+     *
+     * @return The current activity, or null if the dream is not associated with an activity
+     * or not started.
+     *
+     * @hide
+     */
+    public Activity getActivity() {
+        return mActivity;
+    }
+
+    /**
      * Inflates a layout resource and set it to be the content view for this Dream.
      * Behaves similarly to {@link android.app.Activity#setContentView(int)}.
      *
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java
index 4c81888..a6d3bb4 100644
--- a/core/java/android/text/BoringLayout.java
+++ b/core/java/android/text/BoringLayout.java
@@ -454,7 +454,7 @@
             line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
                     Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,
                     mEllipsizedStart, mEllipsizedStart + mEllipsizedCount, useFallbackLineSpacing);
-            mMax = (int) Math.ceil(line.metrics(null, null, false));
+            mMax = (int) Math.ceil(line.metrics(null, null, false, null));
             TextLine.recycle(line);
         }
 
@@ -603,7 +603,7 @@
                 0 /* ellipsisStart, 0 since text has not been ellipsized at this point */,
                 0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */,
                 useFallbackLineSpacing);
-        fm.width = (int) Math.ceil(line.metrics(fm, fm.mDrawingBounds, false));
+        fm.width = (int) Math.ceil(line.metrics(fm, fm.mDrawingBounds, false, null));
         TextLine.recycle(line);
 
         return fm;
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index c9906cc..eca848a 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -18,6 +18,7 @@
 
 import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
 import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
+import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION;
 
 import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
@@ -50,8 +51,10 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.text.BreakIterator;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 
 /**
  * A base class that manages text layout in visual elements on
@@ -669,7 +672,8 @@
             int start = previousLineEnd;
             previousLineEnd = getLineStart(lineNum + 1);
             final boolean justify = isJustificationRequired(lineNum);
-            int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
+            int end = getLineVisibleEnd(lineNum, start, previousLineEnd,
+                    true /* trailingSpaceAtLastLineIsVisible */);
             paint.setStartHyphenEdit(getStartHyphenEdit(lineNum));
             paint.setEndHyphenEdit(getEndHyphenEdit(lineNum));
 
@@ -1056,7 +1060,7 @@
             if (isJustificationRequired(line)) {
                 tl.justify(getJustifyWidth(line));
             }
-            tl.metrics(null, rectF, false);
+            tl.metrics(null, rectF, false, null);
 
             float lineLeft = rectF.left;
             float lineRight = rectF.right;
@@ -1456,7 +1460,7 @@
         tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops,
                 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
                 isFallbackLineSpacingEnabled());
-        float wid = tl.measure(offset - start, trailing, null, null);
+        float wid = tl.measure(offset - start, trailing, null, null, null);
         TextLine.recycle(tl);
 
         if (clamped && wid > mWidth) {
@@ -1792,12 +1796,69 @@
         if (isJustificationRequired(line)) {
             tl.justify(getJustifyWidth(line));
         }
-        final float width = tl.metrics(null, null, mUseBoundsForWidth);
+        final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
         TextLine.recycle(tl);
         return width;
     }
 
     /**
+     * Returns the number of letter spacing unit in the line.
+     *
+     * <p>
+     * This API returns a number of letters that is a target of letter spacing. The letter spacing
+     * won't be added to the middle of the characters that are needed to be treated as a single,
+     * e.g., ligatured or conjunct form. Note that this value is different from the number of]
+     * grapheme clusters that is calculated by {@link BreakIterator#getCharacterInstance(Locale)}.
+     * For example, if the "fi" is ligatured, the ligatured form is treated as single uni and letter
+     * spacing is not added, but it has two separate grapheme cluster.
+     *
+     * <p>
+     * This value is used for calculating the letter spacing amount for the justification because
+     * the letter spacing is applied between clusters. For example, if extra {@code W} pixels needed
+     * to be filled by letter spacing, the amount of letter spacing to be applied is
+     * {@code W}/(letter spacing unit count - 1) px.
+     *
+     * @param line the index of the line
+     * @param includeTrailingWhitespace whether to include trailing whitespace
+     * @return the number of cluster count in the line.
+     */
+    @IntRange(from = 0)
+    @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+    public int getLineLetterSpacingUnitCount(@IntRange(from = 0) int line,
+            boolean includeTrailingWhitespace) {
+        final int start = getLineStart(line);
+        final int end = includeTrailingWhitespace ? getLineEnd(line)
+                : getLineVisibleEnd(line, getLineStart(line), getLineStart(line + 1),
+                        false  // trailingSpaceAtLastLineIsVisible: Treating trailing whitespaces at
+                               // the last line as a invisible chars for single line justification.
+                );
+
+        final Directions directions = getLineDirections(line);
+        // Returned directions can actually be null
+        if (directions == null) {
+            return 0;
+        }
+        final int dir = getParagraphDirection(line);
+
+        final TextLine tl = TextLine.obtain();
+        final TextPaint paint = mWorkPaint;
+        paint.set(mPaint);
+        paint.setStartHyphenEdit(getStartHyphenEdit(line));
+        paint.setEndHyphenEdit(getEndHyphenEdit(line));
+        tl.set(paint, mText, start, end, dir, directions,
+                false, null, // tab width is not used for cluster counting.
+                getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
+                isFallbackLineSpacingEnabled());
+        if (mLineInfo == null) {
+            mLineInfo = new TextLine.LineInfo();
+        }
+        mLineInfo.setClusterCount(0);
+        tl.metrics(null, null, mUseBoundsForWidth, mLineInfo);
+        TextLine.recycle(tl);
+        return mLineInfo.getClusterCount();
+    }
+
+    /**
      * Returns the signed horizontal extent of the specified line, excluding
      * leading margin.  If full is false, excludes trailing whitespace.
      * @param line the index of the line
@@ -1823,7 +1884,7 @@
         if (isJustificationRequired(line)) {
             tl.justify(getJustifyWidth(line));
         }
-        final float width = tl.metrics(null, null, mUseBoundsForWidth);
+        final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
         TextLine.recycle(tl);
         return width;
     }
@@ -2432,14 +2493,21 @@
      * is not counted) on the specified line.
      */
     public int getLineVisibleEnd(int line) {
-        return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
+        return getLineVisibleEnd(line, getLineStart(line), getLineStart(line + 1),
+                true /* trailingSpaceAtLastLineIsVisible */);
     }
 
-    private int getLineVisibleEnd(int line, int start, int end) {
+    private int getLineVisibleEnd(int line, int start, int end,
+            boolean trailingSpaceAtLastLineIsVisible) {
         CharSequence text = mText;
         char ch;
-        if (line == getLineCount() - 1) {
-            return end;
+
+        // Historically, trailing spaces at the last line is counted as visible. However, this
+        // doesn't work well for justification.
+        if (trailingSpaceAtLastLineIsVisible) {
+            if (line == getLineCount() - 1) {
+                return end;
+            }
         }
 
         for (; end > start; end--) {
@@ -2939,7 +3007,7 @@
             tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops,
                     0 /* ellipsisStart */, 0 /* ellipsisEnd */,
                     false /* use fallback line spacing. unused */);
-            return margin + Math.abs(tl.metrics(null, null, useBoundsForWidth));
+            return margin + Math.abs(tl.metrics(null, null, useBoundsForWidth, null));
         } finally {
             TextLine.recycle(tl);
             if (mt != null) {
@@ -3337,6 +3405,8 @@
     private boolean mUseBoundsForWidth;
     private @Nullable Paint.FontMetrics mMinimumFontMetrics;
 
+    private TextLine.LineInfo mLineInfo = null;
+
     /** @hide */
     @IntDef(prefix = { "DIR_" }, value = {
             DIR_LEFT_TO_RIGHT,
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index f9abec0..135935c 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -76,6 +76,21 @@
     private RectF mTmpRectForPaintAPI;
     private Rect mTmpRectForPrecompute;
 
+    // Recycling object for Paint APIs. Do not use outside getRunAdvances method.
+    private Paint.RunInfo mRunInfo;
+
+    public static final class LineInfo {
+        private int mClusterCount;
+
+        public int getClusterCount() {
+            return mClusterCount;
+        }
+
+        public void setClusterCount(int clusterCount) {
+            mClusterCount = clusterCount;
+        }
+    };
+
     private boolean mUseFallbackExtent = false;
 
     // The start and end of a potentially existing ellipsis on this text line.
@@ -270,7 +285,7 @@
             // width.
             return;
         }
-        final float width = Math.abs(measure(end, false, null, null));
+        final float width = Math.abs(measure(end, false, null, null, null));
         mAddedWidthForJustify = (justifyWidth - width) / spaces;
         mIsJustifying = true;
     }
@@ -315,10 +330,12 @@
      * @param drawBounds output parameter for drawing bounding box. optional.
      * @param returnDrawWidth true for returning width of the bounding box, false for returning
      *                       total advances.
+     * @param lineInfo an optional output parameter for filling line information.
      * @return the signed width of the line
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    public float metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth) {
+    public float metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth,
+            @Nullable LineInfo lineInfo) {
         if (returnDrawWidth) {
             if (drawBounds == null) {
                 if (mTmpRectForMeasure == null) {
@@ -327,7 +344,7 @@
                 drawBounds = mTmpRectForMeasure;
             }
             drawBounds.setEmpty();
-            float w = measure(mLen, false, fmi, drawBounds);
+            float w = measure(mLen, false, fmi, drawBounds, lineInfo);
             float boundsWidth = drawBounds.width();
             if (Math.abs(w) > boundsWidth) {
                 return w;
@@ -337,7 +354,7 @@
                 return Math.signum(w) * boundsWidth;
             }
         } else {
-            return measure(mLen, false, fmi, drawBounds);
+            return measure(mLen, false, fmi, drawBounds, lineInfo);
         }
     }
 
@@ -407,12 +424,13 @@
      *                 the edge of the preceding run's edge. See example above.
      * @param fmi receives metrics information about the requested character, can be null
      * @param drawBounds output parameter for drawing bounding box. optional.
+     * @param lineInfo an optional output parameter for filling line information.
      * @return the signed graphical offset from the leading margin to the requested character edge.
      *         The positive value means the offset is right from the leading edge. The negative
      *         value means the offset is left from the leading edge.
      */
     public float measure(@IntRange(from = 0) int offset, boolean trailing,
-            @NonNull FontMetricsInt fmi, @Nullable RectF drawBounds) {
+            @NonNull FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable LineInfo lineInfo) {
         if (offset > mLen) {
             throw new IndexOutOfBoundsException(
                     "offset(" + offset + ") should be less than line limit(" + mLen + ")");
@@ -437,16 +455,16 @@
 
                     if (targetIsInThisSegment && sameDirection) {
                         return h + measureRun(segStart, offset, j, runIsRtl, fmi, drawBounds, null,
-                                0, h);
+                                0, h, lineInfo);
                     }
 
                     final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi, drawBounds,
-                            null, 0, h);
+                            null, 0, h, lineInfo);
                     h += sameDirection ? segmentWidth : -segmentWidth;
 
                     if (targetIsInThisSegment) {
                         return h + measureRun(segStart, offset, j, runIsRtl, null, null,  null, 0,
-                                h);
+                                h, lineInfo);
                     }
 
                     if (j != runLimit) {  // charAt(j) == TAB_CHAR
@@ -537,7 +555,8 @@
                     final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
 
                     final float segmentWidth =
-                            measureRun(segStart, j, j, runIsRtl, null, null, advances, segStart, 0);
+                            measureRun(segStart, j, j, runIsRtl, null, null, advances, segStart, 0,
+                                    null);
 
                     final float oldh = h;
                     h += sameDirection ? segmentWidth : -segmentWidth;
@@ -578,7 +597,7 @@
     }
 
     /**
-     * @see #measure(int, boolean, FontMetricsInt, RectF)
+     * @see #measure(int, boolean, FontMetricsInt, RectF, LineInfo)
      * @return The measure results for all possible offsets
      */
     @VisibleForTesting
@@ -610,7 +629,7 @@
                     final float previousSegEndHorizontal = measurement[segStart];
                     final float width =
                             measureRun(segStart, j, j, runIsRtl, fmi, null, measurement, segStart,
-                                    0);
+                                    0, null);
                     horizontal += sameDirection ? width : -width;
 
                     float currHorizontal = sameDirection ? oldHorizontal : horizontal;
@@ -675,14 +694,14 @@
             boolean needWidth) {
 
         if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
-            float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0);
+            float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null);
             handleRun(start, limit, limit, runIsRtl, c, null, x + w, top,
-                    y, bottom, null, null, false, null, 0);
+                    y, bottom, null, null, false, null, 0, null);
             return w;
         }
 
         return handleRun(start, limit, limit, runIsRtl, c, null, x, top,
-                y, bottom, null, null, needWidth, null, 0);
+                y, bottom, null, null, needWidth, null, 0, null);
     }
 
     /**
@@ -698,19 +717,20 @@
      * @param advances receives the advance information about the requested run, can be null.
      * @param advancesIndex the start index to fill in the advance information.
      * @param x horizontal offset of the run.
+     * @param lineInfo an optional output parameter for filling line information.
      * @return the signed width from the start of the run to the leading edge
      * of the character at offset, based on the run (not paragraph) direction
      */
     private float measureRun(int start, int offset, int limit, boolean runIsRtl,
             @Nullable FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable float[] advances,
-            int advancesIndex, float x) {
+            int advancesIndex, float x, @Nullable LineInfo lineInfo) {
         if (drawBounds != null && (mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
-            float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0);
+            float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0, null);
             return handleRun(start, offset, limit, runIsRtl, null, null, x + w, 0, 0, 0, fmi,
-                    drawBounds, true, advances, advancesIndex);
+                    drawBounds, true, advances, advancesIndex, lineInfo);
         }
         return handleRun(start, offset, limit, runIsRtl, null, null, x, 0, 0, 0, fmi, drawBounds,
-                true, advances, advancesIndex);
+                true, advances, advancesIndex, lineInfo);
     }
 
     /**
@@ -729,14 +749,14 @@
             int limit, boolean runIsRtl, float x, boolean needWidth) {
 
         if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
-            float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0);
+            float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null);
             handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null, null,
-                    false, null, 0);
+                    false, null, 0, null);
             return w;
         }
 
         return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null, null,
-                needWidth, null, 0);
+                needWidth, null, 0, null);
     }
 
 
@@ -1077,16 +1097,35 @@
 
     private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
             boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex,
-            RectF drawingBounds) {
+            RectF drawingBounds, @Nullable LineInfo lineInfo) {
+        if (lineInfo != null) {
+            if (mRunInfo == null) {
+                mRunInfo = new Paint.RunInfo();
+            }
+            mRunInfo.setClusterCount(0);
+        } else {
+            mRunInfo = null;
+        }
         if (mCharsValid) {
-            return wp.getRunCharacterAdvance(mChars, start, end, contextStart, contextEnd,
-                    runIsRtl, offset, advances, advancesIndex, drawingBounds);
+            float r = wp.getRunCharacterAdvance(mChars, start, end, contextStart, contextEnd,
+                    runIsRtl, offset, advances, advancesIndex, drawingBounds, mRunInfo);
+            if (lineInfo != null) {
+                lineInfo.setClusterCount(lineInfo.getClusterCount() + mRunInfo.getClusterCount());
+            }
+            return r;
         } else {
             final int delta = mStart;
-            if (mComputed == null || advances != null) {
-                return wp.getRunCharacterAdvance(mText, delta + start, delta + end,
+            // TODO: Add cluster information to the PrecomputedText for better performance of
+            // justification.
+            if (mComputed == null || advances != null || lineInfo != null) {
+                float r = wp.getRunCharacterAdvance(mText, delta + start, delta + end,
                         delta + contextStart, delta + contextEnd, runIsRtl,
-                        delta + offset, advances, advancesIndex, drawingBounds);
+                        delta + offset, advances, advancesIndex, drawingBounds, mRunInfo);
+                if (lineInfo != null) {
+                    lineInfo.setClusterCount(
+                            lineInfo.getClusterCount() + mRunInfo.getClusterCount());
+                }
+                return r;
             } else {
                 if (drawingBounds != null) {
                     if (mTmpRectForPrecompute == null) {
@@ -1120,6 +1159,7 @@
      * @param decorations the list of locations and paremeters for drawing decorations
      * @param advances receives the advance information about the requested run, can be null.
      * @param advancesIndex the start index to fill in the advance information.
+     * @param lineInfo an optional output parameter for filling line information.
      * @return the signed width of the run based on the run direction; only
      * valid if needWidth is true
      */
@@ -1128,7 +1168,7 @@
             Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom,
             FontMetricsInt fmi, RectF drawBounds, boolean needWidth, int offset,
             @Nullable ArrayList<DecorationInfo> decorations,
-            @Nullable float[] advances, int advancesIndex) {
+            @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo) {
 
         if (mIsJustifying) {
             wp.setWordSpacing(mAddedWidthForJustify);
@@ -1155,7 +1195,8 @@
                 mTmpRectForPaintAPI = new RectF();
             }
             totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset,
-                    advances, advancesIndex, drawBounds == null ? null : mTmpRectForPaintAPI);
+                    advances, advancesIndex, drawBounds == null ? null : mTmpRectForPaintAPI,
+                    lineInfo);
             if (drawBounds != null) {
                 if (runIsRtl) {
                     mTmpRectForPaintAPI.offset(x - totalWidth, 0);
@@ -1206,9 +1247,9 @@
                     final int decorationStart = Math.max(info.start, start);
                     final int decorationEnd = Math.min(info.end, offset);
                     float decorationStartAdvance = getRunAdvance(wp, start, end, contextStart,
-                            contextEnd, runIsRtl, decorationStart, null, 0, null);
+                            contextEnd, runIsRtl, decorationStart, null, 0, null, null);
                     float decorationEndAdvance = getRunAdvance(wp, start, end, contextStart,
-                            contextEnd, runIsRtl, decorationEnd, null, 0, null);
+                            contextEnd, runIsRtl, decorationEnd, null, 0, null, null);
                     final float decorationXLeft, decorationXRight;
                     if (runIsRtl) {
                         decorationXLeft = rightX - decorationEndAdvance;
@@ -1377,6 +1418,7 @@
      * @param needWidth true if the width is required
      * @param advances receives the advance information about the requested run, can be null.
      * @param advancesIndex the start index to fill in the advance information.
+     * @param lineInfo an optional output parameter for filling line information.
      * @return the signed width of the run based on the run direction; only
      * valid if needWidth is true
      */
@@ -1384,7 +1426,7 @@
             int limit, boolean runIsRtl, Canvas c,
             TextShaper.GlyphsConsumer consumer, float x, int top, int y,
             int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth,
-            @Nullable float[] advances, int advancesIndex) {
+            @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo) {
 
         if (measureLimit < start || measureLimit > limit) {
             throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
@@ -1431,7 +1473,7 @@
             wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit()));
             return handleText(wp, start, limit, start, limit, runIsRtl, c, consumer, x, top,
                     y, bottom, fmi, drawBounds, needWidth, measureLimit, null, advances,
-                    advancesIndex);
+                    advancesIndex, lineInfo);
         }
 
         // Shaping needs to take into account context up to metric boundaries,
@@ -1523,7 +1565,7 @@
                             consumer, x, top, y, bottom, fmi, drawBounds,
                             needWidth || activeEnd < measureLimit,
                             Math.min(activeEnd, mlimit), mDecorations,
-                            advances, advancesIndex + activeStart - start);
+                            advances, advancesIndex + activeStart - start, lineInfo);
 
                     activeStart = j;
                     activePaint.set(wp);
@@ -1551,7 +1593,7 @@
             x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, consumer, x,
                     top, y, bottom, fmi, drawBounds, needWidth || activeEnd < measureLimit,
                     Math.min(activeEnd, mlimit), mDecorations,
-                    advances, advancesIndex + activeStart - start);
+                    advances, advancesIndex + activeStart - start, lineInfo);
         }
 
         return x - originalX;
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index a74b06a..9f9b7b4 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -1,6 +1,13 @@
 package: "android.view.flags"
 
 flag {
+     name: "enable_surface_native_alloc_registration"
+     namespace: "toolkit"
+     description: "Feature flag for registering surfaces with the VM for faster cleanup"
+     bug: "306193257"
+}
+
+flag {
     name: "enable_use_measure_cache_during_force_layout"
     namespace: "toolkit"
     description: "Enables using the measure cache during a view force layout from the second "
diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java
index ef50045..1d2f653 100644
--- a/core/java/android/view/textclassifier/TextClassifier.java
+++ b/core/java/android/view/textclassifier/TextClassifier.java
@@ -16,6 +16,9 @@
 
 package android.view.textclassifier;
 
+import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS;
+
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -108,6 +111,9 @@
     String TYPE_DATE_TIME = "datetime";
     /** Flight number in IATA format. */
     String TYPE_FLIGHT_NUMBER = "flight";
+    /** One-time login codes */
+    @FlaggedApi(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
+    String TYPE_OTP_CODE = "otp_code";
     /**
      * Word that users may be interested to look up for meaning.
      * @hide
@@ -126,7 +132,8 @@
             TYPE_DATE,
             TYPE_DATE_TIME,
             TYPE_FLIGHT_NUMBER,
-            TYPE_DICTIONARY
+            TYPE_DICTIONARY,
+            TYPE_OTP_CODE
     })
     @interface EntityType {}
 
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index f2bce9c..bb16ad2 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -72,7 +72,7 @@
     name: "predictive_back_system_animations"
     namespace: "systemui"
     description: "Predictive back for system animations"
-    bug: "309545085"
+    bug: "319421778"
     is_fixed_read_only: true
 }
 
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
index 12aff1c..5d82d04 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
@@ -29,7 +29,6 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
-import android.multiuser.Flags;
 import android.os.Build;
 import android.os.PatternMatcher;
 import android.util.Slog;
@@ -127,10 +126,6 @@
                     .setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SINGLE_USER,
                             R.styleable.AndroidManifestProvider_singleUser, sa));
 
-            if (Flags.enableSystemUserOnlyForServicesAndProviders()) {
-                provider.setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SYSTEM_USER_ONLY,
-                        R.styleable.AndroidManifestProvider_systemUserOnly, sa));
-            }
             visibleToEphemeral = sa.getBoolean(
                     R.styleable.AndroidManifestProvider_visibleToInstantApps, false);
             if (visibleToEphemeral) {
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
index 4ac542f8..a1dd19a3 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
@@ -29,7 +29,6 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
-import android.multiuser.Flags;
 import android.os.Build;
 
 import com.android.internal.R;
@@ -106,11 +105,6 @@
                             | flag(ServiceInfo.FLAG_SINGLE_USER,
                             R.styleable.AndroidManifestService_singleUser, sa)));
 
-            if (Flags.enableSystemUserOnlyForServicesAndProviders()) {
-                service.setFlags(service.getFlags() | flag(ServiceInfo.FLAG_SYSTEM_USER_ONLY,
-                        R.styleable.AndroidManifestService_systemUserOnly, sa));
-            }
-
             visibleToEphemeral = sa.getBoolean(
                     R.styleable.AndroidManifestService_visibleToInstantApps, false);
             if (visibleToEphemeral) {
diff --git a/core/jni/android_os_VintfObject.cpp b/core/jni/android_os_VintfObject.cpp
index b651711..a5b2f65 100644
--- a/core/jni/android_os_VintfObject.cpp
+++ b/core/jni/android_os_VintfObject.cpp
@@ -96,8 +96,11 @@
 
 static jint android_os_VintfObject_verifyBuildAtBoot(JNIEnv* env, jclass) {
     std::string error;
+    // Use temporary VintfObject, not the shared instance, to release memory
+    // after check.
     int32_t status =
-            VintfObject::GetInstance()
+            VintfObject::Builder()
+                    .build()
                     ->checkCompatibility(&error, ENABLE_ALL_CHECKS.disableAvb().disableKernel());
     if (status)
         LOG(WARNING) << "VintfObject.verifyBuildAtBoot() returns " << status << ": " << error;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index ef6caef..5f3f641 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5214,6 +5214,14 @@
     <permission android:name="android.permission.BIND_REMOTE_DISPLAY"
         android:protectionLevel="signature" />
 
+    <!-- Must be required by a android.media.tv.ad.TvAdService to ensure that only the system can
+         bind to it.
+         <p>Protection level: signature|privileged
+         @FlaggedApi("android.media.tv.flags.enable_ad_service_fw")
+    -->
+    <permission android:name="android.permission.BIND_TV_AD_SERVICE"
+                android:protectionLevel="signature|privileged" />
+
     <!-- Must be required by a {@link android.media.tv.TvInputService}
          to ensure that only the system can bind to it.
          <p>Protection level: signature|privileged
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 6019524..8fae6db 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -506,12 +506,6 @@
          receivers, and providers; it can not be used with activities. -->
     <attr name="singleUser" format="boolean" />
 
-    <!-- If set to true, only a single instance of this component will
-    run and be available for the SYSTEM user. Non SYSTEM users will not be
-    allowed to access the component if this flag is enabled.
-    This flag can be used with services, receivers, providers and activities. -->
-    <attr name="systemUserOnly" format="boolean" />
-
     <!-- Specify a specific process that the associated code is to run in.
          Use with the application tag (to supply a default process for all
          application components), or with the activity, receiver, service,
@@ -2865,7 +2859,6 @@
              Context.createAttributionContext() using the first attribution tag
              contained here. -->
         <attr name="attributionTags" />
-        <attr name="systemUserOnly" format="boolean" />
     </declare-styleable>
 
     <!-- Attributes that can be supplied in an AndroidManifest.xml
@@ -3024,7 +3017,6 @@
              ignored when the process is bound into a shared isolated process by a client.
         -->
         <attr name="allowSharedIsolatedProcess" format="boolean" />
-        <attr name="systemUserOnly" format="boolean" />
     </declare-styleable>
 
     <!-- @hide The <code>apex-system-service</code> tag declares an apex system service
@@ -3152,7 +3144,7 @@
         <attr name="uiOptions" />
         <attr name="parentActivityName" />
         <attr name="singleUser" />
-        <!-- This broadcast receiver or activity will only receive broadcasts for the
+        <!-- @hide This broadcast receiver or activity will only receive broadcasts for the
              system user-->
         <attr name="systemUserOnly" format="boolean" />
         <attr name="persistableMode" />
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 7b5c49c..53b473e 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -119,8 +119,6 @@
     <public name="optional"/>
     <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") -->
     <public name="adServiceTypes" />
-    <!-- @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") -->
-    <public name="systemUserOnly"/>
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01bc0000">
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 542e9d6..f285806 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1825,7 +1825,7 @@
     <!-- Message shown during fingerprint acquisision when the fingerprint cannot be recognized -->
     <string name="fingerprint_acquired_partial">Press firmly on the sensor</string>
     <!-- Message shown during fingerprint acquisision when the fingerprint cannot be recognized -->
-    <string name="fingerprint_acquired_insufficient">Can\u2019t recognize fingerprint. Try again.</string>
+    <string name="fingerprint_acquired_insufficient">Fingerprint not recognized. Try again.</string>
     <!-- Message shown during fingerprint acquisision when the fingerprint sensor needs cleaning -->
     <string name="fingerprint_acquired_imager_dirty">Clean fingerprint sensor and try again</string>
     <string name="fingerprint_acquired_imager_dirty_alt">Clean sensor and try again</string>
@@ -1959,7 +1959,7 @@
     <!-- Message shown during face acquisition when the sensor needs to be recalibrated [CHAR LIMIT=50] -->
     <string name="face_acquired_recalibrate">Please re-enroll your face.</string>
     <!-- Message shown during face enrollment when a different person's face is detected [CHAR LIMIT=50] -->
-    <string name="face_acquired_too_different">Can\u2019t recognize face. Try again.</string>
+    <string name="face_acquired_too_different">Face not recognized. Try again.</string>
     <!-- Message shown during face enrollment when the face is too similar to a previous acquisition [CHAR LIMIT=50] -->
     <string name="face_acquired_too_similar">Change the position of your head slightly</string>
     <!-- Message shown during acqusition when the user's face is turned too far left or right [CHAR LIMIT=50] -->
diff --git a/core/tests/coretests/src/android/graphics/PaintTest.java b/core/tests/coretests/src/android/graphics/PaintTest.java
index bf56df1..0dec756 100644
--- a/core/tests/coretests/src/android/graphics/PaintTest.java
+++ b/core/tests/coretests/src/android/graphics/PaintTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertNotEquals;
 
 import android.test.InstrumentationTestCase;
+import android.text.TextUtils;
 
 import androidx.test.filters.SmallTest;
 
@@ -362,4 +363,44 @@
         //    = 30
         assertEquals(30.0f, p.getUnderlineThickness(), 0.5f);
     }
+
+    private int getClusterCount(Paint p, String text) {
+        Paint.RunInfo runInfo = new Paint.RunInfo();
+        p.getRunCharacterAdvance(text, 0, text.length(), 0, text.length(), false, 0, null, 0, null,
+                runInfo);
+        int ccByString = runInfo.getClusterCount();
+        runInfo.setClusterCount(0);
+        char[] buf = new char[text.length()];
+        TextUtils.getChars(text, 0, text.length(), buf, 0);
+        p.getRunCharacterAdvance(buf, 0, buf.length, 0, buf.length, false, 0, null, 0, null,
+                runInfo);
+        int ccByChars = runInfo.getClusterCount();
+        assertEquals(ccByChars, ccByString);
+        return ccByChars;
+    }
+
+    public void testCluster() {
+        final Paint p = new Paint();
+        p.setTextSize(100);
+
+        // Regular String
+        assertEquals(1, getClusterCount(p, "A"));
+        assertEquals(2, getClusterCount(p, "AB"));
+
+        // Ligature is in the same cluster
+        assertEquals(1, getClusterCount(p, "fi"));  // Ligature
+        p.setFontFeatureSettings("'liga' off");
+        assertEquals(2, getClusterCount(p, "fi"));  // Ligature is disabled
+        p.setFontFeatureSettings("");
+
+        // Combining character
+        assertEquals(1, getClusterCount(p, "\u0061\u0300"));  // A + COMBINING GRAVE ACCENT
+
+        // BiDi
+        final String rtlStr = "\u05D0\u05D1\u05D2";
+        final String ltrStr = "abc";
+        assertEquals(3, getClusterCount(p, rtlStr));
+        assertEquals(6, getClusterCount(p, rtlStr + ltrStr));
+        assertEquals(9, getClusterCount(p, ltrStr + rtlStr + ltrStr));
+    }
 }
diff --git a/core/tests/coretests/src/android/text/TextLineTest.java b/core/tests/coretests/src/android/text/TextLineTest.java
index 34842a0..a31992c 100644
--- a/core/tests/coretests/src/android/text/TextLineTest.java
+++ b/core/tests/coretests/src/android/text/TextLineTest.java
@@ -50,11 +50,11 @@
         tl.set(paint, line, 0, line.length(), Layout.DIR_LEFT_TO_RIGHT,
                 Layout.DIRS_ALL_LEFT_TO_RIGHT, false /* hasTabs */, null /* tabStops */,
                 0, 0 /* no ellipsis */, false /* useFallbackLinespace */);
-        final float originalWidth = tl.metrics(null, null, false);
+        final float originalWidth = tl.metrics(null, null, false, null);
         final float expandedWidth = 2 * originalWidth;
 
         tl.justify(expandedWidth);
-        final float newWidth = tl.metrics(null, null, false);
+        final float newWidth = tl.metrics(null, null, false, null);
         TextLine.recycle(tl);
         return Math.abs(newWidth - expandedWidth) < 0.5;
     }
@@ -128,7 +128,7 @@
     private void assertMeasurements(final TextLine tl, final int length, boolean trailing,
             final float[] expected) {
         for (int offset = 0; offset <= length; ++offset) {
-            assertEquals(expected[offset], tl.measure(offset, trailing, null, null), 0.0f);
+            assertEquals(expected[offset], tl.measure(offset, trailing, null, null, null), 0.0f);
         }
 
         final boolean[] trailings = new boolean[length + 1];
@@ -318,7 +318,7 @@
         tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT,
                 false /* hasTabs */, null /* tabStops */, 9, 12,
                 false /* useFallbackLineSpacing */);
-        tl.measure(text.length(), false /* trailing */, null /* fmi */, null);
+        tl.measure(text.length(), false /* trailing */, null /* fmi */, null, null);
 
         assertFalse(span.mIsUsed);
     }
@@ -335,7 +335,7 @@
         tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT,
                 false /* hasTabs */, null /* tabStops */, 9, 12,
                 false /* useFallbackLineSpacing */);
-        tl.measure(text.length(), false /* trailing */, null /* fmi */, null);
+        tl.measure(text.length(), false /* trailing */, null /* fmi */, null, null);
 
         assertTrue(span.mIsUsed);
     }
@@ -352,7 +352,7 @@
         tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT,
                 false /* hasTabs */, null /* tabStops */, 9, 12,
                 false /* useFallbackLineSpacing */);
-        tl.measure(text.length(), false /* trailing */, null /* fmi */, null);
+        tl.measure(text.length(), false /* trailing */, null /* fmi */, null, null);
         assertTrue(span.mIsUsed);
     }
 
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index f10cdb8..c5a2f98 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -65,6 +65,8 @@
     private long mNativeShader;
     private long mNativeColorFilter;
 
+    private static boolean sIsRobolectric = Build.FINGERPRINT.equals("robolectric");
+
     // Use a Holder to allow static initialization of Paint in the boot image.
     private static class NoImagePreloadHolder {
         public static final NativeAllocationRegistry sRegistry =
@@ -2474,6 +2476,19 @@
         nGetFontMetricsInt(mNativePaint, metrics, true);
     }
 
+    /** @hide */
+    public static final class RunInfo {
+        private int mClusterCount = 0;
+
+        public int getClusterCount() {
+            return mClusterCount;
+        }
+
+        public void setClusterCount(int clusterCount) {
+            mClusterCount = clusterCount;
+        }
+    }
+
     /**
      * Return the recommend line spacing based on the current typeface and
      * text size.
@@ -3320,7 +3335,7 @@
             int contextEnd, boolean isRtl, int offset,
             @Nullable float[] advances, int advancesIndex) {
         return getRunCharacterAdvance(text, start, end, contextStart, contextEnd, isRtl, offset,
-                advances, advancesIndex, null);
+                advances, advancesIndex, null, null);
     }
 
     /**
@@ -3339,12 +3354,14 @@
      * @param advances the array that receives the computed character advances
      * @param advancesIndex the start index from which the advances array is filled
      * @param drawBounds the output parameter for the bounding box of drawing text, optional
+     * @param runInfo the output parameter for storing run information.
      * @return width measurement between start and offset
-     * @hide
+     * @hide TODO: Reorganize APIs
      */
     public float getRunCharacterAdvance(@NonNull char[] text, int start, int end, int contextStart,
             int contextEnd, boolean isRtl, int offset,
-            @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds) {
+            @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds,
+            @Nullable RunInfo runInfo) {
         if (text == null) {
             throw new IllegalArgumentException("text cannot be null");
         }
@@ -3370,11 +3387,19 @@
         }
 
         if (end == start) {
+            if (runInfo != null) {
+                runInfo.setClusterCount(0);
+            }
             return 0.0f;
         }
 
-        return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd,
-                isRtl, offset, advances, advancesIndex, drawBounds);
+        if (sIsRobolectric) {
+            return nGetRunCharacterAdvance(mNativePaint, text, start, end,
+                    contextStart, contextEnd, isRtl, offset, advances, advancesIndex, drawBounds);
+        } else {
+            return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd,
+                    isRtl, offset, advances, advancesIndex, drawBounds, runInfo);
+        }
     }
 
     /**
@@ -3402,7 +3427,7 @@
             int contextStart, int contextEnd, boolean isRtl, int offset,
             @Nullable float[] advances, int advancesIndex) {
         return getRunCharacterAdvance(text, start, end, contextStart, contextEnd, isRtl, offset,
-                advances, advancesIndex, null);
+                advances, advancesIndex, null, null);
     }
 
     /**
@@ -3418,12 +3443,14 @@
      * @param advances the array that receives the computed character advances
      * @param advancesIndex the start index from which the advances array is filled
      * @param drawBounds the output parameter for the bounding box of drawing text, optional
+     * @param runInfo an optional output parameter for filling run information.
      * @return width measurement between start and offset
-     * @hide
+     * @hide  TODO: Reorganize APIs
      */
     public float getRunCharacterAdvance(@NonNull CharSequence text, int start, int end,
             int contextStart, int contextEnd, boolean isRtl, int offset,
-            @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds) {
+            @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds,
+            @Nullable RunInfo runInfo) {
         if (text == null) {
             throw new IllegalArgumentException("text cannot be null");
         }
@@ -3456,7 +3483,7 @@
         TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
         final float result = getRunCharacterAdvance(buf, start - contextStart, end - contextStart,
                 0, contextEnd - contextStart, isRtl, offset - contextStart,
-                advances, advancesIndex, drawBounds);
+                advances, advancesIndex, drawBounds, runInfo);
         TemporaryBuffer.recycle(buf);
         return result;
     }
@@ -3574,7 +3601,7 @@
             int contextStart, int contextEnd, boolean isRtl, int offset);
     private static native float nGetRunCharacterAdvance(long paintPtr, char[] text, int start,
             int end, int contextStart, int contextEnd, boolean isRtl, int offset, float[] advances,
-            int advancesIndex, RectF drawingBounds);
+            int advancesIndex, RectF drawingBounds, RunInfo runInfo);
     private static native int nGetOffsetForAdvance(long paintPtr, char[] text, int start, int end,
             int contextStart, int contextEnd, boolean isRtl, float advance);
     private static native void nGetFontMetricsIntForText(long paintPtr, char[] text,
@@ -3729,4 +3756,11 @@
     private static native void nSetTextSize(long paintPtr, float textSize);
     @CriticalNative
     private static native boolean nEqualsForTextMeasurement(long leftPaintPtr, long rightPaintPtr);
+
+
+    // Following Native methods are kept for old Robolectric JNI signature used by
+    // SystemUIGoogleRoboRNGTests
+    private static native float nGetRunCharacterAdvance(long paintPtr, char[] text,
+            int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset,
+            float[] advances, int advancesIndex, RectF drawingBounds);
 }
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 5ad144d..45540e0 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -175,3 +175,74 @@
     plugins: ["dagger2-compiler"],
     use_resource_processor: true,
 }
+
+android_app {
+    name: "WindowManagerShellRobolectric",
+    platform_apis: true,
+    static_libs: [
+        "WindowManager-Shell",
+    ],
+    manifest: "multivalentTests/AndroidManifestRobolectric.xml",
+    use_resource_processor: true,
+}
+
+android_robolectric_test {
+    name: "WMShellRobolectricTests",
+    instrumentation_for: "WindowManagerShellRobolectric",
+    upstream: true,
+    java_resource_dirs: [
+        "multivalentTests/robolectric/config",
+    ],
+    srcs: [
+        "multivalentTests/src/**/*.kt",
+    ],
+    static_libs: [
+        "junit",
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "mockito-robolectric-prebuilt",
+        "mockito-kotlin2",
+        "truth",
+    ],
+}
+
+android_test {
+    name: "WMShellMultivalentTestsOnDevice",
+    srcs: [
+        "multivalentTests/src/**/*.kt",
+    ],
+    static_libs: [
+        "WindowManager-Shell",
+        "junit",
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "frameworks-base-testutils",
+        "mockito-kotlin2",
+        "mockito-target-extended-minus-junit4",
+        "truth",
+        "platform-test-annotations",
+        "platform-test-rules",
+    ],
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+    ],
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+    optimize: {
+        enabled: false,
+    },
+    test_suites: ["device-tests"],
+    platform_apis: true,
+    certificate: "platform",
+    aaptflags: [
+        "--extra-packages",
+        "com.android.wm.shell",
+    ],
+    manifest: "multivalentTests/AndroidManifest.xml",
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
new file mode 100644
index 0000000..f8f8338
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
@@ -0,0 +1,13 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.wm.shell.multivalenttests">
+
+    <application android:debuggable="true" android:supportsRtl="true" >
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:label="Multivalent tests for WindowManager-Shell"
+        android:targetPackage="com.android.wm.shell.multivalenttests">
+    </instrumentation>
+</manifest>
diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidManifestRobolectric.xml b/libs/WindowManager/Shell/multivalentTests/AndroidManifestRobolectric.xml
new file mode 100644
index 0000000..ffcd7d4
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/AndroidManifestRobolectric.xml
@@ -0,0 +1,3 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.wm.shell.multivalenttests">
+</manifest>
diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidTest.xml b/libs/WindowManager/Shell/multivalentTests/AndroidTest.xml
new file mode 100644
index 0000000..36fe8ec
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?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 Tests for WindowManagerShellLib">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="WMShellMultivalentTestsOnDevice.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="framework-base-presubmit" />
+    <option name="test-tag" value="WMShellMultivalentTestsOnDevice" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.wm.shell.multivalenttests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/libs/WindowManager/Shell/multivalentTests/robolectric/config/robolectric.properties b/libs/WindowManager/Shell/multivalentTests/robolectric/config/robolectric.properties
new file mode 100644
index 0000000..7a0527c
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/robolectric/config/robolectric.properties
@@ -0,0 +1,2 @@
+sdk=NEWEST_SDK
+
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
new file mode 100644
index 0000000..ea7c6ed
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
@@ -0,0 +1,481 @@
+/*
+ * 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.wm.shell.bubbles
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.graphics.Insets
+import android.graphics.PointF
+import android.graphics.Rect
+import android.os.UserHandle
+import android.view.WindowManager
+import androidx.test.core.app.ApplicationProvider
+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.MAX_HEIGHT
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests operations and the resulting state managed by [BubblePositioner]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubblePositionerTest {
+
+    private lateinit var positioner: BubblePositioner
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val defaultDeviceConfig =
+        DeviceConfig(
+            windowBounds = Rect(0, 0, 1000, 2000),
+            isLargeScreen = false,
+            isSmallTablet = false,
+            isLandscape = false,
+            isRtl = false,
+            insets = Insets.of(0, 0, 0, 0)
+        )
+
+    @Before
+    fun setUp() {
+        val windowManager = context.getSystemService(WindowManager::class.java)
+        positioner = BubblePositioner(context, windowManager)
+    }
+
+    @Test
+    fun testUpdate() {
+        val insets = Insets.of(10, 20, 5, 15)
+        val screenBounds = Rect(0, 0, 1000, 1200)
+        val availableRect = Rect(screenBounds)
+        availableRect.inset(insets)
+        positioner.update(defaultDeviceConfig.copy(insets = insets, windowBounds = screenBounds))
+        assertThat(positioner.availableRect).isEqualTo(availableRect)
+        assertThat(positioner.isLandscape).isFalse()
+        assertThat(positioner.isLargeScreen).isFalse()
+        assertThat(positioner.insets).isEqualTo(insets)
+    }
+
+    @Test
+    fun testShowBubblesVertically_phonePortrait() {
+        positioner.update(defaultDeviceConfig)
+        assertThat(positioner.showBubblesVertically()).isFalse()
+    }
+
+    @Test
+    fun testShowBubblesVertically_phoneLandscape() {
+        positioner.update(defaultDeviceConfig.copy(isLandscape = true))
+        assertThat(positioner.isLandscape).isTrue()
+        assertThat(positioner.showBubblesVertically()).isTrue()
+    }
+
+    @Test
+    fun testShowBubblesVertically_tablet() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
+        assertThat(positioner.showBubblesVertically()).isTrue()
+    }
+
+    /** If a resting position hasn't been set, calling it will return the default position. */
+    @Test
+    fun testGetRestingPosition_returnsDefaultPosition() {
+        positioner.update(defaultDeviceConfig)
+        val restingPosition = positioner.getRestingPosition()
+        val defaultPosition = positioner.defaultStartPosition
+        assertThat(restingPosition).isEqualTo(defaultPosition)
+    }
+
+    /** If a resting position has been set, it'll return that instead of the default position. */
+    @Test
+    fun testGetRestingPosition_returnsRestingPosition() {
+        positioner.update(defaultDeviceConfig)
+        val restingPosition = PointF(100f, 100f)
+        positioner.restingPosition = restingPosition
+        assertThat(positioner.getRestingPosition()).isEqualTo(restingPosition)
+    }
+
+    /** Test that the default resting position on phone is in upper left. */
+    @Test
+    fun testGetRestingPosition_bubble_onPhone() {
+        positioner.update(defaultDeviceConfig)
+        val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+        val restingPosition = positioner.getRestingPosition()
+        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left)
+        assertThat(restingPosition.y).isEqualTo(defaultYPosition)
+    }
+
+    @Test
+    fun testGetRestingPosition_bubble_onPhone_RTL() {
+        positioner.update(defaultDeviceConfig.copy(isRtl = true))
+        val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+        val restingPosition = positioner.getRestingPosition()
+        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right)
+        assertThat(restingPosition.y).isEqualTo(defaultYPosition)
+    }
+
+    /** Test that the default resting position on tablet is middle left. */
+    @Test
+    fun testGetRestingPosition_chatBubble_onTablet() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
+        val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+        val restingPosition = positioner.getRestingPosition()
+        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left)
+        assertThat(restingPosition.y).isEqualTo(defaultYPosition)
+    }
+
+    @Test
+    fun testGetRestingPosition_chatBubble_onTablet_RTL() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
+        val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+        val restingPosition = positioner.getRestingPosition()
+        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right)
+        assertThat(restingPosition.y).isEqualTo(defaultYPosition)
+    }
+
+    /** Test that the default resting position on tablet is middle right. */
+    @Test
+    fun testGetDefaultPosition_appBubble_onTablet() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
+        val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+        val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */)
+        assertThat(startPosition.x).isEqualTo(allowableStackRegion.right)
+        assertThat(startPosition.y).isEqualTo(defaultYPosition)
+    }
+
+    @Test
+    fun testGetRestingPosition_appBubble_onTablet_RTL() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
+        val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+        val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */)
+        assertThat(startPosition.x).isEqualTo(allowableStackRegion.left)
+        assertThat(startPosition.y).isEqualTo(defaultYPosition)
+    }
+
+    @Test
+    fun testHasUserModifiedDefaultPosition_false() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
+        assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
+        positioner.restingPosition = positioner.defaultStartPosition
+        assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
+    }
+
+    @Test
+    fun testHasUserModifiedDefaultPosition_true() {
+        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
+        assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
+        positioner.restingPosition = PointF(0f, 100f)
+        assertThat(positioner.hasUserModifiedDefaultPosition()).isTrue()
+    }
+
+    @Test
+    fun testGetExpandedViewHeight_max() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT)
+    }
+
+    @Test
+    fun testGetExpandedViewHeight_customHeight_valid() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+        val minHeight =
+            context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height)
+        val bubble =
+            Bubble(
+                "key",
+                ShortcutInfo.Builder(context, "id").build(),
+                minHeight + 100 /* desiredHeight */,
+                0 /* desiredHeightResId */,
+                "title",
+                0 /* taskId */,
+                null /* locus */,
+                true /* isDismissable */,
+                directExecutor()) {}
+
+        // Ensure the height is the same as the desired value
+        assertThat(positioner.getExpandedViewHeight(bubble))
+            .isEqualTo(bubble.getDesiredHeight(context))
+    }
+
+    @Test
+    fun testGetExpandedViewHeight_customHeight_tooSmall() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val bubble =
+            Bubble(
+                "key",
+                ShortcutInfo.Builder(context, "id").build(),
+                10 /* desiredHeight */,
+                0 /* desiredHeightResId */,
+                "title",
+                0 /* taskId */,
+                null /* locus */,
+                true /* isDismissable */,
+                directExecutor()) {}
+
+        // Ensure the height is the same as the desired value
+        val minHeight =
+            context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height)
+        assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight)
+    }
+
+    @Test
+    fun testGetMaxExpandedViewHeight_onLargeTablet() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val manageButtonHeight =
+            context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height)
+        val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width)
+        val expandedViewPadding =
+            context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
+        val expectedHeight =
+            1800 - 2 * 20 - manageButtonHeight - pointerWidth - expandedViewPadding * 2
+        assertThat(positioner.getMaxExpandedViewHeight(false /* isOverflow */))
+            .isEqualTo(expectedHeight)
+    }
+
+    @Test
+    fun testAreBubblesBottomAligned_largeScreen_true() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        assertThat(positioner.areBubblesBottomAligned()).isTrue()
+    }
+
+    @Test
+    fun testAreBubblesBottomAligned_largeScreen_landscape_false() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                isLandscape = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        assertThat(positioner.areBubblesBottomAligned()).isFalse()
+    }
+
+    @Test
+    fun testAreBubblesBottomAligned_smallTablet_false() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                isSmallTablet = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        assertThat(positioner.areBubblesBottomAligned()).isFalse()
+    }
+
+    @Test
+    fun testAreBubblesBottomAligned_phone_false() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        assertThat(positioner.areBubblesBottomAligned()).isFalse()
+    }
+
+    @Test
+    fun testExpandedViewY_phoneLandscape() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLandscape = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        // This bubble will have max height so it'll always be top aligned
+        assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
+            .isEqualTo(positioner.getExpandedViewYTopAligned())
+    }
+
+    @Test
+    fun testExpandedViewY_phonePortrait() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        // Always top aligned in phone portrait
+        assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
+            .isEqualTo(positioner.getExpandedViewYTopAligned())
+    }
+
+    @Test
+    fun testExpandedViewY_smallTabletLandscape() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isSmallTablet = true,
+                isLandscape = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        // This bubble will have max height which is always top aligned on small tablets
+        assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
+            .isEqualTo(positioner.getExpandedViewYTopAligned())
+    }
+
+    @Test
+    fun testExpandedViewY_smallTabletPortrait() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isSmallTablet = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        // This bubble will have max height which is always top aligned on small tablets
+        assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
+            .isEqualTo(positioner.getExpandedViewYTopAligned())
+    }
+
+    @Test
+    fun testExpandedViewY_largeScreenLandscape() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                isLandscape = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        // This bubble will have max height which is always top aligned on landscape, large tablet
+        assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
+            .isEqualTo(positioner.getExpandedViewYTopAligned())
+    }
+
+    @Test
+    fun testExpandedViewY_largeScreenPortrait() {
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 1800, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+
+        val manageButtonHeight =
+            context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height)
+        val manageButtonPlusMargin =
+            manageButtonHeight +
+                2 * context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_margin)
+        val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width)
+
+        val expectedExpandedViewY =
+            positioner.availableRect.bottom -
+                manageButtonPlusMargin -
+                positioner.getExpandedViewHeightForLargeScreen() -
+                pointerWidth
+
+        // Bubbles are bottom aligned on portrait, large tablet
+        assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
+            .isEqualTo(expectedExpandedViewY)
+    }
+
+    private val defaultYPosition: Float
+        /**
+         * Calculates the Y position bubbles should be placed based on the config. Based on the
+         * calculations in [BubblePositioner.getDefaultStartPosition] and
+         * [BubbleStackView.RelativeStackPosition].
+         */
+        get() {
+            val isTablet = positioner.isLargeScreen
+
+            // On tablet the position is centered, on phone it is an offset from the top.
+            val desiredY =
+                if (isTablet) {
+                    positioner.screenRect.height() / 2f - positioner.bubbleSize / 2f
+                } else {
+                    context.resources
+                        .getDimensionPixelOffset(R.dimen.bubble_stack_starting_offset_y)
+                        .toFloat()
+                }
+            // Since we're visually centering the bubbles on tablet, use total screen height rather
+            // than the available height.
+            val height =
+                if (isTablet) {
+                    positioner.screenRect.height()
+                } else {
+                    positioner.availableRect.height()
+                }
+            val offsetPercent = (desiredY / height).coerceIn(0f, 1f)
+            val allowableStackRegion =
+                positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+            return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent
+        }
+}
diff --git a/libs/WindowManager/Shell/multivalentTestsForDevice b/libs/WindowManager/Shell/multivalentTestsForDevice
new file mode 120000
index 0000000..20ee34a
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTestsForDevice
@@ -0,0 +1 @@
+multivalentTests
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/multivalentTestsForDeviceless b/libs/WindowManager/Shell/multivalentTestsForDeviceless
new file mode 120000
index 0000000..20ee34a
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTestsForDeviceless
@@ -0,0 +1 @@
+multivalentTests
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index 80fc3a8..ac2a1c8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -136,6 +136,9 @@
         mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
         mStartTaskRect.offsetTo(0, 0);
 
+        // inset bottom in case of pinned taskbar being present
+        mStartTaskRect.inset(0, 0, 0, mClosingTarget.contentInsets.bottom);
+
         // Draw background.
         mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
                 BACKGROUNDCOLOR, mTransaction);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index bc1a575..5de8a9b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -481,18 +481,20 @@
     private void startFadeAnimation(@NonNull SurfaceControl leash, boolean show) {
         final float end = show ? 1.f : 0.f;
         final float start = 1.f - end;
-        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
         final ValueAnimator va = ValueAnimator.ofFloat(start, end);
         va.setDuration(FADE_DURATION);
         va.setInterpolator(show ? ALPHA_IN : ALPHA_OUT);
         va.addUpdateListener(animation -> {
             float fraction = animation.getAnimatedFraction();
+            final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
             transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction);
             transaction.apply();
+            mTransactionPool.release(transaction);
         });
         va.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
+                final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
                 transaction.setAlpha(leash, end);
                 transaction.apply();
                 mTransactionPool.release(transaction);
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt
index 182a908..be77171 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt
@@ -101,7 +101,8 @@
     override fun pipLayerReduces() {
         flicker.assertLayers {
             val pipLayerList =
-                this.layers { standardAppHelper.layerMatchesAnyOf(it) && it.isVisible }
+                this.layers { standardAppHelper.packageNameMatcher.layerMatchesAnyOf(it)
+                        && it.isVisible }
             pipLayerList.zipWithNext { previous, current ->
                 current.visibleRegion.notBiggerThan(previous.visibleRegion.region)
             }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java
deleted file mode 100644
index 6ebee73..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java
+++ /dev/null
@@ -1,602 +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.wm.shell.bubbles;
-
-import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-
-import static org.mockito.Mockito.mock;
-
-import android.content.Intent;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Insets;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.UserHandle;
-import android.testing.AndroidTestingRunner;
-import android.view.WindowManager;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.R;
-import com.android.wm.shell.ShellTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Tests operations and the resulting state managed by {@link BubblePositioner}.
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class BubblePositionerTest extends ShellTestCase {
-
-    private BubblePositioner mPositioner;
-
-    @Before
-    public void setUp() {
-        WindowManager windowManager = mContext.getSystemService(WindowManager.class);
-        mPositioner = new BubblePositioner(mContext, windowManager);
-    }
-
-    @Test
-    public void testUpdate() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1000, 1200);
-        Rect availableRect = new Rect(screenBounds);
-        availableRect.inset(insets);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.getAvailableRect()).isEqualTo(availableRect);
-        assertThat(mPositioner.isLandscape()).isFalse();
-        assertThat(mPositioner.isLargeScreen()).isFalse();
-        assertThat(mPositioner.getInsets()).isEqualTo(insets);
-    }
-
-    @Test
-    public void testShowBubblesVertically_phonePortrait() {
-        DeviceConfig deviceConfig = new ConfigBuilder().build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.showBubblesVertically()).isFalse();
-    }
-
-    @Test
-    public void testShowBubblesVertically_phoneLandscape() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLandscape().build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.isLandscape()).isTrue();
-        assertThat(mPositioner.showBubblesVertically()).isTrue();
-    }
-
-    @Test
-    public void testShowBubblesVertically_tablet() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.showBubblesVertically()).isTrue();
-    }
-
-    /** If a resting position hasn't been set, calling it will return the default position. */
-    @Test
-    public void testGetRestingPosition_returnsDefaultPosition() {
-        DeviceConfig deviceConfig = new ConfigBuilder().build();
-        mPositioner.update(deviceConfig);
-
-        PointF restingPosition = mPositioner.getRestingPosition();
-        PointF defaultPosition = mPositioner.getDefaultStartPosition();
-
-        assertThat(restingPosition).isEqualTo(defaultPosition);
-    }
-
-    /** If a resting position has been set, it'll return that instead of the default position. */
-    @Test
-    public void testGetRestingPosition_returnsRestingPosition() {
-        DeviceConfig deviceConfig = new ConfigBuilder().build();
-        mPositioner.update(deviceConfig);
-
-        PointF restingPosition = new PointF(100, 100);
-        mPositioner.setRestingPosition(restingPosition);
-
-        assertThat(mPositioner.getRestingPosition()).isEqualTo(restingPosition);
-    }
-
-    /** Test that the default resting position on phone is in upper left. */
-    @Test
-    public void testGetRestingPosition_bubble_onPhone() {
-        DeviceConfig deviceConfig = new ConfigBuilder().build();
-        mPositioner.update(deviceConfig);
-
-        RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        PointF restingPosition = mPositioner.getRestingPosition();
-
-        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left);
-        assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
-    }
-
-    @Test
-    public void testGetRestingPosition_bubble_onPhone_RTL() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setRtl().build();
-        mPositioner.update(deviceConfig);
-
-        RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        PointF restingPosition = mPositioner.getRestingPosition();
-
-        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right);
-        assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
-    }
-
-    /** Test that the default resting position on tablet is middle left. */
-    @Test
-    public void testGetRestingPosition_chatBubble_onTablet() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().build();
-        mPositioner.update(deviceConfig);
-
-        RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        PointF restingPosition = mPositioner.getRestingPosition();
-
-        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left);
-        assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
-    }
-
-    @Test
-    public void testGetRestingPosition_chatBubble_onTablet_RTL() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().setRtl().build();
-        mPositioner.update(deviceConfig);
-
-        RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        PointF restingPosition = mPositioner.getRestingPosition();
-
-        assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right);
-        assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
-    }
-
-    /** Test that the default resting position on tablet is middle right. */
-    @Test
-    public void testGetDefaultPosition_appBubble_onTablet() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().build();
-        mPositioner.update(deviceConfig);
-
-        RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */);
-
-        assertThat(startPosition.x).isEqualTo(allowableStackRegion.right);
-        assertThat(startPosition.y).isEqualTo(getDefaultYPosition());
-    }
-
-    @Test
-    public void testGetRestingPosition_appBubble_onTablet_RTL() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().setRtl().build();
-        mPositioner.update(deviceConfig);
-
-        RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */);
-
-        assertThat(startPosition.x).isEqualTo(allowableStackRegion.left);
-        assertThat(startPosition.y).isEqualTo(getDefaultYPosition());
-    }
-
-    @Test
-    public void testHasUserModifiedDefaultPosition_false() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().setRtl().build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse();
-
-        mPositioner.setRestingPosition(mPositioner.getDefaultStartPosition());
-
-        assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse();
-    }
-
-    @Test
-    public void testHasUserModifiedDefaultPosition_true() {
-        DeviceConfig deviceConfig = new ConfigBuilder().setLargeScreen().setRtl().build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse();
-
-        mPositioner.setRestingPosition(new PointF(0, 100));
-
-        assertThat(mPositioner.hasUserModifiedDefaultPosition()).isTrue();
-    }
-
-    @Test
-    public void testGetExpandedViewHeight_max() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        assertThat(mPositioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT);
-    }
-
-    @Test
-    public void testGetExpandedViewHeight_customHeight_valid() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        final int minHeight = mContext.getResources().getDimensionPixelSize(
-                R.dimen.bubble_expanded_default_height);
-        Bubble bubble = new Bubble("key",
-                mock(ShortcutInfo.class),
-                minHeight + 100 /* desiredHeight */,
-                0 /* desiredHeightResId */,
-                "title",
-                0 /* taskId */,
-                null /* locus */,
-                true /* isDismissable */,
-                directExecutor(),
-                mock(Bubbles.BubbleMetadataFlagListener.class));
-
-        // Ensure the height is the same as the desired value
-        assertThat(mPositioner.getExpandedViewHeight(bubble)).isEqualTo(
-                bubble.getDesiredHeight(mContext));
-    }
-
-
-    @Test
-    public void testGetExpandedViewHeight_customHeight_tooSmall() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Bubble bubble = new Bubble("key",
-                mock(ShortcutInfo.class),
-                10 /* desiredHeight */,
-                0 /* desiredHeightResId */,
-                "title",
-                0 /* taskId */,
-                null /* locus */,
-                true /* isDismissable */,
-                directExecutor(),
-                mock(Bubbles.BubbleMetadataFlagListener.class));
-
-        // Ensure the height is the same as the minimum value
-        final int minHeight = mContext.getResources().getDimensionPixelSize(
-                R.dimen.bubble_expanded_default_height);
-        assertThat(mPositioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight);
-    }
-
-    @Test
-    public void testGetMaxExpandedViewHeight_onLargeTablet() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        int manageButtonHeight =
-                mContext.getResources().getDimensionPixelSize(R.dimen.bubble_manage_button_height);
-        int pointerWidth = mContext.getResources().getDimensionPixelSize(
-                R.dimen.bubble_pointer_width);
-        int expandedViewPadding = mContext.getResources().getDimensionPixelSize(R
-                .dimen.bubble_expanded_view_padding);
-        float expectedHeight = 1800 - 2 * 20 - manageButtonHeight - pointerWidth
-                - expandedViewPadding * 2;
-        assertThat(((float) mPositioner.getMaxExpandedViewHeight(false /* isOverflow */)))
-                .isWithin(0.1f).of(expectedHeight);
-    }
-
-    @Test
-    public void testAreBubblesBottomAligned_largeScreen_true() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.areBubblesBottomAligned()).isTrue();
-    }
-
-    @Test
-    public void testAreBubblesBottomAligned_largeScreen_false() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setLandscape()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.areBubblesBottomAligned()).isFalse();
-    }
-
-    @Test
-    public void testAreBubblesBottomAligned_smallTablet_false() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setSmallTablet()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.areBubblesBottomAligned()).isFalse();
-    }
-
-    @Test
-    public void testAreBubblesBottomAligned_phone_false() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        assertThat(mPositioner.areBubblesBottomAligned()).isFalse();
-    }
-
-    @Test
-    public void testExpandedViewY_phoneLandscape() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLandscape()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        // This bubble will have max height so it'll always be top aligned
-        assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
-                .isEqualTo(mPositioner.getExpandedViewYTopAligned());
-    }
-
-    @Test
-    public void testExpandedViewY_phonePortrait() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        // Always top aligned in phone portrait
-        assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
-                .isEqualTo(mPositioner.getExpandedViewYTopAligned());
-    }
-
-    @Test
-    public void testExpandedViewY_smallTabletLandscape() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setSmallTablet()
-                .setLandscape()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        // This bubble will have max height which is always top aligned on small tablets
-        assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
-                .isEqualTo(mPositioner.getExpandedViewYTopAligned());
-    }
-
-    @Test
-    public void testExpandedViewY_smallTabletPortrait() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setSmallTablet()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        // This bubble will have max height which is always top aligned on small tablets
-        assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
-                .isEqualTo(mPositioner.getExpandedViewYTopAligned());
-    }
-
-    @Test
-    public void testExpandedViewY_largeScreenLandscape() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setLandscape()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        // This bubble will have max height which is always top aligned on landscape, large tablet
-        assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
-                .isEqualTo(mPositioner.getExpandedViewYTopAligned());
-    }
-
-    @Test
-    public void testExpandedViewY_largeScreenPortrait() {
-        Insets insets = Insets.of(10, 20, 5, 15);
-        Rect screenBounds = new Rect(0, 0, 1800, 2600);
-
-        DeviceConfig deviceConfig = new ConfigBuilder()
-                .setLargeScreen()
-                .setInsets(insets)
-                .setScreenBounds(screenBounds)
-                .build();
-        mPositioner.update(deviceConfig);
-
-        Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
-        Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor());
-
-        int manageButtonHeight =
-                mContext.getResources().getDimensionPixelSize(R.dimen.bubble_manage_button_height);
-        int manageButtonPlusMargin = manageButtonHeight + 2
-                * mContext.getResources().getDimensionPixelSize(
-                        R.dimen.bubble_manage_button_margin);
-        int pointerWidth = mContext.getResources().getDimensionPixelSize(
-                R.dimen.bubble_pointer_width);
-
-        final float expectedExpandedViewY = mPositioner.getAvailableRect().bottom
-                - manageButtonPlusMargin
-                - mPositioner.getExpandedViewHeightForLargeScreen()
-                - pointerWidth;
-
-        // Bubbles are bottom aligned on portrait, large tablet
-        assertThat(mPositioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
-                .isEqualTo(expectedExpandedViewY);
-    }
-
-    /**
-     * Calculates the Y position bubbles should be placed based on the config. Based on
-     * the calculations in {@link BubblePositioner#getDefaultStartPosition()} and
-     * {@link BubbleStackView.RelativeStackPosition}.
-     */
-    private float getDefaultYPosition() {
-        final boolean isTablet = mPositioner.isLargeScreen();
-
-        // On tablet the position is centered, on phone it is an offset from the top.
-        final float desiredY = isTablet
-                ? mPositioner.getScreenRect().height() / 2f - (mPositioner.getBubbleSize() / 2f)
-                : mContext.getResources().getDimensionPixelOffset(
-                        R.dimen.bubble_stack_starting_offset_y);
-        // Since we're visually centering the bubbles on tablet, use total screen height rather
-        // than the available height.
-        final float height = isTablet
-                ? mPositioner.getScreenRect().height()
-                : mPositioner.getAvailableRect().height();
-        float offsetPercent = desiredY / height;
-        offsetPercent = Math.max(0f, Math.min(1f, offsetPercent));
-        final RectF allowableStackRegion =
-                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
-        return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent;
-    }
-
-    /**
-     * Sets up window manager to return config values based on what you need for the test.
-     * By default it sets up a portrait phone without any insets.
-     */
-    private static class ConfigBuilder {
-        private Rect mScreenBounds = new Rect(0, 0, 1000, 2000);
-        private boolean mIsLargeScreen = false;
-        private boolean mIsSmallTablet = false;
-        private boolean mIsLandscape = false;
-        private boolean mIsRtl = false;
-        private Insets mInsets = Insets.of(0, 0, 0, 0);
-
-        public ConfigBuilder setScreenBounds(Rect screenBounds) {
-            mScreenBounds = screenBounds;
-            return this;
-        }
-
-        public ConfigBuilder setLargeScreen() {
-            mIsLargeScreen = true;
-            return this;
-        }
-
-        public ConfigBuilder setSmallTablet() {
-            mIsSmallTablet = true;
-            return this;
-        }
-
-        public ConfigBuilder setLandscape() {
-            mIsLandscape = true;
-            return this;
-        }
-
-        public ConfigBuilder setRtl() {
-            mIsRtl = true;
-            return this;
-        }
-
-        public ConfigBuilder setInsets(Insets insets) {
-            mInsets = insets;
-            return this;
-        }
-
-        private DeviceConfig build() {
-            return new DeviceConfig(mIsLargeScreen, mIsSmallTablet, mIsLandscape, mIsRtl,
-                    mScreenBounds, mInsets);
-        }
-    }
-}
diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp
index 2f28363..77800a3 100644
--- a/libs/androidfw/Android.bp
+++ b/libs/androidfw/Android.bp
@@ -31,6 +31,12 @@
     ],
 }
 
+cc_aconfig_library {
+    name: "backup_flags_cc_lib",
+    host_supported: true,
+    aconfig_declarations: "backup_flags",
+}
+
 cc_defaults {
     name: "libandroidfw_defaults",
     cpp_std: "gnu++2b",
@@ -115,7 +121,10 @@
                 "libutils",
                 "libz",
             ],
-            static_libs: ["libziparchive_for_incfs"],
+            static_libs: [
+                "libziparchive_for_incfs",
+                "backup_flags_cc_lib",
+            ],
             static: {
                 enabled: false,
             },
diff --git a/libs/androidfw/BackupHelpers.cpp b/libs/androidfw/BackupHelpers.cpp
index 1a6a952..a1e7c2f 100644
--- a/libs/androidfw/BackupHelpers.cpp
+++ b/libs/androidfw/BackupHelpers.cpp
@@ -36,6 +36,9 @@
 #include <utils/KeyedVector.h>
 #include <utils/String8.h>
 
+#include <com_android_server_backup.h>
+namespace backup_flags = com::android::server::backup;
+
 namespace android {
 
 #define MAGIC0 0x70616e53 // Snap
@@ -214,7 +217,7 @@
 {
     LOGP("write_update_file %s (%s) : mode 0%o\n", realFilename, key.c_str(), mode);
 
-    const int bufsize = 4*1024;
+    const int bufsize = backup_flags::enable_max_size_writes_to_pipes() ? (64*1024) : (4*1024);
     int err;
     int amt;
     int fileSize;
@@ -550,7 +553,8 @@
     }
 
     // read/write up to this much at a time.
-    const size_t BUFSIZE = 32 * 1024;
+    const size_t BUFSIZE = backup_flags::enable_max_size_writes_to_pipes() ? (64*1024) : (32*1024);
+
     char* buf = (char *)calloc(1,BUFSIZE);
     const size_t PAXHEADER_OFFSET = 512;
     const size_t PAXHEADER_SIZE = 512;
@@ -726,7 +730,7 @@
 
 
 
-#define RESTORE_BUF_SIZE (8*1024)
+const size_t RESTORE_BUF_SIZE = backup_flags::enable_max_size_writes_to_pipes() ? 64*1024 : 8*1024;
 
 RestoreHelperBase::RestoreHelperBase()
 {
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index c156c46..72ddecc 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -22,6 +22,13 @@
 }
 
 flag {
+  name: "high_contrast_text_small_text_rect"
+  namespace: "accessibility"
+  description: "Draw a solid rectangle background behind text instead of a stroke outline"
+  bug: "186567103"
+}
+
+flag {
   name: "hdr_10bit_plus"
   namespace: "core_graphics"
   description: "Use 10101010 and FP16 formats for HDR-UI when available"
diff --git a/libs/hwui/hwui/Canvas.cpp b/libs/hwui/hwui/Canvas.cpp
index 80b6c03..e9f4b81c 100644
--- a/libs/hwui/hwui/Canvas.cpp
+++ b/libs/hwui/hwui/Canvas.cpp
@@ -18,6 +18,7 @@
 
 #include <SkFontMetrics.h>
 #include <SkRRect.h>
+#include <minikin/MinikinRect.h>
 
 #include "FeatureFlags.h"
 #include "MinikinUtils.h"
@@ -107,7 +108,13 @@
     // care of all alignment.
     paint.setTextAlign(Paint::kLeft_Align);
 
-    DrawTextFunctor f(layout, this, paint, x, y, layout.getAdvance());
+    minikin::MinikinRect bounds;
+    // We only need the bounds to draw a rectangular background in high contrast mode. Let's save
+    // the cycles otherwise.
+    if (flags::high_contrast_text_small_text_rect() && isHighContrastText()) {
+        MinikinUtils::getBounds(&paint, bidiFlags, typeface, text, textSize, &bounds);
+    }
+    DrawTextFunctor f(layout, this, paint, x, y, layout.getAdvance(), bounds);
     MinikinUtils::forFontRun(layout, &paint, f);
 
     if (text_feature::fix_double_underline()) {
diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h
index 8f99990..ba65439 100644
--- a/libs/hwui/hwui/DrawTextFunctor.h
+++ b/libs/hwui/hwui/DrawTextFunctor.h
@@ -33,6 +33,8 @@
 
 namespace android {
 
+inline constexpr int kHighContrastTextBorderWidth = 4;
+
 static inline void drawStroke(SkScalar left, SkScalar right, SkScalar top, SkScalar thickness,
                               const Paint& paint, Canvas* canvas) {
     const SkScalar strokeWidth = fmax(thickness, 1.0f);
@@ -45,15 +47,26 @@
     paint->setShader(nullptr);
     paint->setColorFilter(nullptr);
     paint->setLooper(nullptr);
-    paint->setStrokeWidth(4 + 0.04 * paint->getSkFont().getSize());
+    paint->setStrokeWidth(kHighContrastTextBorderWidth + 0.04 * paint->getSkFont().getSize());
     paint->setStrokeJoin(SkPaint::kRound_Join);
     paint->setLooper(nullptr);
 }
 
 class DrawTextFunctor {
 public:
+    /**
+     * Creates a Functor to draw the given text layout.
+     *
+     * @param layout
+     * @param canvas
+     * @param paint
+     * @param x
+     * @param y
+     * @param totalAdvance
+     * @param bounds bounds of the text. Only required if high contrast text mode is enabled.
+     */
     DrawTextFunctor(const minikin::Layout& layout, Canvas* canvas, const Paint& paint, float x,
-                    float y, float totalAdvance)
+                    float y, float totalAdvance, const minikin::MinikinRect& bounds)
             : layout(layout)
             , canvas(canvas)
             , paint(paint)
@@ -61,7 +74,8 @@
             , y(y)
             , totalAdvance(totalAdvance)
             , underlinePosition(0)
-            , underlineThickness(0) {}
+            , underlineThickness(0)
+            , bounds(bounds) {}
 
     void operator()(size_t start, size_t end) {
         auto glyphFunc = [&](uint16_t* text, float* positions) {
@@ -91,7 +105,16 @@
             Paint outlinePaint(paint);
             simplifyPaint(darken ? SK_ColorWHITE : SK_ColorBLACK, &outlinePaint);
             outlinePaint.setStyle(SkPaint::kStrokeAndFill_Style);
-            canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance);
+            if (flags::high_contrast_text_small_text_rect()) {
+                auto bgBounds(bounds);
+                auto padding = kHighContrastTextBorderWidth + 0.1f * paint.getSkFont().getSize();
+                bgBounds.offset(x, y);
+                canvas->drawRect(bgBounds.mLeft - padding, bgBounds.mTop - padding,
+                                 bgBounds.mRight + padding, bgBounds.mBottom + padding,
+                                 outlinePaint);
+            } else {
+                canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance);
+            }
 
             // inner
             gDrawTextBlobMode = DrawTextBlobMode::HctInner;
@@ -146,6 +169,7 @@
     float totalAdvance;
     float underlinePosition;
     float underlineThickness;
+    const minikin::MinikinRect& bounds;
 };
 
 }  // namespace android
diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp
index 7552b56d..833069f 100644
--- a/libs/hwui/hwui/MinikinUtils.cpp
+++ b/libs/hwui/hwui/MinikinUtils.cpp
@@ -96,7 +96,7 @@
 float MinikinUtils::measureText(const Paint* paint, minikin::Bidi bidiFlags,
                                 const Typeface* typeface, const uint16_t* buf, size_t start,
                                 size_t count, size_t bufSize, float* advances,
-                                minikin::MinikinRect* bounds) {
+                                minikin::MinikinRect* bounds, uint32_t* clusterCount) {
     minikin::MinikinPaint minikinPaint = prepareMinikinPaint(paint, typeface);
     const minikin::U16StringPiece textBuf(buf, bufSize);
     const minikin::Range range(start, start + count);
@@ -104,7 +104,7 @@
     const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit();
 
     return minikin::Layout::measureText(textBuf, range, bidiFlags, minikinPaint, startHyphen,
-                                        endHyphen, advances, bounds);
+                                        endHyphen, advances, bounds, clusterCount);
 }
 
 minikin::MinikinExtent MinikinUtils::getFontExtent(const Paint* paint, minikin::Bidi bidiFlags,
diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h
index 61bc881..f8574ee 100644
--- a/libs/hwui/hwui/MinikinUtils.h
+++ b/libs/hwui/hwui/MinikinUtils.h
@@ -53,7 +53,7 @@
 
     static float measureText(const Paint* paint, minikin::Bidi bidiFlags, const Typeface* typeface,
                              const uint16_t* buf, size_t start, size_t count, size_t bufSize,
-                             float* advances, minikin::MinikinRect* bounds);
+                             float* advances, minikin::MinikinRect* bounds, uint32_t* clusterCount);
 
     static minikin::MinikinExtent getFontExtent(const Paint* paint, minikin::Bidi bidiFlags,
                                                 const Typeface* typeface, const uint16_t* buf,
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index 7cc4866..8315c4c 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -247,6 +247,9 @@
 static jfieldID gFontMetricsInt_bottom;
 static jfieldID gFontMetricsInt_leading;
 
+static jclass gRunInfo_class;
+static jfieldID gRunInfo_clusterCount;
+
 ///////////////////////////////////////////////////////////////////////////////
 
 void GraphicsJNI::get_jrect(JNIEnv* env, jobject obj, int* L, int* T, int* R, int* B)
@@ -511,6 +514,10 @@
     return descent - ascent + leading;
 }
 
+void GraphicsJNI::set_cluster_count_to_run_info(JNIEnv* env, jobject runInfo, jint clusterCount) {
+    env->SetIntField(runInfo, gRunInfo_clusterCount, clusterCount);
+}
+
 ///////////////////////////////////////////////////////////////////////////////////////////
 
 jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, BitmapRegionDecoderWrapper* bitmap) {
@@ -834,5 +841,10 @@
     gFontMetricsInt_bottom = GetFieldIDOrDie(env, gFontMetricsInt_class, "bottom", "I");
     gFontMetricsInt_leading = GetFieldIDOrDie(env, gFontMetricsInt_class, "leading", "I");
 
+    gRunInfo_class = FindClassOrDie(env, "android/graphics/Paint$RunInfo");
+    gRunInfo_class = MakeGlobalRefOrDie(env, gRunInfo_class);
+
+    gRunInfo_clusterCount = GetFieldIDOrDie(env, gRunInfo_class, "mClusterCount", "I");
+
     return 0;
 }
diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h
index b9fff36..b0a1074 100644
--- a/libs/hwui/jni/GraphicsJNI.h
+++ b/libs/hwui/jni/GraphicsJNI.h
@@ -77,6 +77,8 @@
     static SkRect* jrect_to_rect(JNIEnv*, jobject jrect, SkRect*);
     static void rect_to_jrectf(const SkRect&, JNIEnv*, jobject jrectf);
 
+    static void set_cluster_count_to_run_info(JNIEnv* env, jobject runInfo, jint clusterCount);
+
     static void set_jpoint(JNIEnv*, jobject jrect, int x, int y);
 
     static SkIPoint* jpoint_to_ipoint(JNIEnv*, jobject jpoint, SkIPoint* point);
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index d84b73d..58d9d8b 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -114,7 +114,7 @@
 
         std::unique_ptr<float[]> advancesArray(new float[count]);
         MinikinUtils::measureText(&paint, static_cast<minikin::Bidi>(bidiFlags), typeface, text, 0,
-                                  count, count, advancesArray.get(), nullptr);
+                                  count, count, advancesArray.get(), nullptr, nullptr);
 
         for (int i = 0; i < count; i++) {
             // traverse in the given direction
@@ -206,7 +206,7 @@
         }
         const float advance = MinikinUtils::measureText(
                 paint, static_cast<minikin::Bidi>(bidiFlags), typeface, text, start, count,
-                contextCount, advancesArray.get(), nullptr);
+                contextCount, advancesArray.get(), nullptr, nullptr);
         if (advances) {
             env->SetFloatArrayRegion(advances, advancesIndex, count, advancesArray.get());
         }
@@ -244,7 +244,7 @@
         minikin::Bidi bidiFlags = dir == 1 ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
         std::unique_ptr<float[]> advancesArray(new float[count]);
         MinikinUtils::measureText(paint, bidiFlags, typeface, text, start, count, start + count,
-                                  advancesArray.get(), nullptr);
+                                  advancesArray.get(), nullptr, nullptr);
         size_t result = minikin::GraphemeBreak::getTextRunCursor(advancesArray.get(), text,
                 start, count, offset, moveOpt);
         return static_cast<jint>(result);
@@ -508,7 +508,7 @@
     static jfloat doRunAdvance(JNIEnv* env, const Paint* paint, const Typeface* typeface,
                                const jchar buf[], jint start, jint count, jint bufSize,
                                jboolean isRtl, jint offset, jfloatArray advances,
-                               jint advancesIndex, SkRect* drawBounds) {
+                               jint advancesIndex, SkRect* drawBounds, uint32_t* clusterCount) {
         if (advances) {
             size_t advancesLength = env->GetArrayLength(advances);
             if ((size_t)(count + advancesIndex) > advancesLength) {
@@ -519,9 +519,9 @@
         minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
         minikin::MinikinRect bounds;
         if (offset == start + count && advances == nullptr) {
-            float result =
-                    MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count,
-                                              bufSize, nullptr, drawBounds ? &bounds : nullptr);
+            float result = MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count,
+                                                     bufSize, nullptr,
+                                                     drawBounds ? &bounds : nullptr, clusterCount);
             if (drawBounds) {
                 copyMinikinRectToSkRect(bounds, drawBounds);
             }
@@ -529,7 +529,8 @@
         }
         std::unique_ptr<float[]> advancesArray(new float[count]);
         MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count, bufSize,
-                                  advancesArray.get(), drawBounds ? &bounds : nullptr);
+                                  advancesArray.get(), drawBounds ? &bounds : nullptr,
+                                  clusterCount);
 
         if (drawBounds) {
             copyMinikinRectToSkRect(bounds, drawBounds);
@@ -549,7 +550,7 @@
         ScopedCharArrayRO textArray(env, text);
         jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart,
                                      start - contextStart, end - start, contextEnd - contextStart,
-                                     isRtl, offset - contextStart, nullptr, 0, nullptr);
+                                     isRtl, offset - contextStart, nullptr, 0, nullptr, nullptr);
         return result;
     }
 
@@ -558,27 +559,41 @@
                                                         jint contextStart, jint contextEnd,
                                                         jboolean isRtl, jint offset,
                                                         jfloatArray advances, jint advancesIndex,
-                                                        jobject drawBounds) {
+                                                        jobject drawBounds, jobject runInfo) {
         const Paint* paint = reinterpret_cast<Paint*>(paintHandle);
         const Typeface* typeface = paint->getAndroidTypeface();
         ScopedCharArrayRO textArray(env, text);
         SkRect skDrawBounds;
+        uint32_t clusterCount = 0;
         jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart,
                                      start - contextStart, end - start, contextEnd - contextStart,
                                      isRtl, offset - contextStart, advances, advancesIndex,
-                                     drawBounds ? &skDrawBounds : nullptr);
+                                     drawBounds ? &skDrawBounds : nullptr, &clusterCount);
         if (drawBounds != nullptr) {
             GraphicsJNI::rect_to_jrectf(skDrawBounds, env, drawBounds);
         }
+        if (runInfo) {
+            GraphicsJNI::set_cluster_count_to_run_info(env, runInfo, clusterCount);
+        }
         return result;
     }
 
+    // This method is kept for old Robolectric JNI signature used by SystemUIGoogleRoboRNGTests.
+    static jfloat getRunCharacterAdvance___CIIIIZI_FI_F_ForRobolectric(
+            JNIEnv* env, jclass cls, jlong paintHandle, jcharArray text, jint start, jint end,
+            jint contextStart, jint contextEnd, jboolean isRtl, jint offset, jfloatArray advances,
+            jint advancesIndex, jobject drawBounds) {
+        return getRunCharacterAdvance___CIIIIZI_FI_F(env, cls, paintHandle, text, start, end,
+                                                     contextStart, contextEnd, isRtl, offset,
+                                                     advances, advancesIndex, drawBounds, nullptr);
+    }
+
     static jint doOffsetForAdvance(const Paint* paint, const Typeface* typeface, const jchar buf[],
             jint start, jint count, jint bufSize, jboolean isRtl, jfloat advance) {
         minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
         std::unique_ptr<float[]> advancesArray(new float[count]);
         MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count, bufSize,
-                                  advancesArray.get(), nullptr);
+                                  advancesArray.get(), nullptr, nullptr);
         return minikin::getOffsetForAdvance(advancesArray.get(), buf, start, count, advance);
     }
 
@@ -1145,8 +1160,11 @@
          (void*)PaintGlue::getCharArrayBounds},
         {"nHasGlyph", "(JILjava/lang/String;)Z", (void*)PaintGlue::hasGlyph},
         {"nGetRunAdvance", "(J[CIIIIZI)F", (void*)PaintGlue::getRunAdvance___CIIIIZI_F},
-        {"nGetRunCharacterAdvance", "(J[CIIIIZI[FILandroid/graphics/RectF;)F",
+        {"nGetRunCharacterAdvance",
+         "(J[CIIIIZI[FILandroid/graphics/RectF;Landroid/graphics/Paint$RunInfo;)F",
          (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F},
+        {"nGetRunCharacterAdvance", "(J[CIIIIZI[FILandroid/graphics/RectF;)F",
+         (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F_ForRobolectric},
         {"nGetOffsetForAdvance", "(J[CIIIIZF)I", (void*)PaintGlue::getOffsetForAdvance___CIIIIZF_I},
         {"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
          (void*)PaintGlue::getFontMetricsIntForText___C},
diff --git a/libs/hwui/tests/unit/UnderlineTest.cpp b/libs/hwui/tests/unit/UnderlineTest.cpp
index c70a304..9911bfa 100644
--- a/libs/hwui/tests/unit/UnderlineTest.cpp
+++ b/libs/hwui/tests/unit/UnderlineTest.cpp
@@ -103,8 +103,9 @@
     // Create minikin::Layout
     std::unique_ptr<Typeface> typeface(makeTypeface());
     minikin::Layout layout = doLayout(text, *paint, typeface.get());
+    minikin::MinikinRect bounds;
 
-    DrawTextFunctor f(layout, &canvas, *paint, 0, 0, layout.getAdvance());
+    DrawTextFunctor f(layout, &canvas, *paint, 0, 0, layout.getAdvance(), bounds);
     MinikinUtils::forFontRun(layout, paint, f);
     return f;
 }
diff --git a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
index ee2510f..0d5af50 100644
--- a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
+++ b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
@@ -19,6 +19,7 @@
 import android.Manifest;
 import android.annotation.RequiresPermission;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.location.LocationManager;
 import android.os.SystemClock;
 import android.telephony.TelephonyCallback;
@@ -26,6 +27,8 @@
 import android.telephony.emergency.EmergencyNumber;
 import android.util.Log;
 
+import com.android.internal.telephony.flags.Flags;
+
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -139,8 +142,20 @@
                 (mCallEndElapsedRealtimeMillis > 0)
                         && ((SystemClock.elapsedRealtime() - mCallEndElapsedRealtimeMillis)
                         < emergencyExtensionMillis);
-        boolean isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
-        boolean isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
+        boolean isInEmergencyCallback = false;
+        boolean isInEmergencySmsMode = false;
+        if (!Flags.enforceTelephonyFeatureMappingForPublicApis()) {
+            isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
+            isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
+        } else {
+            PackageManager pm = mContext.getPackageManager();
+            if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
+                isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
+            }
+            if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
+                isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
+            }
+        }
         return mIsInEmergencyCall || isInEmergencyCallback || isInEmergencyExtension
                 || isInEmergencySmsMode;
     }
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 89792c7..8f8cd0b 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -48,6 +48,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -141,7 +142,9 @@
      * dispatch. This is only used to determine what callback a route should be assigned to (added,
      * removed, changed) in {@link #dispatchFilteredRoutesUpdatedOnHandler(List)}.
      */
-    private volatile ArrayMap<String, MediaRoute2Info> mPreviousRoutes = new ArrayMap<>();
+    private volatile ArrayMap<String, MediaRoute2Info> mPreviousFilteredRoutes = new ArrayMap<>();
+
+    private final Map<String, MediaRoute2Info> mPreviousUnfilteredRoutes = new ArrayMap<>();
 
     /**
      * Stores the latest copy of exposed routes after filtering, sorting, and deduplication. Can be
@@ -282,6 +285,8 @@
             MediaRouter2 instance = sAppToProxyRouterMap.get(key);
             if (instance == null) {
                 instance = new MediaRouter2(context, looper, clientPackageName, user);
+                // Register proxy router after instantiation to avoid race condition.
+                ((ProxyMediaRouter2Impl) instance.mImpl).registerProxyRouter();
                 sAppToProxyRouterMap.put(key, instance);
             }
             return instance;
@@ -368,6 +373,7 @@
                 new SystemRoutingController(
                         ProxyMediaRouter2Impl.getSystemSessionInfoImpl(
                                 mMediaRouterService, clientPackageName));
+
         mImpl = new ProxyMediaRouter2Impl(context, clientPackageName, user);
     }
 
@@ -883,7 +889,7 @@
                 newRoutes.stream().map(MediaRoute2Info::getId).collect(Collectors.toSet());
 
         for (MediaRoute2Info route : newRoutes) {
-            MediaRoute2Info prevRoute = mPreviousRoutes.get(route.getId());
+            MediaRoute2Info prevRoute = mPreviousFilteredRoutes.get(route.getId());
             if (prevRoute == null) {
                 addedRoutes.add(route);
             } else if (!prevRoute.equals(route)) {
@@ -891,21 +897,21 @@
             }
         }
 
-        for (int i = 0; i < mPreviousRoutes.size(); i++) {
-            if (!newRouteIds.contains(mPreviousRoutes.keyAt(i))) {
-                removedRoutes.add(mPreviousRoutes.valueAt(i));
+        for (int i = 0; i < mPreviousFilteredRoutes.size(); i++) {
+            if (!newRouteIds.contains(mPreviousFilteredRoutes.keyAt(i))) {
+                removedRoutes.add(mPreviousFilteredRoutes.valueAt(i));
             }
         }
 
         // update previous routes
         for (MediaRoute2Info route : removedRoutes) {
-            mPreviousRoutes.remove(route.getId());
+            mPreviousFilteredRoutes.remove(route.getId());
         }
         for (MediaRoute2Info route : addedRoutes) {
-            mPreviousRoutes.put(route.getId(), route);
+            mPreviousFilteredRoutes.put(route.getId(), route);
         }
         for (MediaRoute2Info route : changedRoutes) {
-            mPreviousRoutes.put(route.getId(), route);
+            mPreviousFilteredRoutes.put(route.getId(), route);
         }
 
         if (!addedRoutes.isEmpty()) {
@@ -924,6 +930,27 @@
         }
     }
 
+    void dispatchControllerUpdatedIfNeededOnHandler(Map<String, MediaRoute2Info> routesMap) {
+        List<RoutingController> controllers = getControllers();
+        for (RoutingController controller : controllers) {
+
+            for (String selectedRoute : controller.getRoutingSessionInfo().getSelectedRoutes()) {
+                if (routesMap.containsKey(selectedRoute)
+                        && mPreviousUnfilteredRoutes.containsKey(selectedRoute)) {
+                    MediaRoute2Info currentRoute = routesMap.get(selectedRoute);
+                    MediaRoute2Info oldRoute = mPreviousUnfilteredRoutes.get(selectedRoute);
+                    if (!currentRoute.equals(oldRoute)) {
+                        notifyControllerUpdated(controller);
+                        break;
+                    }
+                }
+            }
+        }
+
+        mPreviousUnfilteredRoutes.clear();
+        mPreviousUnfilteredRoutes.putAll(routesMap);
+    }
+
     void updateRoutesOnHandler(List<MediaRoute2Info> newRoutes) {
         synchronized (mLock) {
             mRoutes.clear();
@@ -945,6 +972,11 @@
                         MediaRouter2::dispatchFilteredRoutesUpdatedOnHandler,
                         this,
                         mFilteredRoutes));
+        mHandler.sendMessage(
+                obtainMessage(
+                        MediaRouter2::dispatchControllerUpdatedIfNeededOnHandler,
+                        this,
+                        new HashMap<>(mRoutes)));
     }
 
     /**
@@ -2153,18 +2185,19 @@
             mClientUser = user;
             mClientPackageName = clientPackageName;
             mClient = new Client();
+            mDiscoveryPreference = RouteDiscoveryPreference.EMPTY;
+        }
 
+        public void registerProxyRouter() {
             try {
                 mMediaRouterService.registerProxyRouter(
                         mClient,
-                        context.getApplicationContext().getPackageName(),
-                        clientPackageName,
-                        user);
+                        mContext.getApplicationContext().getPackageName(),
+                        mClientPackageName,
+                        mClientUser);
             } catch (RemoteException ex) {
                 throw ex.rethrowFromSystemServer();
             }
-
-            mDiscoveryPreference = RouteDiscoveryPreference.EMPTY;
         }
 
         @Override
diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl
index a7ec6c6..8ce1b6d 100644
--- a/media/java/android/media/projection/IMediaProjectionManager.aidl
+++ b/media/java/android/media/projection/IMediaProjectionManager.aidl
@@ -37,31 +37,41 @@
     const String EXTRA_PACKAGE_REUSING_GRANTED_CONSENT =
             "extra_media_projection_package_reusing_consent";
 
+    /**
+     * Returns whether a combination of process UID and package has the projection permission.
+     *
+     * @param processUid the process UID as returned by {@link android.os.Process.myUid()}.
+     */
     @UnsupportedAppUsage
-    boolean hasProjectionPermission(int uid, String packageName);
+    boolean hasProjectionPermission(int processUid, String packageName);
 
     /**
      * Returns a new {@link IMediaProjection} instance associated with the given package.
+     *
+     * @param processUid the process UID as returned by {@link android.os.Process.myUid()}.
      */
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    IMediaProjection createProjection(int uid, String packageName, int type,
+    IMediaProjection createProjection(int processUid, String packageName, int type,
             boolean permanentGrant);
 
     /**
      * Returns the current {@link IMediaProjection} instance associated with the given
-     * package, or {@code null} if it is not possible to re-use the current projection.
+     * package and process UID, or {@code null} if it is not possible to re-use the current
+     * projection.
      *
      * <p>Should only be invoked when the user has reviewed consent for a re-used projection token.
      * Requires that there is a prior session waiting for the user to review consent, and the given
      * package details match those on the current projection.
      *
      * @see {@link #isCurrentProjection}
+     *
+     * @param processUid the process UID as returned by {@link android.os.Process.myUid()}.
      */
     @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    IMediaProjection getProjection(int uid, String packageName);
+    IMediaProjection getProjection(int processUid, String packageName);
 
     /**
      * Returns {@code true} if the given {@link IMediaProjection} corresponds to the current
@@ -162,8 +172,8 @@
      *
      * <p>Only used for emitting atoms.
      *
-     * @param hostUid               The uid of the process requesting consent to capture, may be an app or
-     *                              SystemUI.
+     * @param hostProcessUid        The uid of the process requesting consent to capture, may be an
+     *                              app or SystemUI.
      * @param sessionCreationSource Only set if the state is MEDIA_PROJECTION_STATE_INITIATED.
      *                              Indicates the entry point for requesting the permission. Must be
      *                              a valid state defined
@@ -172,49 +182,49 @@
     @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    oneway void notifyPermissionRequestInitiated(int hostUid, int sessionCreationSource);
+    oneway void notifyPermissionRequestInitiated(int hostProcessUid, int sessionCreationSource);
 
     /**
      * Notifies system server that the permission request was displayed.
      *
      * <p>Only used for emitting atoms.
      *
-     * @param hostUid The uid of the process requesting consent to capture, may be an app or
-     *                SystemUI.
+     * @param hostProcessUid The uid of the process requesting consent to capture, may be an app or
+     *                       SystemUI.
      */
     @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    oneway void notifyPermissionRequestDisplayed(int hostUid);
+    oneway void notifyPermissionRequestDisplayed(int hostProcessUid);
 
     /**
      * Notifies system server that the permission request was cancelled.
      *
      * <p>Only used for emitting atoms.
      *
-     * @param hostUid The uid of the process requesting consent to capture, may be an app or
-     *                SystemUI.
+     * @param hostProcessUid The uid of the process requesting consent to capture, may be an app or
+     *                       SystemUI.
      */
     @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    oneway void notifyPermissionRequestCancelled(int hostUid);
+    oneway void notifyPermissionRequestCancelled(int hostProcessUid);
 
     /**
      * Notifies system server that the app selector was displayed.
      *
      * <p>Only used for emitting atoms.
      *
-     * @param hostUid The uid of the process requesting consent to capture, may be an app or
-     *                SystemUI.
+     * @param hostProcessUid The uid of the process requesting consent to capture, may be an app or
+     *                       SystemUI.
      */
     @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    oneway void notifyAppSelectorDisplayed(int hostUid);
+    oneway void notifyAppSelectorDisplayed(int hostProcessUid);
 
     @EnforcePermission("MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    void notifyWindowingModeChanged(int contentToRecord, int targetUid, int windowingMode);
+    void notifyWindowingModeChanged(int contentToRecord, int targetProcessUid, int windowingMode);
 }
diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
index 262f5f1..096e8ad 100644
--- a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
+++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
@@ -50,8 +50,16 @@
     private static final String TAG = "BluetoothMidiDevice";
     private static final boolean DEBUG = false;
 
-    private static final int DEFAULT_PACKET_SIZE = 20;
-    private static final int MAX_PACKET_SIZE = 512;
+    // Bluetooth services should subtract 5 bytes from the MTU for headers.
+    private static final int HEADER_SIZE = 5;
+    // Min MTU size for BLE
+    private static final int MIN_L2CAP_MTU = 23;
+    // 23 (min L2CAP MTU) - 5 (header size)
+    private static final int DEFAULT_PACKET_SIZE = MIN_L2CAP_MTU - HEADER_SIZE;
+    // Max MTU size on Android
+    private static final int MAX_ANDROID_MTU = 517;
+    // 517 (max Android MTU) - 5 (header size)
+    private static final int MAX_PACKET_SIZE = MAX_ANDROID_MTU - HEADER_SIZE;
 
     //  Bluetooth MIDI Gatt service UUID
     private static final UUID MIDI_SERVICE = UUID.fromString(
@@ -135,8 +143,8 @@
                         // switch to receiving notifications
                         mBluetoothGatt.readCharacteristic(characteristic);
 
-                        // Request higher MTU size
-                        if (!gatt.requestMtu(MAX_PACKET_SIZE)) {
+                        // Request max MTU size
+                        if (!gatt.requestMtu(MAX_ANDROID_MTU)) {
                             Log.e(TAG, "request mtu failed");
                             mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
                             mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
@@ -204,8 +212,15 @@
         public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
             Log.d(TAG, "onMtuChanged callback received. mtu: " + mtu + ", status: " + status);
             if (status == BluetoothGatt.GATT_SUCCESS) {
-                mPacketEncoder.setMaxPacketSize(Math.min(mtu, MAX_PACKET_SIZE));
-                mPacketDecoder.setMaxPacketSize(Math.min(mtu, MAX_PACKET_SIZE));
+                int packetSize = Math.min(mtu - HEADER_SIZE, MAX_PACKET_SIZE);
+                if (packetSize <= 0) {
+                    Log.e(TAG, "onMtuChanged non-positive packet size: " + packetSize);
+                    packetSize = DEFAULT_PACKET_SIZE;
+                } else if (packetSize < DEFAULT_PACKET_SIZE) {
+                    Log.w(TAG, "onMtuChanged small packet size: " + packetSize);
+                }
+                mPacketEncoder.setMaxPacketSize(packetSize);
+                mPacketDecoder.setMaxPacketSize(packetSize);
             } else {
                 mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
                 mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
diff --git a/packages/CredentialManager/res/drawable/autofill_light_selectable_item_background.xml b/packages/CredentialManager/res/drawable/autofill_light_selectable_item_background.xml
deleted file mode 100644
index 9d16f32d..0000000
--- a/packages/CredentialManager/res/drawable/autofill_light_selectable_item_background.xml
+++ /dev/null
@@ -1,25 +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.
-  -->
-
-<!-- Copied from //frameworks/base/core/res/res/drawable/item_background_material.xml -->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
-        android:color="@color/autofill_light_colorControlHighlight">
-    <item android:id="@android:id/mask">
-        <color android:color="@android:color/white"/>
-    </item>
-</ripple>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml b/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml
index 2f0c83b..5becc86 100644
--- a/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml
+++ b/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2023 The Android Open Source Project
+  ~ Copyright (C) 2024 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -23,8 +23,8 @@
         android:shape="rectangle"
         android:top="1dp">
         <shape>
-            <corners android:radius="28dp" />
-            <solid android:color="@android:color/system_surface_container_high_light" />
+            <corners android:radius="16dp" />
+            <solid android:color="@color/dropdown_container" />
         </shape>
     </item>
 </ripple>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/layout/autofill_dataset_left_with_item_tag_hint.xml b/packages/CredentialManager/res/layout/autofill_dataset_left_with_item_tag_hint.xml
deleted file mode 100644
index e4e9f7a..0000000
--- a/packages/CredentialManager/res/layout/autofill_dataset_left_with_item_tag_hint.xml
+++ /dev/null
@@ -1,37 +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.
-  -->
-
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                android:id="@android:id/content"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                style="@style/autofill.Dataset">
-    <ImageView
-        android:id="@android:id/icon1"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_centerVertical="true"
-        android:layout_alignParentStart="true"
-        android:background="@null"/>
-    <TextView
-        android:id="@android:id/text1"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentTop="true"
-        android:layout_toEndOf="@android:id/icon1"
-        style="@style/autofill.TextAppearance"/>
-
-</RelativeLayout>
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
new file mode 100644
index 0000000..cb6c6b4
--- /dev/null
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -0,0 +1,45 @@
+<!--
+  ~ 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.
+  -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:id="@android:id/content"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:maxWidth="@dimen/autofill_dropdown_layout_width"
+                android:elevation="3dp">
+
+        <ImageView
+            android:id="@android:id/icon1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerVertical="true"
+            android:layout_alignParentStart="true"
+            android:background="@null"/>
+        <TextView
+            android:id="@android:id/text1"
+            android:layout_width="@dimen/autofill_dropdown_text_width"
+            android:layout_height="wrap_content"
+            android:layout_alignParentTop="true"
+            android:layout_toEndOf="@android:id/icon1"
+            style="@style/autofill.TextTitle"/>
+        <TextView
+            android:id="@android:id/text2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@android:id/text1"
+            android:layout_toEndOf="@android:id/icon1"
+            style="@style/autofill.TextSubtitle"/>
+
+</RelativeLayout>
diff --git a/packages/CredentialManager/res/values/colors.xml b/packages/CredentialManager/res/values/colors.xml
index 63b9f24..dcb7ef9 100644
--- a/packages/CredentialManager/res/values/colors.xml
+++ b/packages/CredentialManager/res/values/colors.xml
@@ -16,23 +16,8 @@
 
 <!-- Color palette -->
 <resources>
-    <color name="autofill_light_colorPrimary">@color/primary_material_light</color>
-    <color name="autofill_light_colorAccent">@color/accent_material_light</color>
-    <color name="autofill_light_colorControlHighlight">@color/ripple_material_light</color>
-    <color name="autofill_light_colorButtonNormal">@color/button_material_light</color>
-
-    <!-- Text colors -->
-    <color name="autofill_light_textColorPrimary">@color/abc_primary_text_material_light</color>
-    <color name="autofill_light_textColorSecondary">@color/abc_secondary_text_material_light</color>
-    <color name="autofill_light_textColorHint">@color/abc_hint_foreground_material_light</color>
-    <color name="autofill_light_textColorHintInverse">@color/abc_hint_foreground_material_dark
-    </color>
-    <color name="autofill_light_textColorHighlight">@color/highlighted_text_material_light</color>
-    <color name="autofill_light_textColorLink">@color/autofill_light_colorAccent</color>
-
     <!-- These colors are used for Remote Views. -->
-    <color name="background_dark_mode">#0E0C0B</color>
-    <color name="background">#F1F3F4</color>
-    <color name="text_primary_dark_mode">#DFDEDB</color>
-    <color name="text_primary">#202124</color>
+    <color name="text_primary">#1A1B20</color>
+    <color name="text_secondary">#44474F</color>
+    <color name="dropdown_container">#F3F3FA</color>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/dimens.xml b/packages/CredentialManager/res/values/dimens.xml
index 67003a3..2a4719d 100644
--- a/packages/CredentialManager/res/values/dimens.xml
+++ b/packages/CredentialManager/res/values/dimens.xml
@@ -17,6 +17,12 @@
   -->
 
 <resources>
-    <dimen name="autofill_view_padding">16dp</dimen>
-    <dimen name="autofill_icon_size">16dp</dimen>
+    <dimen name="autofill_view_top_padding">12dp</dimen>
+    <dimen name="autofill_view_right_padding">24dp</dimen>
+    <dimen name="autofill_view_bottom_padding">12dp</dimen>
+    <dimen name="autofill_view_left_padding">16dp</dimen>
+    <dimen name="autofill_view_icon_to_text_padding">10dp</dimen>
+    <dimen name="autofill_icon_size">24dp</dimen>
+    <dimen name="autofill_dropdown_layout_width">296dp</dimen>
+    <dimen name="autofill_dropdown_text_width">240dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/styles.xml b/packages/CredentialManager/res/values/styles.xml
index 4a5761a..7de941e 100644
--- a/packages/CredentialManager/res/values/styles.xml
+++ b/packages/CredentialManager/res/values/styles.xml
@@ -15,24 +15,13 @@
   -->
 
 <resources>
-    <style name="autofill.TextAppearance.Small" parent="@style/autofill.TextAppearance">
-        <item name="android:textSize">12sp</item>
-    </style>
-
-
-    <style name="autofill.Dataset" parent="">
-        <item name="android:background">@drawable/autofill_light_selectable_item_background</item>
-    </style>
-
-    <style name="autofill.TextAppearance" parent="">
-        <item name="android:textColor">@color/autofill_light_textColorPrimary</item>
-        <item name="android:textColorHint">@color/autofill_light_textColorHint</item>
-        <item name="android:textColorHighlight">@color/autofill_light_textColorHighlight</item>
-        <item name="android:textColorLink">@color/autofill_light_textColorLink</item>
+    <style name="autofill.TextTitle" parent="">
+        <item name="android:fontFamily">google-sans-medium</item>
         <item name="android:textSize">14sp</item>
     </style>
 
-    <style name="autofill.TextAppearance.Primary">
-        <item name="android:textColor">@color/autofill_light_textColorPrimary</item>
+    <style name="autofill.TextSubtitle" parent="">
+        <item name="android:fontFamily">google-sans-text</item>
+        <item name="android:textSize">12sp</item>
     </style>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index b2c23a4..58467af 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -57,6 +57,7 @@
 import com.android.credentialmanager.getflow.ProviderDisplayInfo
 import com.android.credentialmanager.getflow.toProviderDisplayInfo
 import com.android.credentialmanager.ktx.credentialEntry
+import com.android.credentialmanager.model.CredentialType
 import com.android.credentialmanager.model.get.CredentialEntryInfo
 import com.android.credentialmanager.model.get.ProviderInfo
 import org.json.JSONException
@@ -313,12 +314,14 @@
         var i = 0
         var datasetAdded = false
 
-        val duplicateDisplayNames: MutableMap<String, Boolean> = mutableMapOf()
+        val duplicateDisplayNamesForPasskeys: MutableMap<String, Boolean> = mutableMapOf()
         providerDisplayInfo.sortedUserNameToCredentialEntryList.forEach {
             val credentialEntry = it.sortedCredentialEntryList.first()
-            credentialEntry.displayName?.let {displayName ->
-                val duplicateEntry = duplicateDisplayNames.contains(displayName)
-                duplicateDisplayNames[displayName] = duplicateEntry
+            if (credentialEntry.credentialType == CredentialType.PASSKEY) {
+                credentialEntry.displayName?.let { displayName ->
+                    val duplicateEntry = duplicateDisplayNamesForPasskeys.contains(displayName)
+                    duplicateDisplayNamesForPasskeys[displayName] = duplicateEntry
+                }
             }
         }
         providerDisplayInfo.sortedUserNameToCredentialEntryList.forEach usernameLoop@{
@@ -355,12 +358,19 @@
                 } else {
                     spec = inlinePresentationSpecs[inlinePresentationSpecsCount - 1]
                 }
-                val displayName : String = primaryEntry.displayName ?: primaryEntry.userName
+                val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY
+                        && primaryEntry.displayName != null) {
+                    primaryEntry.displayName!!
+                } else {
+                    primaryEntry.userName
+                }
                 val sliceBuilder = InlineSuggestionUi
                         .newContentBuilder(pendingIntent)
                         .setTitle(displayName)
                 sliceBuilder.setStartIcon(icon)
-                if (duplicateDisplayNames[displayName] == true) {
+                if (primaryEntry.credentialType ==
+                        CredentialType.PASSKEY && duplicateDisplayNamesForPasskeys[displayName]
+                        == true) {
                     sliceBuilder.setSubtitle(primaryEntry.userName)
                 }
                 inlinePresentation = InlinePresentation(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
index 4dc7f00..e039dea 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
@@ -21,6 +21,7 @@
 import android.widget.RemoteViews
 import androidx.core.content.ContextCompat
 import com.android.credentialmanager.model.get.CredentialEntryInfo
+import com.android.credentialmanager.model.CredentialType
 import android.graphics.drawable.Icon
 
 class RemoteViewsFactory {
@@ -29,48 +30,87 @@
         private const val setAdjustViewBoundsMethodName = "setAdjustViewBounds"
         private const val setMaxHeightMethodName = "setMaxHeight"
         private const val setBackgroundResourceMethodName = "setBackgroundResource"
+        private const val bulletPoint = "\u2022"
+        private const val passwordCharacterLength = 15
 
         fun createDropdownPresentation(
                 context: Context,
                 icon: Icon,
                 credentialEntryInfo: CredentialEntryInfo
         ): RemoteViews {
-            val padding = context.resources.getDimensionPixelSize(com.android
-                    .credentialmanager.R.dimen.autofill_view_padding)
             var layoutId: Int = com.android.credentialmanager.R.layout
-                    .autofill_dataset_left_with_item_tag_hint
+                    .credman_dropdown_presentation_layout
             val remoteViews = RemoteViews(context.packageName, layoutId)
-            setRemoteViewsPaddings(remoteViews, padding)
-            val textColorPrimary = getTextColorPrimary(isDarkMode(context), context);
-            remoteViews.setTextColor(android.R.id.text1, textColorPrimary);
-            remoteViews.setTextViewText(android.R.id.text1, credentialEntryInfo.userName)
-
+            if (credentialEntryInfo.credentialType == CredentialType.UNKNOWN) {
+                return remoteViews
+            }
+            setRemoteViewsPaddings(remoteViews, context)
+            if (credentialEntryInfo.credentialType == CredentialType.PASSKEY) {
+                val displayName = credentialEntryInfo.displayName ?: credentialEntryInfo.userName
+                remoteViews.setTextViewText(android.R.id.text1, displayName)
+                val secondaryText = if (credentialEntryInfo.displayName != null)
+                    (credentialEntryInfo.userName + " " + bulletPoint + " "
+                            + credentialEntryInfo.credentialTypeDisplayName
+                            + " " + bulletPoint + " " + credentialEntryInfo.providerDisplayName)
+                else (credentialEntryInfo.credentialTypeDisplayName + " " + bulletPoint + " "
+                        + credentialEntryInfo.providerDisplayName)
+                remoteViews.setTextViewText(android.R.id.text2, secondaryText)
+            } else {
+                remoteViews.setTextViewText(android.R.id.text1, credentialEntryInfo.userName)
+                remoteViews.setTextViewText(android.R.id.text2,
+                        bulletPoint.repeat(passwordCharacterLength))
+            }
+            val textColorPrimary = ContextCompat.getColor(context,
+                    com.android.credentialmanager.R.color.text_primary)
+            remoteViews.setTextColor(android.R.id.text1, textColorPrimary)
+            val textColorSecondary = ContextCompat.getColor(context, com.android
+                    .credentialmanager.R.color.text_secondary)
+            remoteViews.setTextColor(android.R.id.text2, textColorSecondary)
             remoteViews.setImageViewIcon(android.R.id.icon1, icon);
             remoteViews.setBoolean(
                     android.R.id.icon1, setAdjustViewBoundsMethodName, true);
             remoteViews.setInt(
                     android.R.id.icon1,
-                     setMaxHeightMethodName,
+                    setMaxHeightMethodName,
                     context.resources.getDimensionPixelSize(
                             com.android.credentialmanager.R.dimen.autofill_icon_size));
-            val drawableId = if (isDarkMode(context))
-                com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one_dark
-            else com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one
+            val drawableId =
+                    com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one
             remoteViews.setInt(
                     android.R.id.content, setBackgroundResourceMethodName, drawableId);
             return remoteViews
         }
 
         private fun setRemoteViewsPaddings(
-                remoteViews: RemoteViews,
-                padding: Int) {
-            val halfPadding = padding / 2
+                remoteViews: RemoteViews, context: Context) {
+            val leftPadding = context.resources.getDimensionPixelSize(
+                    com.android.credentialmanager.R.dimen.autofill_view_left_padding)
+            val iconToTextPadding = context.resources.getDimensionPixelSize(
+                    com.android.credentialmanager.R.dimen.autofill_view_icon_to_text_padding)
+            val rightPadding = context.resources.getDimensionPixelSize(
+                    com.android.credentialmanager.R.dimen.autofill_view_right_padding)
+            val topPadding = context.resources.getDimensionPixelSize(
+                    com.android.credentialmanager.R.dimen.autofill_view_top_padding)
+            val bottomPadding = context.resources.getDimensionPixelSize(
+                    com.android.credentialmanager.R.dimen.autofill_view_bottom_padding)
+            remoteViews.setViewPadding(
+                    android.R.id.icon1,
+                    leftPadding,
+                    /* top=*/0,
+                    /* right=*/0,
+                    /* bottom=*/0)
             remoteViews.setViewPadding(
                     android.R.id.text1,
-                    halfPadding,
-                    halfPadding,
-                    halfPadding,
-                    halfPadding)
+                    iconToTextPadding,
+                    /* top=*/topPadding,
+                    /* right=*/rightPadding,
+                    /* bottom=*/0)
+            remoteViews.setViewPadding(
+                    android.R.id.text2,
+                    iconToTextPadding,
+                    /* top=*/0,
+                    /* right=*/rightPadding,
+                    /* bottom=*/bottomPadding)
         }
 
         private fun isDarkMode(context: Context): Boolean {
@@ -78,11 +118,5 @@
                     Configuration.UI_MODE_NIGHT_MASK
             return currentNightMode == Configuration.UI_MODE_NIGHT_YES
         }
-
-        private fun getTextColorPrimary(darkMode: Boolean, context: Context): Int {
-            return if (darkMode) ContextCompat.getColor(
-                    context, com.android.credentialmanager.R.color.text_primary_dark_mode)
-            else ContextCompat.getColor(context, com.android.credentialmanager.R.color.text_primary)
-        }
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt
index a9974dc..514ad669 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt
@@ -39,6 +39,9 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -68,6 +71,7 @@
     ) {
         val contentPadding = PaddingValues(horizontal = SettingsDimension.itemPaddingEnd)
         Button(
+            modifier = Modifier.semantics { role = Role.DropdownList },
             onClick = { expanded = true },
             colors = ButtonDefaults.buttonColors(
                 containerColor = SettingsTheme.colorScheme.spinnerHeaderContainer,
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index 21263a9..f7b1a26 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -10,6 +10,13 @@
 }
 
 flag {
+    name: "floating_menu_drag_to_hide"
+    namespace: "accessibility"
+    description: "Allows users to hide the FAB then use notification to dismiss or bring it back."
+    bug: "298718415"
+}
+
+flag {
     name: "floating_menu_ime_displacement_animation"
     namespace: "accessibility"
     description: "Adds an animation for when the FAB is displaced by an IME becoming visible."
@@ -28,4 +35,4 @@
     namespace: "accessibility"
     description: "Animates the floating menu's transition between curved and jagged edges."
     bug: "281140482"
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt
index 0acc76f..105f572 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt
@@ -20,24 +20,26 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.geometry.toRect
 import androidx.compose.ui.graphics.BlendMode
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.Outline
-import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.drawOutline
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
 import androidx.compose.ui.graphics.drawscope.translate
-import androidx.compose.ui.graphics.withSaveLayer
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.DrawModifierNode
 import androidx.compose.ui.node.GlobalPositionAwareModifierNode
+import androidx.compose.ui.node.LayoutModifierNode
 import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.toSize
 
@@ -88,11 +90,22 @@
     var size: () -> Size,
     var offset: () -> Offset,
     var shape: () -> Shape,
-) : Modifier.Node(), DrawModifierNode {
+) : Modifier.Node(), DrawModifierNode, LayoutModifierNode {
     private var lastSize: Size = Size.Unspecified
     private var lastLayoutDirection: LayoutDirection = LayoutDirection.Ltr
     private var lastOutline: Outline? = null
 
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        return measurable.measure(constraints).run {
+            layout(width, height) {
+                placeWithLayer(0, 0) { compositingStrategy = CompositingStrategy.Offscreen }
+            }
+        }
+    }
+
     override fun ContentDrawScope.draw() {
         val holeSize = size()
         if (holeSize == Size.Zero) {
@@ -100,14 +113,10 @@
             return
         }
 
-        drawIntoCanvas { canvas ->
-            canvas.withSaveLayer(size.toRect(), Paint()) {
-                drawContent()
+        drawContent()
 
-                val offset = offset()
-                translate(offset.x, offset.y) { drawHole(holeSize) }
-            }
-        }
+        val offset = offset()
+        translate(offset.x, offset.y) { drawHole(holeSize) }
     }
 
     private fun DrawScope.drawHole(size: Size) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index c055919..64388b7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -315,10 +315,13 @@
                 with(layoutImpl.state) { coroutineScope.onChangeScene(targetScene.key) }
             }
 
-            animateOffset(
+            swipeTransition.animateOffset(
+                coroutineScope = coroutineScope,
                 initialVelocity = velocity,
                 targetOffset = targetOffset,
-                targetScene = targetScene.key
+                onAnimationCompleted = {
+                    layoutState.finishTransition(swipeTransition, idleScene = targetScene.key)
+                }
             )
         }
 
@@ -410,34 +413,6 @@
         }
     }
 
-    private fun animateOffset(
-        initialVelocity: Float,
-        targetOffset: Float,
-        targetScene: SceneKey,
-    ) {
-        swipeTransition.startOffsetAnimation {
-            coroutineScope.launch {
-                if (!swipeTransition.isAnimatingOffset) {
-                    swipeTransition.offsetAnimatable.snapTo(swipeTransition.dragOffset)
-                }
-                swipeTransition.isAnimatingOffset = true
-
-                swipeTransition.offsetAnimatable.animateTo(
-                    targetOffset,
-                    // TODO(b/290184746): Make this spring spec configurable.
-                    spring(
-                        stiffness = Spring.StiffnessMediumLow,
-                        visibilityThreshold = OffsetVisibilityThreshold
-                    ),
-                    initialVelocity = initialVelocity,
-                )
-
-                swipeTransition.finishOffsetAnimation()
-                layoutState.finishTransition(swipeTransition, targetScene)
-            }
-        }
-    }
-
     internal class SwipeTransition(
         val _fromScene: Scene,
         val _toScene: Scene,
@@ -479,12 +454,14 @@
         private var offsetAnimationJob: Job? = null
 
         /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
-        fun startOffsetAnimation(job: () -> Job) {
+        private fun startOffsetAnimation(job: () -> Job) {
             cancelOffsetAnimation()
             offsetAnimationJob = job()
         }
 
         /** Cancel any ongoing offset animation. */
+        // TODO(b/317063114) This should be a suspended function to avoid multiple jobs running at
+        // the same time.
         fun cancelOffsetAnimation() {
             offsetAnimationJob?.cancel()
             finishOffsetAnimation()
@@ -496,6 +473,43 @@
                 dragOffset = offsetAnimatable.value
             }
         }
+
+        // TODO(b/290184746): Make this spring spec configurable.
+        private val animationSpec =
+            spring(
+                stiffness = Spring.StiffnessMediumLow,
+                visibilityThreshold = OffsetVisibilityThreshold
+            )
+
+        fun animateOffset(
+            // TODO(b/317063114) The CoroutineScope should be removed.
+            coroutineScope: CoroutineScope,
+            initialVelocity: Float,
+            targetOffset: Float,
+            onAnimationCompleted: () -> Unit,
+        ) {
+            startOffsetAnimation {
+                coroutineScope.launch {
+                    animateOffset(targetOffset, initialVelocity)
+                    onAnimationCompleted()
+                }
+            }
+        }
+
+        private suspend fun animateOffset(targetOffset: Float, initialVelocity: Float) {
+            if (!isAnimatingOffset) {
+                offsetAnimatable.snapTo(dragOffset)
+            }
+            isAnimatingOffset = true
+
+            offsetAnimatable.animateTo(
+                targetValue = targetOffset,
+                animationSpec = animationSpec,
+                initialVelocity = initialVelocity,
+            )
+
+            finishOffsetAnimation()
+        }
     }
 
     companion object {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index 41bde52..3dfe65a 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -22,6 +22,7 @@
 import android.os.UserHandle
 import android.provider.Settings
 import androidx.annotation.OpenForTesting
+import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.core.LogcatOnlyMessageBuffer
 import com.android.systemui.log.core.Logger
@@ -120,8 +121,9 @@
             override fun onPluginAttached(
                 manager: PluginLifecycleManager<ClockProviderPlugin>
             ): Boolean {
-                manager.isDebug = !keepAllLoaded
-
+                manager.setLogFunc({ tag, msg ->
+                    (clockBuffers?.infraMessageBuffer as LogBuffer?)?.log(tag, LogLevel.DEBUG, msg)
+                })
                 if (keepAllLoaded) {
                     // Always load new plugins if requested
                     return true
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index c8461d2..02d30c5e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -197,7 +197,6 @@
         whenever(deviceProvisionedController.isUserSetup(anyInt())).thenReturn(true)
 
         featureFlags = FakeFeatureFlags()
-        featureFlags.set(Flags.BOUNCER_USER_SWITCHER, false)
         featureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false)
         featureFlags.set(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT, false)
         featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt
new file mode 100644
index 0000000..721fc49
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.log
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.shared.log.CommunalUiEvent
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+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.any
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class CommunalLoggerStartableTest : SysuiTestCase() {
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+
+    private lateinit var testScope: TestScope
+    private lateinit var communalInteractor: CommunalInteractor
+    private lateinit var underTest: CommunalLoggerStartable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        val withDeps = CommunalInteractorFactory.create()
+        testScope = withDeps.testScope
+        communalInteractor = withDeps.communalInteractor
+
+        underTest =
+            CommunalLoggerStartable(
+                testScope.backgroundScope,
+                communalInteractor,
+                uiEventLogger,
+            )
+        underTest.start()
+    }
+
+    @Test
+    fun transitionStateLogging_enterCommunalHub() =
+        testScope.runTest {
+            // Transition state is default (non-communal)
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(idle(CommunalSceneKey.DEFAULT))
+            communalInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // Verify nothing is logged from the default state
+            verify(uiEventLogger, never()).log(any())
+
+            // Start transition to communal
+            transitionState.value = transition(to = CommunalSceneKey.Communal)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_START)
+
+            // Finish transition to communal
+            transitionState.value = idle(CommunalSceneKey.Communal)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_FINISH)
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SHOWN)
+        }
+
+    @Test
+    fun transitionStateLogging_enterCommunalHub_canceled() =
+        testScope.runTest {
+            // Transition state is default (non-communal)
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(idle(CommunalSceneKey.DEFAULT))
+            communalInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // Verify nothing is logged from the default state
+            verify(uiEventLogger, never()).log(any())
+
+            // Start transition to communal
+            transitionState.value = transition(to = CommunalSceneKey.Communal)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_START)
+
+            // Cancel the transition
+            transitionState.value = idle(CommunalSceneKey.DEFAULT)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_CANCEL)
+
+            // Verify neither SHOWN nor GONE is logged
+            verify(uiEventLogger, never()).log(CommunalUiEvent.COMMUNAL_HUB_SHOWN)
+            verify(uiEventLogger, never()).log(CommunalUiEvent.COMMUNAL_HUB_GONE)
+        }
+
+    @Test
+    fun transitionStateLogging_exitCommunalHub() =
+        testScope.runTest {
+            // Transition state is communal
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(idle(CommunalSceneKey.Communal))
+            communalInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // Verify SHOWN is logged when it's the default state
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SHOWN)
+
+            // Start transition from communal
+            transitionState.value = transition(from = CommunalSceneKey.Communal)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_START)
+
+            // Finish transition to communal
+            transitionState.value = idle(CommunalSceneKey.DEFAULT)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_FINISH)
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_GONE)
+        }
+
+    @Test
+    fun transitionStateLogging_exitCommunalHub_canceled() =
+        testScope.runTest {
+            // Transition state is communal
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(idle(CommunalSceneKey.Communal))
+            communalInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // Clear the initial SHOWN event from the logger
+            clearInvocations(uiEventLogger)
+
+            // Start transition from communal
+            transitionState.value = transition(from = CommunalSceneKey.Communal)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_START)
+
+            // Cancel the transition
+            transitionState.value = idle(CommunalSceneKey.Communal)
+            runCurrent()
+
+            // Verify UiEvent logged
+            verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_CANCEL)
+
+            // Verify neither SHOWN nor GONE is logged
+            verify(uiEventLogger, never()).log(CommunalUiEvent.COMMUNAL_HUB_SHOWN)
+            verify(uiEventLogger, never()).log(CommunalUiEvent.COMMUNAL_HUB_GONE)
+        }
+
+    private fun transition(
+        from: CommunalSceneKey = CommunalSceneKey.DEFAULT,
+        to: CommunalSceneKey = CommunalSceneKey.DEFAULT,
+    ): ObservableCommunalTransitionState.Transition {
+        return ObservableCommunalTransitionState.Transition(
+            fromScene = from,
+            toScene = to,
+            progress = emptyFlow(),
+            isInitiatedByUserInput = true,
+            isUserInputOngoing = emptyFlow(),
+        )
+    }
+
+    private fun idle(sceneKey: CommunalSceneKey): ObservableCommunalTransitionState.Idle {
+        return ObservableCommunalTransitionState.Idle(sceneKey)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
index e7037a6..9daf186 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
@@ -94,7 +94,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(5)
+            assertThat(values.size).isEqualTo(6)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index e141c2b..f1690daf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -95,7 +95,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(6)
+            assertThat(values.size).isEqualTo(7)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
         }
 
@@ -117,7 +117,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(3)
+            assertThat(values.size).isEqualTo(4)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
         }
 
@@ -232,7 +232,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(4)
+            assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
index 897ce6d..f763a67 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -59,9 +59,9 @@
                 testScope,
             )
 
-            // Only three values should be present, since the dream overlay runs for a small
+            // Only five values should be present, since the dream overlay runs for a small
             // fraction of the overall animation time
-            assertThat(values.size).isEqualTo(4)
+            assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
         }
 
@@ -84,7 +84,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(4)
+            assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index 4843f8b..74025fd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -73,9 +73,9 @@
                 testScope = testScope,
             )
 
-            // Only three values should be present, since the dream overlay runs for a small
+            // Only five values should be present, since the dream overlay runs for a small
             // fraction of the overall animation time
-            assertThat(values.size).isEqualTo(4)
+            assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
         }
 
@@ -98,10 +98,10 @@
                 testScope = testScope,
             )
 
-            assertThat(values.size).isEqualTo(5)
+            assertThat(values.size).isEqualTo(6)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
             // Validate finished value
-            assertThat(values[4]).isEqualTo(0f)
+            assertThat(values[5]).isEqualTo(0f)
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index a1b8aab..6fcb0c1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -74,9 +74,9 @@
                     ),
                 testScope = testScope,
             )
-            // Only 3 values should be present, since the dream overlay runs for a small fraction
+            // Only 5 values should be present, since the dream overlay runs for a small fraction
             // of the overall animation time
-            assertThat(values.size).isEqualTo(4)
+            assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
index 2111ad5..639114c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -33,6 +33,7 @@
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -54,6 +55,7 @@
     fun lockscreenFadeIn() =
         testScope.runTest {
             val values by collectValues(underTest.lockscreenAlpha)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionSteps(
                 listOf(
@@ -83,6 +85,7 @@
                 100
             )
             val values by collectValues(underTest.lockscreenTranslationY)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionSteps(
                 listOf(
@@ -95,7 +98,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(4)
+            assertThat(values.size).isEqualTo(5)
             values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
         }
 
@@ -107,6 +110,7 @@
                 100
             )
             val values by collectValues(underTest.lockscreenTranslationY)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionStep(step(0.5f, TransitionState.CANCELED))
 
@@ -117,6 +121,7 @@
     fun deviceEntryParentViewFadeIn() =
         testScope.runTest {
             val values by collectValues(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionSteps(
                 listOf(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index 90b8362..30b87bb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -65,6 +66,7 @@
     fun bouncerAlpha() =
         testScope.runTest {
             val values by collectValues(underTest.bouncerAlpha)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionSteps(
                 listOf(
@@ -83,6 +85,7 @@
     fun bouncerAlpha_runDimissFromKeyguard() =
         testScope.runTest {
             val values by collectValues(underTest.bouncerAlpha)
+            runCurrent()
 
             whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true)
 
@@ -95,7 +98,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(1)
+            assertThat(values.size).isEqualTo(2)
             values.forEach { assertThat(it).isEqualTo(0f) }
         }
 
@@ -103,11 +106,12 @@
     fun lockscreenAlpha() =
         testScope.runTest {
             val values by collectValues(underTest.lockscreenAlpha)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
             keyguardTransitionRepository.sendTransitionStep(step(1f))
 
-            assertThat(values.size).isEqualTo(1)
+            assertThat(values.size).isEqualTo(2)
             values.forEach { assertThat(it).isEqualTo(0f) }
         }
 
@@ -115,13 +119,14 @@
     fun lockscreenAlpha_runDimissFromKeyguard() =
         testScope.runTest {
             val values by collectValues(underTest.lockscreenAlpha)
+            runCurrent()
 
             sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
 
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
             keyguardTransitionRepository.sendTransitionStep(step(1f))
 
-            assertThat(values.size).isEqualTo(1)
+            assertThat(values.size).isEqualTo(2)
             values.forEach { assertThat(it).isEqualTo(1f) }
         }
 
@@ -129,13 +134,14 @@
     fun lockscreenAlpha_leaveShadeOpen() =
         testScope.runTest {
             val values by collectValues(underTest.lockscreenAlpha)
+            runCurrent()
 
             sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
 
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
             keyguardTransitionRepository.sendTransitionStep(step(1f))
 
-            assertThat(values.size).isEqualTo(1)
+            assertThat(values.size).isEqualTo(2)
             values.forEach { assertThat(it).isEqualTo(1f) }
         }
 
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java
index 3e5e8a0..f0ce460 100644
--- a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java
@@ -18,6 +18,8 @@
 
 import android.content.ComponentName;
 
+import java.util.function.BiConsumer;
+
 /**
  * Provides the ability for consumers to control plugin lifecycle.
  *
@@ -33,11 +35,8 @@
     /** Returns the currently loaded plugin instance (if plugin is loaded) */
     T getPlugin();
 
-    /** Returns true if the lifecycle manager should log debug messages */
-    boolean getIsDebug();
-
-    /** Sets whether or not hte lifecycle manager should log debug messages */
-    void setIsDebug(boolean debug);
+    /** Log tag and messages will be sent to the provided Consumer */
+    void setLogFunc(BiConsumer<String, String> logConsumer);
 
     /** returns true if the plugin is currently loaded */
     default boolean isLoaded() {
diff --git a/packages/SystemUI/res/drawable/stat_sys_no_internet_branded_vpn.xml b/packages/SystemUI/res/drawable/stat_sys_no_internet_branded_vpn.xml
new file mode 100644
index 0000000..2161a62
--- /dev/null
+++ b/packages/SystemUI/res/drawable/stat_sys_no_internet_branded_vpn.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** 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.
+*/
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="17dp"
+    android:height="17dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M12.09,9C11.11,7.5 9.43,6.5 7.5,6.5C4.46,6.5 2,8.96 2,12c0,3.04 2.46,5.5 5.5,5.5c1.93,0 3.61,-1 4.59,-2.5H14v3h4V9H12.09zM18,13hv3h-2v-3h-5.16c-0.43,1.44 -1.76,2.5 -3.34,2.5C5.57,15.5 4,13.93 4,12c0,-1.93 1.57,-3.5 3.5,-3.5c1.58,0 2.9,1.06 3.34,2.5H18V13z"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M7.5,12m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M22,10h-2v8h2V10z"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M22,20h-2v2h2V20z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/stat_sys_no_internet_vpn_ic.xml b/packages/SystemUI/res/drawable/stat_sys_no_internet_vpn_ic.xml
new file mode 100644
index 0000000..2161a62
--- /dev/null
+++ b/packages/SystemUI/res/drawable/stat_sys_no_internet_vpn_ic.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** 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.
+*/
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="17dp"
+    android:height="17dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M12.09,9C11.11,7.5 9.43,6.5 7.5,6.5C4.46,6.5 2,8.96 2,12c0,3.04 2.46,5.5 5.5,5.5c1.93,0 3.61,-1 4.59,-2.5H14v3h4V9H12.09zM18,13hv3h-2v-3h-5.16c-0.43,1.44 -1.76,2.5 -3.34,2.5C5.57,15.5 4,13.93 4,12c0,-1.93 1.57,-3.5 3.5,-3.5c1.58,0 2.9,1.06 3.34,2.5H18V13z"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M7.5,12m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M22,10h-2v8h2V10z"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M22,20h-2v2h2V20z"/>
+</vector>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 5e10905..f55a11e2 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -449,11 +449,11 @@
     <!-- Content description after successful auth when confirmation required -->
     <string name="fingerprint_dialog_authenticated_confirmation">Press the unlock icon to continue</string>
     <!-- Message shown to inform the user a face cannot be recognized and fingerprint should instead be used.[CHAR LIMIT=50] -->
-    <string name="fingerprint_dialog_use_fingerprint_instead">Can\u2019t recognize face. Use fingerprint instead.</string>
+    <string name="fingerprint_dialog_use_fingerprint_instead">Face not recognized. Use fingerprint instead.</string>
     <!-- Message shown to inform the user a face cannot be recognized and fingerprint should instead be used.[CHAR LIMIT=50] -->
     <string name="keyguard_face_failed_use_fp">@string/fingerprint_dialog_use_fingerprint_instead</string>
     <!-- Message shown to inform the user a face cannot be recognized. [CHAR LIMIT=25] -->
-    <string name="keyguard_face_failed">Can\u2019t recognize face</string>
+    <string name="keyguard_face_failed">Face not recognized</string>
     <!-- Message shown to suggest using fingerprint sensor to authenticate after another biometric failed. [CHAR LIMIT=25] -->
     <string name="keyguard_suggest_fingerprint">Use fingerprint instead</string>
     <!-- Message shown to inform the user that face unlock is not available. [CHAR LIMIT=59] -->
@@ -2284,6 +2284,8 @@
     <string name="notification_channel_storage">Storage</string>
     <!-- Title for the notification channel for hints and suggestions. [CHAR LIMIT=NONE] -->
     <string name="notification_channel_hints">Hints</string>
+    <!-- Title for the notification channel for accessibility related (i.e. accessibility floating menu). [CHAR LIMIT=NONE] -->
+    <string name="notification_channel_accessibility">Accessibility</string>
 
     <!-- App label of the instant apps notification [CHAR LIMIT=60] -->
     <string name="instant_apps">Instant Apps</string>
@@ -2554,6 +2556,11 @@
     <string name="accessibility_floating_button_docking_tooltip">Move button to the edge to hide it temporarily</string>
     <!-- Text for the undo action button of the message view of the accessibility floating menu to perform undo operation. [CHAR LIMIT=30]-->
     <string name="accessibility_floating_button_undo">Undo</string>
+    <!-- Notification title shown when accessibility floating button is in hidden state. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_floating_button_hidden_notification_title">Accessibility button hidden</string>
+    <!-- Notification content text to explain user can tap notification to bring back accessibility floating button. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_floating_button_hidden_notification_text">Tap to show accessibility button</string>
+
     <!-- Text for the message view with undo action of the accessibility floating menu to show which feature shortcut was removed. [CHAR LIMIT=30]-->
     <string name="accessibility_floating_button_undo_message_label_text"><xliff:g id="feature name" example="Magnification">%s</xliff:g> shortcut removed</string>
     <!-- Text for the message view with undo action of the accessibility floating menu to show how many features shortcuts were removed. [CHAR LIMIT=30]-->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
index 387f2e1..87cc86f 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
@@ -38,6 +38,7 @@
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.BiConsumer;
 import java.util.function.Supplier;
 
 /**
@@ -57,7 +58,7 @@
     private final PluginFactory<T> mPluginFactory;
     private final String mTag;
 
-    private boolean mIsDebug = false;
+    private BiConsumer<String, String> mLogConsumer = null;
     private Context mPluginContext;
     private T mPlugin;
 
@@ -86,17 +87,13 @@
         return mTag;
     }
 
-    public boolean getIsDebug() {
-        return mIsDebug;
+    public void setLogFunc(BiConsumer logConsumer) {
+        mLogConsumer = logConsumer;
     }
 
-    public void setIsDebug(boolean debug) {
-        mIsDebug = debug;
-    }
-
-    private void logDebug(String message) {
-        if (mIsDebug) {
-            Log.i(mTag, message);
+    private void log(String message) {
+        if (mLogConsumer != null) {
+            mLogConsumer.accept(mTag, message);
         }
     }
 
@@ -105,19 +102,19 @@
         boolean loadPlugin = mListener.onPluginAttached(this);
         if (!loadPlugin) {
             if (mPlugin != null) {
-                logDebug("onCreate: auto-unload");
+                log("onCreate: auto-unload");
                 unloadPlugin();
             }
             return;
         }
 
         if (mPlugin == null) {
-            logDebug("onCreate auto-load");
+            log("onCreate auto-load");
             loadPlugin();
             return;
         }
 
-        logDebug("onCreate: load callbacks");
+        log("onCreate: load callbacks");
         mPluginFactory.checkVersion(mPlugin);
         if (!(mPlugin instanceof PluginFragment)) {
             // Only call onCreate for plugins that aren't fragments, as fragments
@@ -129,7 +126,7 @@
 
     /** Alerts listener and plugin that the plugin is being shutdown. */
     public synchronized void onDestroy() {
-        logDebug("onDestroy");
+        log("onDestroy");
         unloadPlugin();
         mListener.onPluginDetached(this);
     }
@@ -145,7 +142,7 @@
      */
     public synchronized void loadPlugin() {
         if (mPlugin != null) {
-            logDebug("Load request when already loaded");
+            log("Load request when already loaded");
             return;
         }
 
@@ -157,7 +154,7 @@
             return;
         }
 
-        logDebug("Loaded plugin; running callbacks");
+        log("Loaded plugin; running callbacks");
         mPluginFactory.checkVersion(mPlugin);
         if (!(mPlugin instanceof PluginFragment)) {
             // Only call onCreate for plugins that aren't fragments, as fragments
@@ -174,11 +171,11 @@
      */
     public synchronized void unloadPlugin() {
         if (mPlugin == null) {
-            logDebug("Unload request when already unloaded");
+            log("Unload request when already unloaded");
             return;
         }
 
-        logDebug("Unloading plugin, running callbacks");
+        log("Unloading plugin, running callbacks");
         mListener.onPluginUnloaded(mPlugin, this);
         if (!(mPlugin instanceof PluginFragment)) {
             // Only call onDestroy for plugins that aren't fragments, as fragments
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index 823bd2d..7088829 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -88,18 +88,23 @@
      */
     void onNavButtonsDarkIntensityChanged(float darkIntensity) = 22;
 
-     /**
-      * Sent when split keyboard shortcut is triggered to enter stage split.
-      */
-     void enterStageSplitFromRunningApp(boolean leftOrTop) = 25;
+    /**
+     * Sent when when navigation bar luma sampling is enabled or disabled.
+     */
+    void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) = 23;
 
-     /**
-      * Sent when the surface for navigation bar is created or changed
-      */
-     void onNavigationBarSurface(in SurfaceControl surface) = 26;
+    /**
+     * Sent when split keyboard shortcut is triggered to enter stage split.
+     */
+    void enterStageSplitFromRunningApp(boolean leftOrTop) = 25;
 
-     /**
-      * Sent when the task bar stash state is toggled.
-      */
-     void onTaskbarToggled() = 27;
+    /**
+     * Sent when the surface for navigation bar is created or changed
+     */
+    void onNavigationBarSurface(in SurfaceControl surface) = 26;
+
+    /**
+     * Sent when the task bar stash state is toggled.
+     */
+    void onTaskbarToggled() = 27;
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index e457ca1..8e5d0da 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -1111,7 +1111,7 @@
     }
 
     private boolean canDisplayUserSwitcher() {
-        return mFeatureFlags.isEnabled(Flags.BOUNCER_USER_SWITCHER);
+        return getContext().getResources().getBoolean(R.bool.config_enableBouncerUserSwitcher);
     }
 
     private void configureMode() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java
index 49e0df6..568b24d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java
@@ -24,6 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.wm.shell.common.bubbles.DismissView;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 
@@ -116,6 +117,11 @@
         mMagnetizedObject.setMagnetListener(magnetListener);
     }
 
+    @VisibleForTesting
+    MagnetizedObject.MagnetListener getMagnetListener() {
+        return mMagnetizedObject.getMagnetListener();
+    }
+
     void maybeConsumeDownMotionEvent(MotionEvent event) {
         mMagnetizedObject.maybeConsumeMotionEvent(event);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactory.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactory.java
new file mode 100644
index 0000000..b5eeaa1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactory.java
@@ -0,0 +1,75 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import com.android.systemui.res.R;
+import com.android.systemui.util.NotificationChannels;
+
+class MenuNotificationFactory {
+    public static final String ACTION_UNDO =
+            "com.android.systemui.accessibility.floatingmenu.action.UNDO";
+    public static final String ACTION_DELETE =
+            "com.android.systemui.accessibility.floatingmenu.action.DELETE";
+
+    private final Context mContext;
+
+    MenuNotificationFactory(Context context) {
+        mContext = context;
+    }
+
+    public Notification createHiddenNotification() {
+        final CharSequence title = mContext.getText(
+                R.string.accessibility_floating_button_hidden_notification_title);
+        final CharSequence content = mContext.getText(
+                R.string.accessibility_floating_button_hidden_notification_text);
+
+        return new Notification.Builder(mContext, NotificationChannels.ALERTS)
+                .setContentTitle(title)
+                .setContentText(content)
+                .setSmallIcon(R.drawable.ic_settings_24dp)
+                .setContentIntent(buildUndoIntent())
+                .setDeleteIntent(buildDeleteIntent())
+                .setColor(mContext.getResources().getColor(
+                        com.android.internal.R.color.system_notification_accent_color))
+                .setLocalOnly(true)
+                .setCategory(Notification.CATEGORY_SYSTEM)
+                .build();
+    }
+
+    private PendingIntent buildUndoIntent() {
+        final Intent intent = new Intent(ACTION_UNDO);
+
+        return PendingIntent.getBroadcast(mContext, /* requestCode= */ 0, intent,
+                PendingIntent.FLAG_IMMUTABLE);
+
+    }
+
+    private PendingIntent buildDeleteIntent() {
+        final Intent intent = new Intent(ACTION_DELETE);
+
+        return PendingIntent.getBroadcastAsUser(mContext, /* requestCode= */ 0, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT
+                        | PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
+
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index 62d5feb..6869bba 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -26,16 +26,21 @@
 import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType;
 import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState;
 import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index;
+import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_DELETE;
+import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_UNDO;
 import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.annotation.IntDef;
 import android.annotation.StringDef;
 import android.annotation.SuppressLint;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
 import android.content.ComponentCallbacks;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
@@ -58,6 +63,7 @@
 
 import com.android.internal.accessibility.dialog.AccessibilityTarget;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.internal.util.Preconditions;
 import com.android.systemui.Flags;
 import com.android.systemui.res.R;
@@ -91,6 +97,8 @@
     private final MenuViewAppearance mMenuViewAppearance;
     private final MenuAnimationController mMenuAnimationController;
     private final AccessibilityManager mAccessibilityManager;
+    private final NotificationManager mNotificationManager;
+    private final MenuNotificationFactory mNotificationFactory;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final IAccessibilityFloatingMenu mFloatingMenu;
     private final SecureSettings mSecureSettings;
@@ -103,7 +111,9 @@
     private final Rect mImeInsetsRect = new Rect();
     private boolean mIsMigrationTooltipShowing;
     private boolean mShouldShowDockTooltip;
+    private boolean mIsNotificationShown;
     private Optional<MenuEduTooltipView> mEduTooltipView = Optional.empty();
+    private BroadcastReceiver mNotificationActionReceiver;
 
     @IntDef({
             LayerIndex.MENU_VIEW,
@@ -184,10 +194,16 @@
         mMenuViewAppearance = new MenuViewAppearance(context, windowManager);
         mMenuView = new MenuView(context, mMenuViewModel, mMenuViewAppearance);
         mMenuAnimationController = mMenuView.getMenuAnimationController();
-        mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
+        if (Flags.floatingMenuDragToHide()) {
+            mMenuAnimationController.setDismissCallback(this::hideMenuAndShowNotification);
+        } else {
+            mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
+        }
         mMenuAnimationController.setSpringAnimationsEndAction(this::onSpringAnimationsEndAction);
         mDismissView = new DismissView(context);
         DismissViewUtils.setup(mDismissView);
+        mNotificationFactory = new MenuNotificationFactory(context);
+        mNotificationManager = context.getSystemService(NotificationManager.class);
         mDragToInteractAnimationController = new DragToInteractAnimationController(
                 mDismissView, mMenuView);
         mDragToInteractAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() {
@@ -204,7 +220,11 @@
 
             @Override
             public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
-                hideMenuAndShowMessage();
+                if (Flags.floatingMenuDragToHide()) {
+                    hideMenuAndShowNotification();
+                } else {
+                    hideMenuAndShowMessage();
+                }
                 mDismissView.hide();
                 mDragToInteractAnimationController.animateDismissMenu(/* scaleUp= */ false);
             }
@@ -218,18 +238,25 @@
         mMessageView = new MenuMessageView(context);
 
         mMenuView.setOnTargetFeaturesChangeListener(newTargetFeatures -> {
-            if (newTargetFeatures.size() < 1) {
-                return;
-            }
-
-            // During the undo action period, the pending action will be canceled and undo back
-            // to the previous state if users did any action related to the accessibility features.
-            if (mMessageView.getVisibility() == VISIBLE) {
+            if (Flags.floatingMenuDragToHide()) {
+                dismissNotification();
                 undo();
-            }
+            } else {
+                if (newTargetFeatures.size() < 1) {
+                    return;
+                }
 
-            final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW);
-            messageText.setText(getMessageText(newTargetFeatures));
+                // During the undo action period, the pending action will be canceled and undo back
+                // to the previous state if users did any action related to the accessibility
+                // features.
+                if (mMessageView.getVisibility() == VISIBLE) {
+                    undo();
+                }
+
+
+                final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW);
+                messageText.setText(getMessageText(newTargetFeatures));
+            }
         });
 
         addView(mMenuView, LayerIndex.MENU_VIEW);
@@ -456,6 +483,50 @@
         mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE));
     }
 
+    private void hideMenuAndShowNotification() {
+        mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE));
+        showNotification();
+    }
+
+    private void showNotification() {
+        registerReceiverIfNeeded();
+        if (!mIsNotificationShown) {
+            mNotificationManager.notify(
+                    SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN,
+                    mNotificationFactory.createHiddenNotification());
+            mIsNotificationShown = true;
+        }
+    }
+
+    private void dismissNotification() {
+        unregisterReceiverIfNeeded();
+        if (mIsNotificationShown) {
+            mNotificationManager.cancel(
+                    SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN);
+            mIsNotificationShown = false;
+        }
+    }
+
+    private void registerReceiverIfNeeded() {
+        if (mNotificationActionReceiver != null) {
+            return;
+        }
+        mNotificationActionReceiver = new MenuNotificationActionReceiver();
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(ACTION_UNDO);
+        intentFilter.addAction(ACTION_DELETE);
+        getContext().registerReceiver(mNotificationActionReceiver, intentFilter,
+                Context.RECEIVER_EXPORTED);
+    }
+
+    private void unregisterReceiverIfNeeded() {
+        if (mNotificationActionReceiver == null) {
+            return;
+        }
+        getContext().unregisterReceiver(mNotificationActionReceiver);
+        mNotificationActionReceiver = null;
+    }
+
     private void undo() {
         mHandler.removeCallbacksAndMessages(/* token= */ null);
         mMessageView.setVisibility(GONE);
@@ -464,4 +535,23 @@
         mMenuView.setVisibility(VISIBLE);
         mMenuAnimationController.startGrowAnimation();
     }
+
+    @VisibleForTesting
+    DragToInteractAnimationController getDragToInteractAnimationController() {
+        return mDragToInteractAnimationController;
+    }
+
+    private class MenuNotificationActionReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (ACTION_UNDO.equals(action)) {
+                dismissNotification();
+                undo();
+            } else if (ACTION_DELETE.equals(action)) {
+                dismissNotification();
+                mDismissMenuAction.run();
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/log/CommunalLoggerStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/log/CommunalLoggerStartable.kt
new file mode 100644
index 0000000..889023e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/log/CommunalLoggerStartable.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.log
+
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.shared.log.CommunalUiEvent
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.kotlin.pairwise
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+/** A [CoreStartable] responsible for logging metrics for the communal hub. */
+@SysUISingleton
+class CommunalLoggerStartable
+@Inject
+constructor(
+    @Background private val backgroundScope: CoroutineScope,
+    private val communalInteractor: CommunalInteractor,
+    private val uiEventLogger: UiEventLogger,
+) : CoreStartable {
+
+    override fun start() {
+        communalInteractor.transitionState
+            .map { state ->
+                when {
+                    state.isOnCommunal() -> CommunalUiEvent.COMMUNAL_HUB_SHOWN
+                    state.isNotOnCommunal() -> CommunalUiEvent.COMMUNAL_HUB_GONE
+                    else -> null
+                }
+            }
+            .filterNotNull()
+            .distinctUntilChanged()
+            // Drop the default value.
+            .drop(1)
+            .onEach { uiEvent -> uiEventLogger.log(uiEvent) }
+            .launchIn(backgroundScope)
+
+        communalInteractor.transitionState
+            .pairwise()
+            .map { (old, new) ->
+                when {
+                    new.isOnCommunal() && old.isSwipingToCommunal() ->
+                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_FINISH
+                    new.isOnCommunal() && old.isSwipingFromCommunal() ->
+                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_CANCEL
+                    new.isNotOnCommunal() && old.isSwipingFromCommunal() ->
+                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_FINISH
+                    new.isNotOnCommunal() && old.isSwipingToCommunal() ->
+                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_CANCEL
+                    new.isSwipingToCommunal() && old.isNotOnCommunal() ->
+                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_ENTER_START
+                    new.isSwipingFromCommunal() && old.isOnCommunal() ->
+                        CommunalUiEvent.COMMUNAL_HUB_SWIPE_TO_EXIT_START
+                    else -> null
+                }
+            }
+            .filterNotNull()
+            .distinctUntilChanged()
+            .onEach { uiEvent -> uiEventLogger.log(uiEvent) }
+            .launchIn(backgroundScope)
+    }
+}
+
+/** Whether currently in communal scene. */
+private fun ObservableCommunalTransitionState.isOnCommunal(): Boolean {
+    return this is ObservableCommunalTransitionState.Idle && scene == CommunalSceneKey.Communal
+}
+
+/** Whether currently in a scene other than communal. */
+private fun ObservableCommunalTransitionState.isNotOnCommunal(): Boolean {
+    return this is ObservableCommunalTransitionState.Idle && scene != CommunalSceneKey.Communal
+}
+
+/** Whether currently transitioning from another scene to communal. */
+private fun ObservableCommunalTransitionState.isSwipingToCommunal(): Boolean {
+    return this is ObservableCommunalTransitionState.Transition &&
+        toScene == CommunalSceneKey.Communal &&
+        isInitiatedByUserInput
+}
+
+/** Whether currently transitioning from communal to another scene. */
+private fun ObservableCommunalTransitionState.isSwipingFromCommunal(): Boolean {
+    return this is ObservableCommunalTransitionState.Transition &&
+        fromScene == CommunalSceneKey.Communal &&
+        isInitiatedByUserInput
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt
index e167f3e..b64c195 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt
@@ -22,8 +22,6 @@
 /** UI events for the Communal Hub. */
 enum class CommunalUiEvent(private val id: Int) : UiEventEnum {
     @UiEvent(doc = "Communal Hub is fully shown") COMMUNAL_HUB_SHOWN(1566),
-    @UiEvent(doc = "Communal Hub starts entering") COMMUNAL_HUB_ENTERING(1575),
-    @UiEvent(doc = "Communal Hub starts exiting") COMMUNAL_HUB_EXITING(1576),
     @UiEvent(doc = "Communal Hub is fully gone") COMMUNAL_HUB_GONE(1577),
     @UiEvent(doc = "Communal Hub times out") COMMUNAL_HUB_TIMEOUT(1578),
     @UiEvent(doc = "The visible content in the Communal Hub is fully loaded and rendered")
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 bfc6f2b..380ed61 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -30,6 +30,8 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
 import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent
 import javax.inject.Inject
@@ -41,6 +43,7 @@
 constructor(
     private val communalViewModel: CommunalEditModeViewModel,
     private var windowManagerService: IWindowManager? = null,
+    private val uiEventLogger: UiEventLogger,
 ) : ComponentActivity() {
     companion object {
         private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
@@ -54,6 +57,8 @@
         registerForActivityResult(StartActivityForResult()) { result ->
             when (result.resultCode) {
                 RESULT_OK -> {
+                    uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_WIDGET_PICKER_SHOWN)
+
                     result.data?.let { intent ->
                         val isPendingWidgetDrag =
                             intent.getBooleanExtra(EXTRA_IS_PENDING_WIDGET_DRAG, false)
@@ -144,4 +149,16 @@
             communalViewModel.setConfigurationResult(resultCode)
         }
     }
+
+    override fun onStart() {
+        super.onStart()
+
+        uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_SHOWN)
+    }
+
+    override fun onStop() {
+        super.onStop()
+
+        uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_GONE)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 87a736d..8d82b55 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.back.domain.interactor.BackActionInteractor
 import com.android.systemui.biometrics.BiometricNotificationService
 import com.android.systemui.clipboardoverlay.ClipboardListener
+import com.android.systemui.communal.log.CommunalLoggerStartable
 import com.android.systemui.controls.dagger.StartControlsStartableModule
 import com.android.systemui.dagger.qualifiers.PerUser
 import com.android.systemui.dreams.AssistantAttentionMonitor
@@ -318,4 +319,9 @@
     @IntoMap
     @ClassKey(KeyguardDismissBinder::class)
     abstract fun bindKeyguardDismissBinder(impl: KeyguardDismissBinder): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(CommunalLoggerStartable::class)
+    abstract fun bindCommunalLoggerStartable(impl: CommunalLoggerStartable): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 699532c..89a983b 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -122,11 +122,6 @@
     val NEW_UNLOCK_SWIPE_ANIMATION = releasedFlag("new_unlock_swipe_animation")
     val CHARGING_RIPPLE = resourceBooleanFlag(R.bool.flag_charging_ripple, "charging_ripple")
 
-    // TODO(b/254512281): Tracking Bug
-    @JvmField
-    val BOUNCER_USER_SWITCHER =
-        resourceBooleanFlag(R.bool.config_enableBouncerUserSwitcher, "bouncer_user_switcher")
-
     // TODO(b/254512676): Tracking Bug
     @JvmField
     val LOCKSCREEN_CUSTOM_CLOCKS =
@@ -377,9 +372,6 @@
     // 1000 - dock
     val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag("simulate_dock_through_charging")
 
-    // TODO(b/254512758): Tracking Bug
-    @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag("rounded_box_ripple")
-
     // TODO(b/273509374): Tracking Bug
     @JvmField
     val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index f7d1543..5b0c562 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -154,6 +154,8 @@
     val dozingToLockscreenTransition: Flow<TransitionStep> =
         repository.transition(DOZING, LOCKSCREEN)
 
+    val transitions = repository.transitions
+
     /** Receive all [TransitionStep] matching a filter of [from]->[to] */
     fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> {
         return repository.transition(from, to)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index cf1d247..4abda74 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -17,9 +17,13 @@
 
 import android.view.animation.Interpolator
 import com.android.app.animation.Interpolators.LINEAR
+import com.android.app.tracing.coroutines.launch
 import com.android.keyguard.logging.KeyguardTransitionAnimationLogger
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+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.shared.model.TransitionState.CANCELED
 import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
@@ -31,10 +35,12 @@
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
 
 /**
  * Assists in creating sub-flows for a KeyguardTransition. Call [setup] once for a transition, and
@@ -45,21 +51,49 @@
 @Inject
 constructor(
     @Application private val scope: CoroutineScope,
+    private val transitionInteractor: KeyguardTransitionInteractor,
     private val logger: KeyguardTransitionAnimationLogger,
 ) {
+    private val transitionMap = mutableMapOf<Edge, MutableSharedFlow<TransitionStep>>()
 
-    /**
-     * Invoke once per transition between FROM->TO states to get access to
-     * [SharedFlowBuilder#sharedFlow].
-     */
+    init {
+        scope.launch("KeyguardTransitionAnimationFlow") {
+            transitionInteractor.transitions.collect {
+                // FROM->TO
+                transitionMap[Edge(it.from, it.to)]?.emit(it)
+                // FROM->(ANY)
+                transitionMap[Edge(it.from, null)]?.emit(it)
+                // (ANY)->TO
+                transitionMap[Edge(null, it.to)]?.emit(it)
+            }
+        }
+    }
+
+    private fun getOrCreateFlow(edge: Edge): MutableSharedFlow<TransitionStep> {
+        return transitionMap.getOrPut(edge) {
+            MutableSharedFlow<TransitionStep>(
+                extraBufferCapacity = 10,
+                onBufferOverflow = BufferOverflow.DROP_OLDEST
+            )
+        }
+    }
+
+    /** Invoke once per transition between FROM->TO states to get access to a shared flow. */
     fun setup(
         duration: Duration,
-        stepFlow: Flow<TransitionStep>,
-    ) = SharedFlowBuilder(duration, stepFlow)
+        from: KeyguardState?,
+        to: KeyguardState?,
+    ): FlowBuilder {
+        if (from == null && to == null) {
+            throw IllegalArgumentException("from and to are both null")
+        }
 
-    inner class SharedFlowBuilder(
+        return FlowBuilder(duration, Edge(from, to))
+    }
+
+    inner class FlowBuilder(
         private val transitionDuration: Duration,
-        private val stepFlow: Flow<TransitionStep>,
+        private val edge: Edge,
     ) {
         /**
          * Transitions will occur over a [transitionDuration] with [TransitionStep]s being emitted
@@ -115,20 +149,21 @@
                 }?.let { onStep(interpolator.getInterpolation(it)) }
             }
 
-            return stepFlow
+            return getOrCreateFlow(edge)
                 .map { step ->
-                    val value =
-                        when (step.transitionState) {
-                            STARTED -> stepToValue(step)
-                            RUNNING -> stepToValue(step)
-                            CANCELED -> onCancel?.invoke()
-                            FINISHED -> onFinish?.invoke()
-                        }
-                    logger.logTransitionStep(name, step, value)
-                    value
+                    StateToValue(
+                            step.transitionState,
+                            when (step.transitionState) {
+                                STARTED -> stepToValue(step)
+                                RUNNING -> stepToValue(step)
+                                CANCELED -> onCancel?.invoke()
+                                FINISHED -> onFinish?.invoke()
+                            }
+                        )
+                        .also { logger.logTransitionStep(name, step, it.value) }
                 }
-                .filterNotNull()
                 .distinctUntilChanged()
+                .mapNotNull { stateToValue -> stateToValue.value }
         }
 
         /**
@@ -138,4 +173,14 @@
             return sharedFlow(duration = 1.milliseconds, onStep = { value }, onFinish = { value })
         }
     }
+
+    data class Edge(
+        val from: KeyguardState?,
+        val to: KeyguardState?,
+    )
+
+    data class StateToValue(
+        val transitionState: TransitionState,
+        val value: Float?,
+    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
index a8e3be7..b4b48a8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
@@ -19,7 +19,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
@@ -38,14 +37,14 @@
 class AlternateBouncerToAodTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromAlternateBouncerTransitionInteractor.TO_AOD_DURATION,
-            stepFlow = interactor.transition(KeyguardState.ALTERNATE_BOUNCER, KeyguardState.AOD),
+            from = KeyguardState.ALTERNATE_BOUNCER,
+            to = KeyguardState.AOD,
         )
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
index 5d6b0ce..3737e6f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor.Companion.TO_GONE_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
 import com.android.systemui.keyguard.shared.model.ScrimAlpha
@@ -37,14 +36,14 @@
 class AlternateBouncerToGoneTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     bouncerToGoneFlows: BouncerToGoneFlows,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_GONE_DURATION,
-            stepFlow = interactor.transition(ALTERNATE_BOUNCER, KeyguardState.GONE),
+            from = ALTERNATE_BOUNCER,
+            to = KeyguardState.GONE,
         )
 
     /** Scrim alpha values */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
index 8e729f7..2526f0a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
@@ -19,7 +19,6 @@
 
 import android.graphics.Color
 import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor.Companion.TRANSITION_DURATION_MS
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
@@ -37,7 +36,6 @@
 @Inject
 constructor(
     private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
-    transitionInteractor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
     // When we're fully transitioned to the AlternateBouncer, the alpha of the scrim should be:
@@ -46,7 +44,8 @@
         animationFlow
             .setup(
                 duration = TRANSITION_DURATION_MS,
-                stepFlow = transitionInteractor.anyStateToAlternateBouncerTransition,
+                from = null,
+                to = ALTERNATE_BOUNCER,
             )
             .sharedFlow(
                 duration = TRANSITION_DURATION_MS,
@@ -60,7 +59,8 @@
         animationFlow
             .setup(
                 TRANSITION_DURATION_MS,
-                transitionInteractor.transitionStepsFromState(ALTERNATE_BOUNCER),
+                from = ALTERNATE_BOUNCER,
+                to = null,
             )
             .sharedFlow(
                 duration = TRANSITION_DURATION_MS,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
index 2b14521..b92a9a0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
@@ -31,14 +30,14 @@
 class AodToGoneTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromAodTransitionInteractor.TO_GONE_DURATION,
-            stepFlow = interactor.transition(KeyguardState.AOD, KeyguardState.GONE),
+            from = KeyguardState.AOD,
+            to = KeyguardState.GONE,
         )
 
     override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
index 5e552e1..266fd02 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
@@ -19,7 +19,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
@@ -37,7 +37,6 @@
 class AodToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
@@ -45,7 +44,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_LOCKSCREEN_DURATION,
-            stepFlow = interactor.aodToLockscreenTransition,
+            from = KeyguardState.AOD,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     /** Ensure alpha is set to be visible */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
index d283af3..105a7ed 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
@@ -29,13 +28,13 @@
 class AodToOccludedTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromAodTransitionInteractor.TO_OCCLUDED_DURATION,
-            stepFlow = interactor.transition(KeyguardState.AOD, KeyguardState.OCCLUDED),
+            from = KeyguardState.AOD,
+            to = KeyguardState.OCCLUDED,
         )
 
     override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
index 41dc157..924fc5d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
@@ -21,7 +21,6 @@
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.ScrimAlpha
@@ -41,7 +40,6 @@
 class BouncerToGoneFlows
 @Inject
 constructor(
-    private val interactor: KeyguardTransitionInteractor,
     private val statusBarStateController: SysuiStatusBarStateController,
     private val primaryBouncerInteractor: PrimaryBouncerInteractor,
     private val keyguardDismissActionInteractor: Lazy<KeyguardDismissActionInteractor>,
@@ -76,7 +74,8 @@
         val transitionAnimation =
             animationFlow.setup(
                 duration = duration,
-                stepFlow = interactor.transition(fromState, GONE)
+                from = fromState,
+                to = GONE,
             )
 
         return shadeInteractor.shadeExpansion.flatMapLatest { shadeExpansion ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModel.kt
index 0b34326..e4610c1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModel.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromDozingTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
@@ -34,13 +34,13 @@
 class DozingToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromDozingTransitionInteractor.TO_LOCKSCREEN_DURATION,
-            stepFlow = interactor.dozingToLockscreenTransition,
+            from = KeyguardState.DOZING,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingHostedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingHostedToLockscreenTransitionViewModel.kt
index 8bcf3f8..67568e1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingHostedToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingHostedToLockscreenTransitionViewModel.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromDreamingLockscreenHostedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -28,14 +28,14 @@
 class DreamingHostedToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_LOCKSCREEN_DURATION,
-            stepFlow = interactor.dreamingLockscreenHostedToLockscreenTransition,
+            from = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index 5f620af..ead2d48 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 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.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
@@ -52,7 +53,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_LOCKSCREEN_DURATION,
-            stepFlow = keyguardTransitionInteractor.dreamingToLockscreenTransition,
+            from = KeyguardState.DREAMING,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     val transitionEnded =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
index 3f27eb0..ba04fd3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_AOD_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
@@ -36,7 +36,6 @@
 class GoneToAodTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
@@ -44,7 +43,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_AOD_DURATION,
-            stepFlow = interactor.goneToAodTransition,
+            from = KeyguardState.GONE,
+            to = KeyguardState.AOD,
         )
 
     /** y-translation from the top of the screen for AOD */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingLockscreenHostedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingLockscreenHostedTransitionViewModel.kt
index bba790a..b527463 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingLockscreenHostedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingLockscreenHostedTransitionViewModel.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -32,14 +32,14 @@
 class GoneToDreamingLockscreenHostedTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_DREAMING_DURATION,
-            stepFlow = interactor.goneToDreamingLockscreenHostedTransition,
+            from = KeyguardState.GONE,
+            to = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
         )
 
     /** Lockscreen views alpha - hide immediately */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
index 6762ba6..102242a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
@@ -19,7 +19,7 @@
 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -30,14 +30,14 @@
 class GoneToDreamingTransitionViewModel
 @Inject
 constructor(
-    private val interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_DREAMING_DURATION,
-            stepFlow = interactor.goneToDreamingTransition,
+            from = KeyguardState.GONE,
+            to = KeyguardState.DREAMING,
         )
 
     /** Lockscreen views y-translation */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt
index adae8ab..793abb4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -28,14 +28,14 @@
 class GoneToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_LOCKSCREEN_DURATION,
-            stepFlow = interactor.goneToLockscreenTransition
+            from = KeyguardState.GONE,
+            to = KeyguardState.LOCKSCREEN
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt
index 65614f4..7bf51a7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModel.kt
@@ -19,7 +19,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
@@ -36,7 +36,6 @@
 class LockscreenToAodTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     shadeDependentFlows: ShadeDependentFlows,
     animationFlow: KeyguardTransitionAnimationFlow,
@@ -45,7 +44,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromLockscreenTransitionInteractor.TO_AOD_DURATION,
-            stepFlow = interactor.lockscreenToAodTransition,
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.AOD,
         )
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDozingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDozingTransitionViewModel.kt
index accb20c..4c0cd2f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDozingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDozingTransitionViewModel.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DOZING_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -28,14 +28,14 @@
 class LockscreenToDozingTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_DOZING_DURATION,
-            stepFlow = interactor.lockscreenToDozingTransition
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.DOZING,
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingHostedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingHostedTransitionViewModel.kt
index c649b12..19b9cf47 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingHostedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingHostedTransitionViewModel.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_HOSTED_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -28,14 +28,14 @@
 class LockscreenToDreamingHostedTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_DREAMING_HOSTED_DURATION,
-            stepFlow = interactor.lockscreenToDreamingLockscreenHostedTransition
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
index 7f75b54..13522a6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
@@ -19,7 +19,7 @@
 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
@@ -34,14 +34,14 @@
 class LockscreenToDreamingTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     shadeDependentFlows: ShadeDependentFlows,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_DREAMING_DURATION,
-            stepFlow = interactor.lockscreenToDreamingTransition,
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.DREAMING,
         )
 
     /** Lockscreen views y-translation */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
index 9e19713..a26ef07 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
@@ -35,14 +34,14 @@
 class LockscreenToGoneTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromLockscreenTransitionInteractor.TO_GONE_DURATION,
-            stepFlow = interactor.transition(KeyguardState.LOCKSCREEN, KeyguardState.GONE),
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.GONE,
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
index 9db0b77..dd6652e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import com.android.systemui.res.R
@@ -37,7 +37,6 @@
 class LockscreenToOccludedTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     shadeDependentFlows: ShadeDependentFlows,
     configurationInteractor: ConfigurationInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
@@ -46,7 +45,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_OCCLUDED_DURATION,
-            stepFlow = interactor.lockscreenToOccludedTransition,
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.OCCLUDED,
         )
 
     /** Lockscreen views alpha */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
index 52e3257..ce47f3c6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
@@ -26,7 +25,6 @@
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
 
 /**
  * Breaks down LOCKSCREEN->PRIMARY BOUNCER transition into discrete steps for corresponding views to
@@ -37,21 +35,21 @@
 class LockscreenToPrimaryBouncerTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     shadeDependentFlows: ShadeDependentFlows,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromLockscreenTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
-            stepFlow =
-                interactor.transition(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER),
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.PRIMARY_BOUNCER,
         )
 
     val shortcutsAlpha: Flow<Float> =
-        interactor.transition(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER).map {
-            1 - it.value
-        }
+        transitionAnimation.sharedFlow(
+            duration = FromLockscreenTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
+            onStep = { 1f - it }
+        )
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
         shadeDependentFlows.transitionFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModel.kt
index ed5e83c..07c1141 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModel.kt
@@ -19,7 +19,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
@@ -35,14 +34,14 @@
 class OccludedToAodTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromOccludedTransitionInteractor.TO_AOD_DURATION,
-            stepFlow = interactor.transition(KeyguardState.OCCLUDED, KeyguardState.AOD),
+            from = KeyguardState.OCCLUDED,
+            to = KeyguardState.AOD,
         )
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
index 4c24f83..90195bd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import com.android.systemui.res.R
@@ -41,7 +41,6 @@
 class OccludedToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     configurationInteractor: ConfigurationInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
@@ -50,7 +49,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_LOCKSCREEN_DURATION,
-            stepFlow = interactor.occludedToLockscreenTransition,
+            from = KeyguardState.OCCLUDED,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     /** Lockscreen views y-translation */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
index 93482ea..74094be 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -27,14 +27,14 @@
 class OffToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
 
     private val transitionAnimation =
         animationFlow.setup(
             duration = 250.milliseconds,
-            stepFlow = interactor.offToLockscreenTransition
+            from = KeyguardState.OFF,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     val shortcutsAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
index b0e2aa2..cd8e2f1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
@@ -19,7 +19,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
@@ -39,14 +38,14 @@
 class PrimaryBouncerToAodTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromPrimaryBouncerTransitionInteractor.TO_AOD_DURATION,
-            stepFlow = interactor.transition(KeyguardState.PRIMARY_BOUNCER, KeyguardState.AOD),
+            from = KeyguardState.PRIMARY_BOUNCER,
+            to = KeyguardState.AOD,
         )
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
index 9dbe97f..4f28b46 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
@@ -22,7 +22,6 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.ScrimAlpha
@@ -44,7 +43,6 @@
 class PrimaryBouncerToGoneTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     private val statusBarStateController: SysuiStatusBarStateController,
     private val primaryBouncerInteractor: PrimaryBouncerInteractor,
     keyguardDismissActionInteractor: Lazy<KeyguardDismissActionInteractor>,
@@ -55,7 +53,8 @@
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_GONE_DURATION,
-            stepFlow = interactor.transition(PRIMARY_BOUNCER, GONE)
+            from = PRIMARY_BOUNCER,
+            to = GONE,
         )
 
     private var leaveShadeOpen: Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
index b2eed60..284a134 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
@@ -19,7 +19,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
@@ -28,7 +27,6 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
 
 /**
  * Breaks down PRIMARY BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -39,15 +37,14 @@
 class PrimaryBouncerToLockscreenTransitionViewModel
 @Inject
 constructor(
-    interactor: KeyguardTransitionInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromPrimaryBouncerTransitionInteractor.TO_LOCKSCREEN_DURATION,
-            stepFlow =
-                interactor.transition(KeyguardState.PRIMARY_BOUNCER, KeyguardState.LOCKSCREEN),
+            from = KeyguardState.PRIMARY_BOUNCER,
+            to = KeyguardState.LOCKSCREEN,
         )
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
@@ -60,9 +57,10 @@
         }
 
     val shortcutsAlpha: Flow<Float> =
-        interactor.transition(KeyguardState.PRIMARY_BOUNCER, KeyguardState.LOCKSCREEN).map {
-            it.value
-        }
+        transitionAnimation.sharedFlow(
+            duration = FromPrimaryBouncerTransitionInteractor.TO_LOCKSCREEN_DURATION,
+            onStep = { it }
+        )
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(1f)
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
index 0320dec..092f1ed 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
@@ -302,9 +302,6 @@
             final NavigationBarView navBarView = getNavigationBarView(displayId);
             if (navBarView != null) {
                 navBarView.showPinningEnterExitToast(entering);
-            } else if (displayId == mDisplayTracker.getDefaultDisplayId()
-                    && mTaskbarDelegate.isInitialized()) {
-                mTaskbarDelegate.showPinningEnterExitToast(entering);
             }
         }
 
@@ -314,9 +311,6 @@
             final NavigationBarView navBarView = getNavigationBarView(displayId);
             if (navBarView != null) {
                 navBarView.showPinningEscapeToast();
-            } else if (displayId == mDisplayTracker.getDefaultDisplayId()
-                    && mTaskbarDelegate.isInitialized()) {
-                mTaskbarDelegate.showPinningEscapeToast();
             }
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index 62c7343..0167287 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -504,6 +504,11 @@
     }
 
     @Override
+    public void setNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
+        mOverviewProxyService.onNavigationBarLumaSamplingEnabled(displayId, enable);
+    }
+
+    @Override
     public void showPinningEnterExitToast(boolean entering) {
         updateSysuiFlags();
         if (mScreenPinningNotify == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 45917e8..fd53423 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -1037,6 +1037,19 @@
         }
     }
 
+    public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
+        try {
+            if (mOverviewProxy != null) {
+                mOverviewProxy.onNavigationBarLumaSamplingEnabled(displayId, enable);
+            } else {
+                Log.e(TAG_OPS, "Failed to get overview proxy to enable/disable nav bar luma"
+                        + "sampling");
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG_OPS, "Failed to call onNavigationBarLumaSamplingEnabled()", e);
+        }
+    }
+
     private void updateEnabledState() {
         final int currentUser = mUserTracker.getUserId();
         mIsEnabled = mContext.getPackageManager().resolveServiceAsUser(mQuickStepIntent,
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index bd43307..7aa0dad 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -32,6 +32,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
@@ -143,6 +144,7 @@
         channel.enableVibration(true);
         mNotificationManager.createNotificationChannel(channel);
 
+        int currentUid = Process.myUid();
         int currentUserId = mUserContextTracker.getUserContext().getUserId();
         UserHandle currentUser = new UserHandle(currentUserId);
         switch (action) {
@@ -166,7 +168,7 @@
                 mRecorder = new ScreenMediaRecorder(
                         mUserContextTracker.getUserContext(),
                         mMainHandler,
-                        currentUserId,
+                        currentUid,
                         mAudioSource,
                         captureTarget,
                         this
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
index 3aab3bf..a170d0da 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
@@ -83,7 +83,7 @@
     private Surface mInputSurface;
     private VirtualDisplay mVirtualDisplay;
     private MediaRecorder mMediaRecorder;
-    private int mUser;
+    private int mUid;
     private ScreenRecordingMuxer mMuxer;
     private ScreenInternalAudioRecorder mAudio;
     private ScreenRecordingAudioSource mAudioSource;
@@ -94,12 +94,12 @@
     ScreenMediaRecorderListener mListener;
 
     public ScreenMediaRecorder(Context context, Handler handler,
-            int user, ScreenRecordingAudioSource audioSource,
+            int uid, ScreenRecordingAudioSource audioSource,
             MediaProjectionCaptureTarget captureRegion,
             ScreenMediaRecorderListener listener) {
         mContext = context;
         mHandler = handler;
-        mUser = user;
+        mUid = uid;
         mCaptureRegion = captureRegion;
         mListener = listener;
         mAudioSource = audioSource;
@@ -111,7 +111,7 @@
         IMediaProjectionManager mediaService =
                 IMediaProjectionManager.Stub.asInterface(b);
         IMediaProjection proj = null;
-        proj = mediaService.createProjection(mUser, mContext.getPackageName(),
+        proj = mediaService.createProjection(mUid, mContext.getPackageName(),
                     MediaProjectionManager.TYPE_SCREEN_CAPTURE, false);
         IMediaProjection projection = IMediaProjection.Stub.asInterface(proj.asBinder());
         if (mCaptureRegion != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
index 30a445f..703b3c6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
@@ -118,15 +118,25 @@
 
     private void updateVpn() {
         boolean vpnVisible = mSecurityController.isVpnEnabled();
-        int vpnIconId = currentVpnIconId(mSecurityController.isVpnBranded());
+        int vpnIconId = currentVpnIconId(
+                mSecurityController.isVpnBranded(),
+                mSecurityController.isVpnValidated());
 
         mIconController.setIcon(mSlotVpn, vpnIconId,
                 mContext.getResources().getString(R.string.accessibility_vpn_on));
         mIconController.setIconVisibility(mSlotVpn, vpnVisible);
     }
 
-    private int currentVpnIconId(boolean isBranded) {
-        return isBranded ? R.drawable.stat_sys_branded_vpn : R.drawable.stat_sys_vpn_ic;
+    private int currentVpnIconId(boolean isBranded, boolean isValidated) {
+        if (isBranded) {
+            return isValidated
+                    ? R.drawable.stat_sys_branded_vpn
+                    : R.drawable.stat_sys_no_internet_branded_vpn;
+        } else {
+            return isValidated
+                    ? R.drawable.stat_sys_vpn_ic
+                    : R.drawable.stat_sys_no_internet_vpn_ic;
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index 8fc8b2f..de46a5e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -19,7 +19,7 @@
 import android.os.OutcomeReceiver
 import android.telephony.satellite.NtnSignalStrengthCallback
 import android.telephony.satellite.SatelliteManager
-import android.telephony.satellite.SatelliteStateCallback
+import android.telephony.satellite.SatelliteModemStateCallback
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -180,7 +180,7 @@
     // By using the SupportedSatelliteManager here, we expect registration never to fail
     private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> =
         conflatedCallbackFlow {
-                val cb = SatelliteStateCallback { state ->
+                val cb = SatelliteModemStateCallback { state ->
                     trySend(SatelliteConnectionState.fromModemState(state))
                 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
index 3be14bc..10bf068 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
@@ -48,6 +48,8 @@
     boolean isNetworkLoggingEnabled();
     boolean isVpnEnabled();
     boolean isVpnRestricted();
+    /** Whether the VPN network is validated. */
+    boolean isVpnValidated();
     /** Whether the VPN app should use branded VPN iconography.  */
     boolean isVpnBranded();
     String getPrimaryVpnName();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index 5d69f36..9f4a906 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -15,6 +15,9 @@
  */
 package com.android.systemui.statusbar.policy;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+
 import android.annotation.Nullable;
 import android.app.admin.DeviceAdminInfo;
 import android.app.admin.DevicePolicyManager;
@@ -32,7 +35,9 @@
 import android.graphics.drawable.Drawable;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
 import android.net.Network;
+import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.VpnManager;
 import android.os.Handler;
@@ -76,7 +81,10 @@
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final NetworkRequest REQUEST =
-            new NetworkRequest.Builder().clearCapabilities().build();
+            new NetworkRequest.Builder()
+                    .clearCapabilities()
+                    .addTransportType(TRANSPORT_VPN)
+                    .build();
     private static final int NO_NETWORK = -1;
 
     private static final String VPN_BRANDED_META_DATA = "com.android.systemui.IS_BRANDED";
@@ -99,6 +107,8 @@
     private SparseArray<VpnConfig> mCurrentVpns = new SparseArray<>();
     private int mCurrentUserId;
     private int mVpnUserId;
+    @GuardedBy("mNetworkProperties")
+    private final SparseArray<NetworkProperties> mNetworkProperties = new SparseArray<>();
 
     // Key: userId, Value: whether the user has CACerts installed
     // Needs to be cached here since the query has to be asynchronous
@@ -162,6 +172,21 @@
             pw.print(mCurrentVpns.valueAt(i).user);
         }
         pw.println("}");
+        pw.print("  mNetworkProperties={");
+        synchronized (mNetworkProperties) {
+            for (int i = 0; i < mNetworkProperties.size(); ++i) {
+                if (i > 0) {
+                    pw.print(", ");
+                }
+                pw.print(mNetworkProperties.keyAt(i));
+                pw.print("={");
+                pw.print(mNetworkProperties.valueAt(i).interfaceName);
+                pw.print(", ");
+                pw.print(mNetworkProperties.valueAt(i).validated);
+                pw.print("}");
+            }
+        }
+        pw.println("}");
     }
 
     @Override
@@ -304,6 +329,26 @@
     }
 
     @Override
+    public boolean isVpnValidated() {
+        // Prioritize reporting the network status of the parent user.
+        final VpnConfig primaryVpnConfig = mCurrentVpns.get(mVpnUserId);
+        if (primaryVpnConfig != null) {
+            return getVpnValidationStatus(primaryVpnConfig);
+        }
+        // Identify any Unvalidated status in each active VPN network within other profiles.
+        for (int profileId : mUserManager.getEnabledProfileIds(mVpnUserId)) {
+            final VpnConfig vpnConfig = mCurrentVpns.get(profileId);
+            if (vpnConfig == null) {
+                continue;
+            }
+            if (!getVpnValidationStatus(vpnConfig)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
     public boolean hasCACertInCurrentUser() {
         Boolean hasCACerts = mHasCACerts.get(mCurrentUserId);
         return hasCACerts != null && hasCACerts.booleanValue();
@@ -493,11 +538,74 @@
         @Override
         public void onLost(Network network) {
             if (DEBUG) Log.d(TAG, "onLost " + network.getNetId());
+            synchronized (mNetworkProperties) {
+                mNetworkProperties.delete(network.getNetId());
+            }
             updateState();
             fireCallbacks();
         };
+
+
+        @Override
+        public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
+            if (DEBUG) Log.d(TAG, "onCapabilitiesChanged " + network.getNetId());
+            final NetworkProperties properties;
+            synchronized (mNetworkProperties) {
+                properties = mNetworkProperties.get(network.getNetId());
+            }
+            // When a new network appears, the system first notifies the application about
+            // its capabilities through onCapabilitiesChanged. This initial notification
+            // will be skipped because the interface information is included in the
+            // subsequent onLinkPropertiesChanged call. After validating the network, the
+            // system might send another onCapabilitiesChanged notification if the network
+            // becomes validated.
+            if (properties == null) {
+                return;
+            }
+            final boolean validated = nc.hasCapability(NET_CAPABILITY_VALIDATED);
+            if (properties.validated != validated) {
+                properties.validated = validated;
+                fireCallbacks();
+            }
+        }
+
+        @Override
+        public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {
+            if (DEBUG) Log.d(TAG, "onLinkPropertiesChanged " + network.getNetId());
+            final String interfaceName = linkProperties.getInterfaceName();
+            if (interfaceName == null) {
+                Log.w(TAG, "onLinkPropertiesChanged event with null interface");
+                return;
+            }
+            synchronized (mNetworkProperties) {
+                final NetworkProperties properties = mNetworkProperties.get(network.getNetId());
+                if (properties == null) {
+                    mNetworkProperties.put(
+                            network.getNetId(),
+                            new NetworkProperties(interfaceName, false));
+                } else {
+                    properties.interfaceName = interfaceName;
+                }
+            }
+        }
     };
 
+    /**
+     *  Retrieve the validation status of the VPN network associated with the given VpnConfig.
+     */
+    private boolean getVpnValidationStatus(@NonNull VpnConfig vpnConfig) {
+        synchronized (mNetworkProperties) {
+            // Find the network has the same interface as the VpnConfig
+            for (int i = 0; i < mNetworkProperties.size(); ++i) {
+                if (mNetworkProperties.valueAt(i).interfaceName.equals(vpnConfig.interfaze)) {
+                    return mNetworkProperties.valueAt(i).validated;
+                }
+            }
+        }
+        // If no matching network is found, consider it validated.
+        return true;
+    }
+
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override public void onReceive(Context context, Intent intent) {
             if (KeyChain.ACTION_TRUST_STORE_CHANGED.equals(intent.getAction())) {
@@ -508,4 +616,17 @@
             }
         }
     };
+
+    /**
+     *  A data class to hold specific Network properties received through the NetworkCallback.
+     */
+    private static class NetworkProperties {
+        public String interfaceName;
+        public boolean validated;
+
+        NetworkProperties(@NonNull String interfaceName, boolean validated) {
+            this.interfaceName = interfaceName;
+            this.validated = validated;
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactoryTest.java
new file mode 100644
index 0000000..9dd337e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactoryTest.java
@@ -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.accessibility.floatingmenu;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class MenuNotificationFactoryTest extends SysuiTestCase {
+    private MenuNotificationFactory mMenuNotificationFactory;
+
+    @Before
+    public void setUp() {
+        mMenuNotificationFactory = new MenuNotificationFactory(mContext);
+    }
+
+    @Test
+    public void createHiddenNotification_hasUndoAndDeleteAction() {
+        Notification notification = mMenuNotificationFactory.createHiddenNotification();
+
+        assertThat(notification.contentIntent.getIntent().getAction()).isEqualTo(
+                MenuNotificationFactory.ACTION_UNDO);
+        assertThat(notification.deleteIntent.getIntent().getAction()).isEqualTo(
+                MenuNotificationFactory.ACTION_DELETE);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index be6f3ff..68879a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -21,15 +21,30 @@
 import static android.view.WindowInsets.Type.displayCutout;
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsets.Type.systemBars;
+
+import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_DELETE;
+import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_UNDO;
 import static com.android.systemui.accessibility.floatingmenu.MenuViewLayer.LayerIndex;
+
 import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
@@ -40,6 +55,8 @@
 import android.os.UserHandle;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -53,16 +70,21 @@
 import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.SysuiTestableContext;
 import com.android.systemui.util.settings.SecureSettings;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
@@ -98,6 +120,12 @@
     @Rule
     public MockitoRule mockito = MockitoJUnit.rule();
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Spy
+    private SysuiTestableContext mSpyContext = getContext();
     @Mock
     private IAccessibilityFloatingMenu mFloatingMenu;
 
@@ -110,8 +138,12 @@
     @Mock
     private AccessibilityManager mStubAccessibilityManager;
 
+    private final NotificationManager mMockNotificationManager = mock(NotificationManager.class);
+
     @Before
     public void setUp() throws Exception {
+        mSpyContext.addMockSystemService(Context.NOTIFICATION_SERVICE, mMockNotificationManager);
+
         final Rect mDisplayBounds = new Rect();
         mDisplayBounds.set(/* left= */ 0, /* top= */ 0, DISPLAY_WINDOW_WIDTH,
                 DISPLAY_WINDOW_HEIGHT);
@@ -119,31 +151,31 @@
                 new WindowMetrics(mDisplayBounds, fakeDisplayInsets(), /* density = */ 0.0f));
         doReturn(mWindowMetrics).when(mStubWindowManager).getCurrentWindowMetrics();
 
-        mMenuViewLayer = new MenuViewLayer(mContext, mStubWindowManager, mStubAccessibilityManager,
-                mFloatingMenu, mSecureSettings);
+        mMenuViewLayer = new MenuViewLayer(mSpyContext, mStubWindowManager,
+                mStubAccessibilityManager, mFloatingMenu, mSecureSettings);
         mMenuView = (MenuView) mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW);
         mMenuAnimationController = mMenuView.getMenuAnimationController();
 
         mLastAccessibilityButtonTargets =
-                Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                Settings.Secure.getStringForUser(mSpyContext.getContentResolver(),
                         Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT);
         mLastEnabledAccessibilityServices =
-                Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                Settings.Secure.getStringForUser(mSpyContext.getContentResolver(),
                         Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, UserHandle.USER_CURRENT);
 
         mMenuViewLayer.onAttachedToWindow();
-        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+        Settings.Secure.putStringForUser(mSpyContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", UserHandle.USER_CURRENT);
-        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+        Settings.Secure.putStringForUser(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "", UserHandle.USER_CURRENT);
     }
 
     @After
     public void tearDown() throws Exception {
-        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+        Settings.Secure.putStringForUser(mSpyContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, mLastAccessibilityButtonTargets,
                 UserHandle.USER_CURRENT);
-        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+        Settings.Secure.putStringForUser(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, mLastEnabledAccessibilityServices,
                 UserHandle.USER_CURRENT);
 
@@ -188,7 +220,7 @@
         setupEnabledAccessibilityServiceList();
 
         mMenuViewLayer.mDismissMenuAction.run();
-        final String value = Settings.Secure.getString(mContext.getContentResolver(),
+        final String value = Settings.Secure.getString(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
 
         assertThat(value).isEqualTo("");
@@ -203,7 +235,7 @@
                 AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY)).thenReturn(stubShortcutTargets);
 
         mMenuViewLayer.mDismissMenuAction.run();
-        final String value = Settings.Secure.getString(mContext.getContentResolver(),
+        final String value = Settings.Secure.getString(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
 
         assertThat(value).isEqualTo(TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString());
@@ -278,9 +310,60 @@
         assertThat(mMenuView.getTranslationX()).isEqualTo(beforePosition.x);
         assertThat(mMenuView.getTranslationY()).isEqualTo(beforePosition.y);
     }
+    @Test
+    @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE)
+    public void onReleasedInTarget_hideMenuAndShowNotificationWithExpectedActions() {
+        dragMenuThenReleasedInTarget();
+
+        verify(mMockNotificationManager).notify(
+                eq(SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN),
+                any(Notification.class));
+        ArgumentCaptor<IntentFilter> intentFilterCaptor = ArgumentCaptor.forClass(
+                IntentFilter.class);
+        verify(mSpyContext).registerReceiver(
+                any(BroadcastReceiver.class),
+                intentFilterCaptor.capture(),
+                anyInt());
+        assertThat(intentFilterCaptor.getValue().matchAction(ACTION_UNDO)).isTrue();
+        assertThat(intentFilterCaptor.getValue().matchAction(ACTION_DELETE)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE)
+    public void receiveActionUndo_dismissNotificationAndMenuVisible() {
+        ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = ArgumentCaptor.forClass(
+                BroadcastReceiver.class);
+        dragMenuThenReleasedInTarget();
+
+        verify(mSpyContext).registerReceiver(broadcastReceiverCaptor.capture(),
+                any(IntentFilter.class), anyInt());
+        broadcastReceiverCaptor.getValue().onReceive(mSpyContext, new Intent(ACTION_UNDO));
+
+        verify(mSpyContext).unregisterReceiver(broadcastReceiverCaptor.getValue());
+        verify(mMockNotificationManager).cancel(
+                SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN);
+        assertThat(mMenuView.getVisibility()).isEqualTo(VISIBLE);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE)
+    public void receiveActionDelete_dismissNotificationAndHideMenu() {
+        ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = ArgumentCaptor.forClass(
+                BroadcastReceiver.class);
+        dragMenuThenReleasedInTarget();
+
+        verify(mSpyContext).registerReceiver(broadcastReceiverCaptor.capture(),
+                any(IntentFilter.class), anyInt());
+        broadcastReceiverCaptor.getValue().onReceive(mSpyContext, new Intent(ACTION_DELETE));
+
+        verify(mSpyContext).unregisterReceiver(broadcastReceiverCaptor.getValue());
+        verify(mMockNotificationManager).cancel(
+                SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN);
+        verify(mFloatingMenu).hide();
+    }
 
     private void setupEnabledAccessibilityServiceList() {
-        Settings.Secure.putString(mContext.getContentResolver(),
+        Settings.Secure.putString(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
                 TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString());
 
@@ -344,6 +427,12 @@
                     springAnimation.skipToEnd();
                     springAnimation.doAnimationFrame(500);
                 });
+    }
 
+    private void dragMenuThenReleasedInTarget() {
+        MagnetizedObject.MagnetListener magnetListener =
+                mMenuViewLayer.getDragToInteractAnimationController().getMagnetListener();
+        magnetListener.onReleasedInTarget(
+                new MagnetizedObject.MagneticTarget(mock(View.class), 200));
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
index edd781d..2d9d5ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -20,14 +20,16 @@
 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.coroutines.collectValues
+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.util.mockito.mock
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.milliseconds
-import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -37,23 +39,21 @@
 @SmallTest
 @RunWith(JUnit4::class)
 class KeyguardTransitionAnimationFlowTest : SysuiTestCase() {
-    private lateinit var underTest: KeyguardTransitionAnimationFlow.SharedFlowBuilder
-    private lateinit var repository: FakeKeyguardTransitionRepository
-    private lateinit var testScope: TestScope
+    val kosmos = testKosmos()
+    val testScope = kosmos.testScope
+    val animationFlow = kosmos.keyguardTransitionAnimationFlow
+    val repository = kosmos.fakeKeyguardTransitionRepository
+
+    private lateinit var underTest: KeyguardTransitionAnimationFlow.FlowBuilder
 
     @Before
     fun setUp() {
-        testScope = TestScope()
-        repository = FakeKeyguardTransitionRepository()
         underTest =
-            KeyguardTransitionAnimationFlow(
-                    testScope.backgroundScope,
-                    mock(),
-                )
-                .setup(
-                    duration = 1000.milliseconds,
-                    stepFlow = repository.transitions,
-                )
+            animationFlow.setup(
+                duration = 1000.milliseconds,
+                from = KeyguardState.GONE,
+                to = KeyguardState.DREAMING,
+            )
     }
 
     @Test(expected = IllegalArgumentException::class)
@@ -83,6 +83,8 @@
                     onFinish = { 10f },
                 )
             var animationValues = collectLastValue(flow)
+            runCurrent()
+
             repository.sendTransitionStep(step(1f, TransitionState.FINISHED), validateStep = false)
             assertThat(animationValues()).isEqualTo(10f)
         }
@@ -97,6 +99,8 @@
                     onCancel = { 100f },
                 )
             var animationValues = collectLastValue(flow)
+            runCurrent()
+
             repository.sendTransitionStep(step(0.5f, TransitionState.CANCELED))
             assertThat(animationValues()).isEqualTo(100f)
         }
@@ -111,6 +115,8 @@
                     onStep = { it },
                 )
             var animationValues = collectLastValue(flow)
+            runCurrent()
+
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             assertThat(animationValues()).isEqualTo(0f)
 
@@ -137,6 +143,8 @@
                     onStep = { it },
                 )
             var animationValues = collectLastValue(flow)
+            runCurrent()
+
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0f))
             repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
@@ -157,17 +165,56 @@
                     duration = 1000.milliseconds,
                     onStep = { it * 2 },
                 )
-            var animationValues = collectLastValue(flow)
+            val animationValues by collectLastValue(flow)
+            runCurrent()
+
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
-            assertFloat(animationValues(), 0f)
+            assertFloat(animationValues, 0f)
             repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
-            assertFloat(animationValues(), 0.6f)
+            assertFloat(animationValues, 0.6f)
             repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
-            assertFloat(animationValues(), 1.2f)
+            assertFloat(animationValues, 1.2f)
             repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
-            assertFloat(animationValues(), 1.6f)
+            assertFloat(animationValues, 1.6f)
             repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
-            assertFloat(animationValues(), 2f)
+            assertFloat(animationValues, 2f)
+        }
+
+    @Test
+    fun sameFloatValueWithTheSameTransitionStateDoesNotEmitTwice() =
+        testScope.runTest {
+            val flow =
+                underTest.sharedFlow(
+                    duration = 1000.milliseconds,
+                    onStep = { it },
+                )
+            val values by collectValues(flow)
+            runCurrent()
+
+            repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+            repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+
+            assertThat(values.size).isEqualTo(1)
+            assertThat(values[0]).isEqualTo(0.3f)
+        }
+
+    @Test
+    fun sameFloatValueWithADifferentTransitionStateDoesEmitTwice() =
+        testScope.runTest {
+            val flow =
+                underTest.sharedFlow(
+                    duration = 1000.milliseconds,
+                    onStep = { it },
+                )
+            val values by collectValues(flow)
+            runCurrent()
+
+            repository.sendTransitionStep(step(0.3f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+
+            assertThat(values.size).isEqualTo(2)
+            assertThat(values[0]).isEqualTo(0.3f)
+            assertThat(values[0]).isEqualTo(0.3f)
         }
 
     private fun assertFloat(actual: Float?, expected: Float) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
index d959872..87391cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
@@ -31,6 +31,7 @@
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -50,6 +51,7 @@
     fun transitionToAlternateBouncer_scrimAlphaUpdate() =
         testScope.runTest {
             val scrimAlphas by collectValues(underTest.scrimAlpha)
+            runCurrent()
 
             transitionRepository.sendTransitionSteps(
                 listOf(
@@ -69,17 +71,17 @@
     fun transitionFromAlternateBouncer_scrimAlphaUpdate() =
         testScope.runTest {
             val scrimAlphas by collectValues(underTest.scrimAlpha)
+            runCurrent()
 
             transitionRepository.sendTransitionSteps(
                 listOf(
-                    stepToAlternateBouncer(0f, TransitionState.STARTED),
-                    stepToAlternateBouncer(.4f),
-                    stepToAlternateBouncer(.6f),
-                    stepToAlternateBouncer(1f),
+                    stepFromAlternateBouncer(0f, TransitionState.STARTED),
+                    stepFromAlternateBouncer(.4f),
+                    stepFromAlternateBouncer(.6f),
+                    stepFromAlternateBouncer(1f),
                 ),
                 testScope,
             )
-
             assertThat(scrimAlphas.size).isEqualTo(4)
             scrimAlphas.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
index af8d8a8..795e68d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -63,6 +64,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             val deviceEntryBackgroundViewAlpha by
                 collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            runCurrent()
 
             // fade in
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
index daafe12..75994da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
@@ -39,6 +39,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -78,6 +79,8 @@
     fun scrimAlpha_runDimissFromKeyguard_shadeExpanded() =
         testScope.runTest {
             val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
+            runCurrent()
+
             shadeRepository.setLockscreenShadeExpansion(1f)
             whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true)
 
@@ -101,6 +104,8 @@
     fun scrimAlpha_runDimissFromKeyguard_shadeNotExpanded() =
         testScope.runTest {
             val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
+            runCurrent()
+
             shadeRepository.setLockscreenShadeExpansion(0f)
 
             whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true)
@@ -123,6 +128,7 @@
     fun scrimBehindAlpha_leaveShadeOpen() =
         testScope.runTest {
             val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
+            runCurrent()
 
             sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
 
@@ -146,6 +152,8 @@
     fun scrimBehindAlpha_doNotLeaveShadeOpen() =
         testScope.runTest {
             val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
+            runCurrent()
+
             keyguardTransitionRepository.sendTransitionSteps(
                 listOf(
                     step(0f, TransitionState.STARTED),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelTest.kt
index dd542d4..471029b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelTest.kt
@@ -20,18 +20,15 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+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.KeyguardTransitionAnimationFlow
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -39,29 +36,10 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class DozingToLockscreenTransitionViewModelTest : SysuiTestCase() {
-    private lateinit var testScope: TestScope
-    private lateinit var underTest: DozingToLockscreenTransitionViewModel
-    private lateinit var repository: FakeKeyguardTransitionRepository
-
-    @Before
-    fun setUp() {
-        testScope = TestScope()
-        repository = FakeKeyguardTransitionRepository()
-        underTest =
-            DozingToLockscreenTransitionViewModel(
-                interactor =
-                    KeyguardTransitionInteractorFactory.create(
-                            scope = testScope.backgroundScope,
-                            repository = repository,
-                        )
-                        .keyguardTransitionInteractor,
-                animationFlow =
-                    KeyguardTransitionAnimationFlow(
-                        scope = testScope.backgroundScope,
-                        logger = mock()
-                    ),
-            )
-    }
+    val kosmos = testKosmos()
+    val testScope = kosmos.testScope
+    val repository = kosmos.fakeKeyguardTransitionRepository
+    val underTest = kosmos.dozingToLockscreenTransitionViewModel
 
     @Test
     fun deviceEntryParentViewShows() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
index a105008..1c9c942 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
@@ -31,6 +31,7 @@
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -52,6 +53,7 @@
             val pixels = -100f
             val enterFromTopTranslationY by
                 collectLastValue(underTest.enterFromTopTranslationY(pixels.toInt()))
+            runCurrent()
 
             // The animation should only start > .4f way through
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -72,6 +74,7 @@
     fun enterFromTopAnimationAlpha() =
         testScope.runTest {
             val enterFromTopAnimationAlpha by collectLastValue(underTest.enterFromTopAnimationAlpha)
+            runCurrent()
 
             // The animation should only start > .4f way through
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -92,6 +95,7 @@
         testScope.runTest {
             val deviceEntryBackgroundViewAlpha by
                 collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            runCurrent()
 
             // immediately 0f
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -113,6 +117,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // animation doesn't start until the end
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -137,6 +142,7 @@
             fingerprintPropertyRepository.supportsRearFps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // animation doesn't start until the end
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -161,6 +167,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // animation doesn't start until the end
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelTest.kt
index 5e62317..1912987 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -51,6 +52,7 @@
         testScope.runTest {
             val deviceEntryBackgroundViewAlpha by
                 collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            runCurrent()
 
             // immediately 0f
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -72,6 +74,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // immediately 1f
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -96,6 +99,7 @@
             fingerprintPropertyRepository.supportsRearFps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // no updates
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -120,6 +124,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // no updates
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt
index 9729022..c55c27c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -54,6 +55,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             val deviceEntryBackgroundViewAlpha by
                 collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            runCurrent()
 
             // immediately 0f
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -75,6 +77,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
 
@@ -92,6 +95,7 @@
             fingerprintPropertyRepository.supportsRearFps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             // animation doesn't start until the end
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -116,6 +120,7 @@
             fingerprintPropertyRepository.supportsUdfps()
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
             val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+            runCurrent()
 
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
             assertThat(deviceEntryParentViewAlpha).isNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
index 2c6436e..0796af0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -71,6 +72,7 @@
         testScope.runTest {
             fingerprintPropertyRepository.supportsUdfps()
             val bgViewAlpha by collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            runCurrent()
 
             // immediately 1f
             keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index ee27c5c..64fd80d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.plugins.PluginManager
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.eq
+import java.util.function.BiConsumer
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.fail
 import kotlinx.coroutines.CoroutineDispatcher
@@ -100,10 +101,7 @@
         override fun toString() = "Manager[$tag]"
         override fun getPackage(): String = mComponentName.getPackageName()
         override fun getComponentName(): ComponentName = mComponentName
-
-        private var isDebug: Boolean = false
-        override fun getIsDebug(): Boolean = isDebug
-        override fun setIsDebug(value: Boolean) { isDebug = value }
+        override fun setLogFunc(func: BiConsumer<String, String>) { }
 
         override fun loadPlugin() {
             if (!mIsLoaded) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
index bc50c25..3defee9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
@@ -112,7 +112,7 @@
         mPluginInstance = mPluginInstanceFactory.create(
                 mContext, mAppInfo, TEST_PLUGIN_COMPONENT_NAME,
                 TestPlugin.class, mPluginListener);
-        mPluginInstance.setIsDebug(true);
+        mPluginInstance.setLogFunc((tag, msg) -> Log.d((String) tag, (String) msg));
         mPluginContext = new WeakReference<>(mPluginInstance.getPluginContext());
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index a906a89..02e6fd5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -31,7 +31,7 @@
 import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE
 import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
 import android.telephony.satellite.SatelliteManager.SatelliteException
-import android.telephony.satellite.SatelliteStateCallback
+import android.telephony.satellite.SatelliteModemStateCallback
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -106,7 +106,7 @@
             val latest by collectLastValue(underTest.connectionState)
             runCurrent()
             val callback =
-                withArgCaptor<SatelliteStateCallback> {
+                withArgCaptor<SatelliteModemStateCallback> {
                     verify(satelliteManager).registerForSatelliteModemStateChanged(any(), capture())
                 }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java
index 1dab84e..cb6ce68 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.policy;
 
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -214,7 +215,8 @@
     public void testNetworkRequest() {
         verify(mConnectivityManager, times(1)).registerNetworkCallback(argThat(
                 (NetworkRequest request) ->
-                        request.equals(new NetworkRequest.Builder().clearCapabilities().build())
+                        request.equals(new NetworkRequest.Builder()
+                                .clearCapabilities().addTransportType(TRANSPORT_VPN).build())
                 ), any(NetworkCallback.class));
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
index eb287ee..95ff889 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
@@ -35,7 +35,8 @@
     @JvmStatic
     fun create(
         testScope: TestScope = TestScope(),
-        communalRepository: FakeCommunalRepository = FakeCommunalRepository(),
+        communalRepository: FakeCommunalRepository =
+            FakeCommunalRepository(testScope.backgroundScope),
         widgetRepository: FakeCommunalWidgetRepository =
             FakeCommunalWidgetRepository(testScope.backgroundScope),
         mediaRepository: FakeCommunalMediaRepository = FakeCommunalMediaRepository(),
@@ -51,6 +52,7 @@
                 communalRepository = communalRepository,
             )
         return WithDependencies(
+            testScope,
             communalRepository,
             widgetRepository,
             mediaRepository,
@@ -74,6 +76,7 @@
     }
 
     data class WithDependencies(
+        val testScope: TestScope,
         val communalRepository: FakeCommunalRepository,
         val widgetRepository: FakeCommunalWidgetRepository,
         val mediaRepository: FakeCommunalMediaRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index a94ca29..0c1dbfe 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -147,7 +147,6 @@
                 )
             }
         }
-
         _transitions.emit(step)
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt
index 8d6529a..dad1887 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt
@@ -19,6 +19,7 @@
 package com.android.systemui.keyguard.ui
 
 import com.android.keyguard.logging.keyguardTransitionAnimationLogger
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.applicationCoroutineScope
@@ -27,6 +28,7 @@
 val Kosmos.keyguardTransitionAnimationFlow by Fixture {
     KeyguardTransitionAnimationFlow(
         scope = applicationCoroutineScope,
+        transitionInteractor = keyguardTransitionInteractor,
         logger = keyguardTransitionAnimationLogger,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelKosmos.kt
index d9c6e4f..3ed9392 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +26,6 @@
 
 val Kosmos.alternateBouncerToAodTransitionViewModel by Fixture {
     AlternateBouncerToAodTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelKosmos.kt
index e4821b0..c909dd6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -26,7 +25,6 @@
 
 val Kosmos.alternateBouncerToGoneTransitionViewModel by Fixture {
     AlternateBouncerToGoneTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         bouncerToGoneFlows = bouncerToGoneFlows,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt
index 9f0466d..b4f1218 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -28,7 +27,6 @@
 val Kosmos.alternateBouncerViewModel by Fixture {
     AlternateBouncerViewModel(
         statusBarKeyguardViewManager = statusBarKeyguardViewManager,
-        transitionInteractor = keyguardTransitionInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModelKosmos.kt
index 44e5426..b6f278c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -26,7 +25,6 @@
 
 val Kosmos.aodToGoneTransitionViewModel by Fixture {
     AodToGoneTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelKosmos.kt
index b5a5f03..733340c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +26,6 @@
 
 val Kosmos.aodToLockscreenTransitionViewModel by Fixture {
     AodToLockscreenTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModelKosmos.kt
index 27ad0f0..8d066fc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -26,7 +25,6 @@
 
 val Kosmos.aodToOccludedTransitionViewModel by Fixture {
     AodToOccludedTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
index 6ffcc9a..c71c1c3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
@@ -20,7 +20,6 @@
 
 import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
 import com.android.systemui.flags.featureFlagsClassic
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -31,7 +30,6 @@
 
 val Kosmos.bouncerToGoneFlows by Fixture {
     BouncerToGoneFlows(
-        interactor = keyguardTransitionInteractor,
         statusBarStateController = sysuiStatusBarStateController,
         primaryBouncerInteractor = primaryBouncerInteractor,
         keyguardDismissActionInteractor = mock(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..400a0d8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToLockscreenTransitionViewModelKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.dozingToLockscreenTransitionViewModel by Fixture {
+    DozingToLockscreenTransitionViewModel(
+        animationFlow = keyguardTransitionAnimationFlow,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelKosmos.kt
index 00ece14..19e4241 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +26,6 @@
 
 var Kosmos.goneToAodTransitionViewModel by Fixture {
     GoneToAodTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelKosmos.kt
index 073b34b..b267a96 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -26,7 +25,6 @@
 
 val Kosmos.goneToDreamingTransitionViewModel by Fixture {
     GoneToDreamingTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelKosmos.kt
index 7865f71..07b4cd4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +26,6 @@
 
 val Kosmos.lockscreenToAodTransitionViewModel by Fixture {
     LockscreenToAodTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         shadeDependentFlows = shadeDependentFlows,
         animationFlow = keyguardTransitionAnimationFlow,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelKosmos.kt
index b9f4b71..56d5ff6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -26,7 +25,6 @@
 
 val Kosmos.lockscreenToDreamingTransitionViewModel by Fixture {
     LockscreenToDreamingTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         shadeDependentFlows = shadeDependentFlows,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelKosmos.kt
index 475aa2d..1b2337f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -26,7 +25,6 @@
 
 val Kosmos.lockscreenToGoneTransitionViewModel by Fixture {
     LockscreenToGoneTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelKosmos.kt
index 8541a4f..9953d39 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +26,6 @@
 
 val Kosmos.lockscreenToOccludedTransitionViewModel by Fixture {
     LockscreenToOccludedTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         shadeDependentFlows = shadeDependentFlows,
         configurationInteractor = configurationInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelKosmos.kt
index 65c47fc..f094f22 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -26,7 +25,6 @@
 
 val Kosmos.lockscreenToPrimaryBouncerTransitionViewModel by Fixture {
     LockscreenToPrimaryBouncerTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         shadeDependentFlows = shadeDependentFlows,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelKosmos.kt
index ddde549..b7867b6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAodTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +26,6 @@
 
 val Kosmos.occludedToAodTransitionViewModel by Fixture {
     OccludedToAodTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt
index 93ecb79..e6651a4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt
@@ -20,7 +20,6 @@
 
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -28,7 +27,6 @@
 
 var Kosmos.occludedToLockscreenTransitionViewModel by Fixture {
     OccludedToLockscreenTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         configurationInteractor = configurationInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelKosmos.kt
index a7f29d6..8d88730 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +26,6 @@
 
 val Kosmos.primaryBouncerToAodTransitionViewModel by Fixture {
     PrimaryBouncerToAodTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
index ace6ae3..ab28d0d6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
@@ -20,7 +20,6 @@
 
 import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
 import com.android.systemui.flags.featureFlagsClassic
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -30,7 +29,6 @@
 
 val Kosmos.primaryBouncerToGoneTransitionViewModel by Fixture {
     PrimaryBouncerToGoneTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         statusBarStateController = sysuiStatusBarStateController,
         primaryBouncerInteractor = primaryBouncerInteractor,
         keyguardDismissActionInteractor = mock(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
index 3bbabf7..8566251 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
@@ -19,7 +19,6 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +26,6 @@
 
 val Kosmos.primaryBouncerToLockscreenTransitionViewModel by Fixture {
     PrimaryBouncerToLockscreenTransitionViewModel(
-        interactor = keyguardTransitionInteractor,
         deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt
index 021e7df..ac90a45 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt
@@ -77,6 +77,8 @@
 
     override fun isVpnBranded(): Boolean = fakeState.isVpnBranded
 
+    override fun isVpnValidated(): Boolean = fakeState.isVpnValidated
+
     override fun getPrimaryVpnName(): String? = fakeState.primaryVpnName
 
     override fun getWorkProfileVpnName(): String? = fakeState.workProfileVpnName
@@ -110,6 +112,7 @@
         var isVpnEnabled: Boolean = false,
         var isVpnRestricted: Boolean = false,
         var isVpnBranded: Boolean = false,
+        var isVpnValidated: Boolean = false,
         var primaryVpnName: String? = null,
         var workProfileVpnName: String? = null,
         var hasCACertInCurrentUser: Boolean = false,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeSecurityController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeSecurityController.java
index 76199e3..791165d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeSecurityController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeSecurityController.java
@@ -109,6 +109,11 @@
     }
 
     @Override
+    public boolean isVpnValidated() {
+        return false;
+    }
+
+    @Override
     public String getPrimaryVpnName() {
         return null;
     }
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index b403a7f..7f542d1 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -408,5 +408,9 @@
     // Notify the user about external display events related to screenshot.
     // Package: com.android.systemui
     NOTE_GLOBAL_SCREENSHOT_EXTERNAL_DISPLAY = 1008;
+
+    // Notify the user that accessibility floating menu is hidden.
+    // Package: com.android.systemui
+    NOTE_A11Y_FLOATING_MENU_HIDDEN = 1009;
   }
 }
diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig
index d695d36..549fa36 100644
--- a/services/backup/flags.aconfig
+++ b/services/backup/flags.aconfig
@@ -7,4 +7,12 @@
             "restore for apps that have been launched."
     bug: "308401499"
     is_fixed_read_only: true
+}
+
+flag {
+    name: "enable_max_size_writes_to_pipes"
+    namespace: "onboarding"
+    description: "Enables the write buffer to pipes to be of maximum size."
+    bug: "265976737"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index 6aed9aa..cca166b 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -40,8 +40,8 @@
 
 import com.android.server.EventLogTags;
 import com.android.server.backup.BackupAgentTimeoutParameters;
-import com.android.server.backup.BackupAndRestoreFeatureFlags;
 import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.Flags;
 import com.android.server.backup.FullBackupJob;
 import com.android.server.backup.OperationStorage;
 import com.android.server.backup.OperationStorage.OpState;
@@ -390,8 +390,11 @@
 
             // Set up to send data to the transport
             final int N = mPackages.size();
-            final int chunkSizeInBytes =
-                    BackupAndRestoreFeatureFlags.getFullBackupWriteToTransportBufferSizeBytes();
+            int chunkSizeInBytes = 8 * 1024; // 8KB
+            if (Flags.enableMaxSizeWritesToPipes()) {
+                // Linux pipe capacity (buffer size) is 16 pages where each page is 4KB
+                chunkSizeInBytes = 64 * 1024; // 64KB
+            }
             final byte[] buffer = new byte[chunkSizeInBytes];
             for (int i = 0; i < N; i++) {
                 mBackupRunner = null;
diff --git a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
index ff72476..2c9eb51 100644
--- a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
+++ b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
@@ -29,7 +29,6 @@
 import android.app.IBackupAgent;
 import android.app.backup.BackupAgent;
 import android.app.backup.BackupAnnotations;
-import android.app.backup.BackupManager;
 import android.app.backup.FullBackup;
 import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.IFullBackupRestoreObserver;
@@ -51,6 +50,7 @@
 import com.android.server.backup.BackupAgentTimeoutParameters;
 import com.android.server.backup.BackupRestoreTask;
 import com.android.server.backup.FileMetadata;
+import com.android.server.backup.Flags;
 import com.android.server.backup.KeyValueAdbRestoreEngine;
 import com.android.server.backup.OperationStorage;
 import com.android.server.backup.OperationStorage.OpType;
@@ -157,13 +157,19 @@
         mMonitor = monitor;
         mOnlyPackage = onlyPackage;
         mAllowApks = allowApks;
-        mBuffer = new byte[32 * 1024];
         mAgentTimeoutParameters = Objects.requireNonNull(
                 backupManagerService.getAgentTimeoutParameters(),
                 "Timeout parameters cannot be null");
         mIsAdbRestore = isAdbRestore;
         mUserId = backupManagerService.getUserId();
         mBackupEligibilityRules = backupEligibilityRules;
+
+        if (Flags.enableMaxSizeWritesToPipes()) {
+            // Linux pipe capacity (buffer size) is 16 pages where each page is 4KB
+            mBuffer = new byte[64 * 1024]; // 64KB
+        } else {
+            mBuffer = new byte[32 * 1024];
+        }
     }
 
     @VisibleForTesting
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 316a16d..2fbc3cd 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -968,7 +968,12 @@
             throws Exception {
         Set<String> excludedKeysForPackage = getExcludedKeysForPackage(packageName);
 
-        byte[] buffer = new byte[8192]; // will grow when needed
+        int bufferSize = 8192; // 8KB
+        if (Flags.enableMaxSizeWritesToPipes()) {
+            // Linux pipe capacity (buffer size) is 16 pages where each page is 4KB
+            bufferSize = 64 * 1024; // 64KB
+        }
+        byte[] buffer = new byte[bufferSize]; // will grow when needed
         while (in.readNextHeader()) {
             final String key = in.getKey();
             final int size = in.getDataSize();
@@ -1116,7 +1121,11 @@
             ParcelFileDescriptor tReadEnd = mTransportPipes[0];
             ParcelFileDescriptor tWriteEnd = mTransportPipes[1];
 
-            int bufferSize = 32 * 1024;
+            int bufferSize = 32 * 1024; // 32KB
+            if (Flags.enableMaxSizeWritesToPipes()) {
+                // Linux pipe capacity (buffer size) is 16 pages where each page is 4KB
+                bufferSize = 64 * 1024; // 64KB
+            }
             byte[] buffer = new byte[bufferSize];
             FileOutputStream engineOut = new FileOutputStream(eWriteEnd.getFileDescriptor());
             FileInputStream transportIn = new FileInputStream(tReadEnd.getFileDescriptor());
diff --git a/services/backup/java/com/android/server/backup/utils/FullBackupUtils.java b/services/backup/java/com/android/server/backup/utils/FullBackupUtils.java
index 1c0cd87..843354e 100644
--- a/services/backup/java/com/android/server/backup/utils/FullBackupUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/FullBackupUtils.java
@@ -21,7 +21,7 @@
 import android.os.ParcelFileDescriptor;
 import android.util.Slog;
 
-import com.android.server.backup.BackupAndRestoreFeatureFlags;
+import com.android.server.backup.Flags;
 
 import java.io.DataInputStream;
 import java.io.EOFException;
@@ -46,8 +46,11 @@
         // We do not take close() responsibility for the pipe FD
         FileInputStream raw = new FileInputStream(inPipe.getFileDescriptor());
         DataInputStream in = new DataInputStream(raw);
-        final int chunkSizeInBytes =
-                BackupAndRestoreFeatureFlags.getFullBackupUtilsRouteBufferSizeBytes();
+        int chunkSizeInBytes = 32 * 1024; // 32KB
+        if (Flags.enableMaxSizeWritesToPipes()) {
+            // Linux pipe capacity (buffer size) is 16 pages where each page is 4KB
+            chunkSizeInBytes = 64 * 1024; // 64KB
+        }
         byte[] buffer = new byte[chunkSizeInBytes];
         int chunkTotal;
         while ((chunkTotal = in.readInt()) > 0) {
diff --git a/services/backup/java/com/android/server/backup/utils/RestoreUtils.java b/services/backup/java/com/android/server/backup/utils/RestoreUtils.java
index 0accb9f..5a8533a 100644
--- a/services/backup/java/com/android/server/backup/utils/RestoreUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/RestoreUtils.java
@@ -40,6 +40,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.LocalServices;
 import com.android.server.backup.FileMetadata;
+import com.android.server.backup.Flags;
 import com.android.server.backup.restore.RestoreDeleteObserver;
 import com.android.server.backup.restore.RestorePolicy;
 
@@ -93,7 +94,12 @@
                 try (Session session = installer.openSession(sessionId)) {
                     try (OutputStream apkStream = session.openWrite(info.packageName, 0,
                             info.size)) {
-                        byte[] buffer = new byte[32 * 1024];
+                        int bufferSize = 32 * 1024; // 32KB
+                        if (Flags.enableMaxSizeWritesToPipes()) {
+                            // Linux pipe capacity (buffer size) is 16 pages where each page is 4KB
+                            bufferSize = 64 * 1024; // 64KB
+                        }
+                        byte[] buffer = new byte[bufferSize];
                         long size = info.size;
                         while (size > 0) {
                             long toRead = (buffer.length < size) ? buffer.length : size;
diff --git a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
index 720687e..0e66fbc 100644
--- a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
+++ b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
@@ -23,12 +23,12 @@
 import android.os.Build;
 import android.util.Slog;
 
-import com.google.security.cryptauth.lib.securegcm.BadHandleException;
-import com.google.security.cryptauth.lib.securegcm.CryptoException;
-import com.google.security.cryptauth.lib.securegcm.D2DConnectionContextV1;
-import com.google.security.cryptauth.lib.securegcm.D2DHandshakeContext;
-import com.google.security.cryptauth.lib.securegcm.D2DHandshakeContext.Role;
-import com.google.security.cryptauth.lib.securegcm.HandshakeException;
+import com.google.security.cryptauth.lib.securegcm.ukey2.BadHandleException;
+import com.google.security.cryptauth.lib.securegcm.ukey2.CryptoException;
+import com.google.security.cryptauth.lib.securegcm.ukey2.D2DConnectionContextV1;
+import com.google.security.cryptauth.lib.securegcm.ukey2.D2DHandshakeContext;
+import com.google.security.cryptauth.lib.securegcm.ukey2.D2DHandshakeContext.Role;
+import com.google.security.cryptauth.lib.securegcm.ukey2.HandshakeException;
 
 import libcore.io.IoUtils;
 import libcore.io.Streams;
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 9eb35fd..eb6fdd7 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -101,6 +101,7 @@
 import com.android.internal.telephony.IPhoneStateListener;
 import com.android.internal.telephony.ITelephonyRegistry;
 import com.android.internal.telephony.TelephonyPermissions;
+import com.android.internal.telephony.flags.Flags;
 import com.android.internal.telephony.util.TelephonyUtils;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
@@ -2679,6 +2680,14 @@
         if (!checkNotifyPermission("notifyEmergencyNumberList()")) {
             return;
         }
+        if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
+            if (!mContext.getPackageManager().hasSystemFeature(
+                    PackageManager.FEATURE_TELEPHONY_CALLING)) {
+                // TelephonyManager.getEmergencyNumberList() throws an exception if
+                // FEATURE_TELEPHONY_CALLING is not defined.
+                return;
+            }
+        }
 
         synchronized (mRecords) {
             if (validatePhoneId(phoneId)) {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 7b14a02..02f4485 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -4464,12 +4464,6 @@
                     }
                 }
                 if (userId > 0) {
-                    if (mAm.isSystemUserOnly(sInfo.flags)) {
-                        Slog.w(TAG_SERVICE, service + " is only available for the SYSTEM user,"
-                                + " calling userId is: " + userId);
-                        return null;
-                    }
-
                     if (mAm.isSingleton(sInfo.processName, sInfo.applicationInfo,
                             sInfo.name, sInfo.flags)
                             && mAm.isValidSingletonCall(callingUid, sInfo.applicationInfo.uid)) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 8ad60e6..72e62c3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -243,7 +243,7 @@
     /**
      * The default value to {@link #KEY_ENABLE_NEW_OOMADJ}.
      */
-    private static final boolean DEFAULT_ENABLE_NEW_OOM_ADJ = Flags.oomadjusterCorrectnessRewrite();
+    private static final boolean DEFAULT_ENABLE_NEW_OOM_ADJ = false;
 
     /**
      * Same as {@link TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 0c70bfe..fddb570 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -13747,11 +13747,6 @@
         return result;
     }
 
-    boolean isSystemUserOnly(int flags) {
-        return android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders()
-                && (flags & ServiceInfo.FLAG_SYSTEM_USER_ONLY) != 0;
-    }
-
     /**
      * Checks to see if the caller is in the same app as the singleton
      * component, or the component is in a special app. It allows special apps
diff --git a/services/core/java/com/android/server/am/AnrHelper.java b/services/core/java/com/android/server/am/AnrHelper.java
index e0a2246..9fc0bf9 100644
--- a/services/core/java/com/android/server/am/AnrHelper.java
+++ b/services/core/java/com/android/server/am/AnrHelper.java
@@ -63,6 +63,11 @@
     private static final long CONSECUTIVE_ANR_TIME_MS = TimeUnit.MINUTES.toMillis(2);
 
     /**
+     * Time to wait before taking dumps for other processes to reduce load at boot time.
+     */
+    private static final long SELF_ONLY_AFTER_BOOT_MS = TimeUnit.MINUTES.toMillis(10);
+
+    /**
      * The keep alive time for the threads in the helper threadpool executor
     */
     private static final int DEFAULT_THREAD_KEEP_ALIVE_SECOND = 10;
@@ -231,7 +236,8 @@
                 // If there are many ANR at the same time, the latency may be larger.
                 // If the latency is too large, the stack trace might not be meaningful.
                 final long reportLatency = startTime - r.mTimestamp;
-                final boolean onlyDumpSelf = reportLatency > EXPIRED_REPORT_TIME_MS;
+                final boolean onlyDumpSelf = reportLatency > EXPIRED_REPORT_TIME_MS
+                        || startTime < SELF_ONLY_AFTER_BOOT_MS;
                 r.appNotResponding(onlyDumpSelf);
                 final long endTime = SystemClock.uptimeMillis();
                 Slog.d(TAG, "Completed ANR of " + r.mApp.processName + " in "
diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java
index 30f21a6..095d907 100644
--- a/services/core/java/com/android/server/am/ContentProviderHelper.java
+++ b/services/core/java/com/android/server/am/ContentProviderHelper.java
@@ -1249,9 +1249,9 @@
             ProviderInfo cpi = providers.get(i);
             boolean singleton = mService.isSingleton(cpi.processName, cpi.applicationInfo,
                     cpi.name, cpi.flags);
-            if (isSingletonOrSystemUserOnly(cpi) && app.userId != UserHandle.USER_SYSTEM) {
-                // This is a singleton or a SYSTEM user only provider, but a user besides the
-                // SYSTEM user is asking to initialize a process it runs
+            if (singleton && app.userId != UserHandle.USER_SYSTEM) {
+                // This is a singleton provider, but a user besides the
+                // default user is asking to initialize a process it runs
                 // in...  well, no, it doesn't actually run in this process,
                 // it runs in the process of the default user.  Get rid of it.
                 providers.remove(i);
@@ -1398,7 +1398,8 @@
                                     final boolean processMatch =
                                             Objects.equals(pi.processName, app.processName)
                                             || pi.multiprocess;
-                                    final boolean userMatch = !isSingletonOrSystemUserOnly(pi)
+                                    final boolean userMatch = !mService.isSingleton(
+                                            pi.processName, pi.applicationInfo, pi.name, pi.flags)
                                             || app.userId == UserHandle.USER_SYSTEM;
                                     final boolean isInstantApp = pi.applicationInfo.isInstantApp();
                                     final boolean splitInstalled = pi.splitName == null
@@ -1984,13 +1985,4 @@
             return isAuthRedirected;
         }
     }
-
-    /**
-     * Returns true if Provider is either singleUser or systemUserOnly provider.
-     */
-    private boolean isSingletonOrSystemUserOnly(ProviderInfo pi) {
-        return (android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders()
-                && mService.isSystemUserOnly(pi.flags))
-                || mService.isSingleton(pi.processName, pi.applicationInfo, pi.name, pi.flags);
-    }
 }
diff --git a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java
index 9f31f37..5f12ce1 100644
--- a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java
+++ b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java
@@ -73,7 +73,8 @@
     private static final Set<Integer> DEFAULT_EVENT_SET = Sets.newHashSet(
             AmbientContextEvent.EVENT_COUGH,
             AmbientContextEvent.EVENT_SNORE,
-            AmbientContextEvent.EVENT_BACK_DOUBLE_TAP);
+            AmbientContextEvent.EVENT_BACK_DOUBLE_TAP,
+            AmbientContextEvent.EVENT_HEART_RATE);
 
     /** Default value in absence of {@link DeviceConfig} override. */
     private static final boolean DEFAULT_SERVICE_ENABLED = true;
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index e05824a..bf20ae3 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -1992,7 +1992,7 @@
             // TODO: return;
         } else {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                    "A2DP source device addr=" + Utils.anonymizeBluetoothAddress(address)
+                    "A2DP sink device addr=" + Utils.anonymizeBluetoothAddress(address)
                             + " now available").printLog(TAG));
         }
 
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 91d533c..4cbee2b 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -8835,6 +8835,8 @@
                 synchronized (VolumeStreamState.class) {
                     oldIndex = getIndex(device);
                     index = getValidIndex(index, hasModifyAudioSettings);
+                    // for STREAM_SYSTEM_ENFORCED, do not sync aliased streams on the enforced index
+                    int aliasIndex = index;
                     if ((mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED) && mCameraSoundForced) {
                         index = mIndexMax;
                     }
@@ -8853,7 +8855,8 @@
                         if (streamType != mStreamType &&
                                 mStreamVolumeAlias[streamType] == mStreamType &&
                                 (changed || !aliasStreamState.hasIndexForDevice(device))) {
-                            final int scaledIndex = rescaleIndex(index, mStreamType, streamType);
+                            final int scaledIndex =
+                                    rescaleIndex(aliasIndex, mStreamType, streamType);
                             aliasStreamState.setIndex(scaledIndex, device, caller,
                                     hasModifyAudioSettings);
                             if (isCurrentDevice) {
@@ -9375,6 +9378,14 @@
             if (mIsSingleVolume && (streamState.mStreamType != AudioSystem.STREAM_MUSIC)) {
                 return;
             }
+
+            // Persisting STREAM_SYSTEM_ENFORCED index is not needed as its alias (STREAM_RING)
+            // is persisted. This can also be problematic when the enforcement is active as it will
+            // override current SYSTEM_RING persisted value given they share the same settings name
+            // (due to aliasing).
+            if (streamState.mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED) {
+                return;
+            }
             if (streamState.hasValidSettingsName()) {
                 mSettings.putSystemIntForUser(mContentResolver,
                         streamState.getSettingNameForDevice(device),
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index a818c30..f51043d 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -634,16 +634,17 @@
             return;
         }
         List<BluetoothDevice> activeDevices = adapter.getActiveDevices(profile);
-        if (activeDevices.isEmpty() || activeDevices.get(0) == null) {
-            return;
+        BluetoothProfileConnectionInfo bpci = new BluetoothProfileConnectionInfo(profile);
+        for (BluetoothDevice device : activeDevices) {
+            if (device == null) {
+                continue;
+            }
+            AudioDeviceBroker.BtDeviceChangedData data = new AudioDeviceBroker.BtDeviceChangedData(
+                    device, null, bpci, "mBluetoothProfileServiceListener");
+            AudioDeviceBroker.BtDeviceInfo info = mDeviceBroker.createBtDeviceInfo(
+                    data, device, BluetoothProfile.STATE_CONNECTED);
+            mDeviceBroker.postBluetoothActiveDevice(info, 0 /* delay */);
         }
-        AudioDeviceBroker.BtDeviceChangedData data = new AudioDeviceBroker.BtDeviceChangedData(
-                activeDevices.get(0), null, new BluetoothProfileConnectionInfo(profile),
-                "mBluetoothProfileServiceListener");
-        AudioDeviceBroker.BtDeviceInfo info =
-                mDeviceBroker.createBtDeviceInfo(data, activeDevices.get(0),
-                        BluetoothProfile.STATE_CONNECTED);
-        mDeviceBroker.postBluetoothActiveDevice(info, 0 /* delay */);
     }
 
     // @GuardedBy("mDeviceBroker.mSetModeLock")
@@ -678,8 +679,11 @@
         if (adapter != null) {
             List<BluetoothDevice> activeDevices =
                     adapter.getActiveDevices(BluetoothProfile.HEADSET);
-            if (activeDevices.size() > 0 && activeDevices.get(0) != null) {
-                onSetBtScoActiveDevice(activeDevices.get(0));
+            for (BluetoothDevice device : activeDevices) {
+                if (device == null) {
+                    continue;
+                }
+                onSetBtScoActiveDevice(device);
             }
         } else {
             Log.e(TAG, "onHeadsetProfileConnected: Null BluetoothAdapter");
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index fbac924..9cf9119 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -3390,17 +3390,10 @@
         // with the corresponding displaydevice.
         HighBrightnessModeMetadata hbmMetadata =
                 mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
-        if (mConfigParameterProvider.isNewPowerControllerFeatureEnabled()) {
-            displayPowerController = new DisplayPowerController2(
-                    mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
-                    mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
-                    () -> handleBrightnessChange(display), hbmMetadata, mBootCompleted, mFlags);
-        } else {
-            displayPowerController = new DisplayPowerController(
-                    mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
-                    mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
-                    () -> handleBrightnessChange(display), hbmMetadata, mBootCompleted, mFlags);
-        }
+        displayPowerController = new DisplayPowerController(
+                mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
+                mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
+                () -> handleBrightnessChange(display), hbmMetadata, mBootCompleted, mFlags);
         mDisplayPowerControllers.append(display.getDisplayIdLocked(), displayPowerController);
         return displayPowerController;
     }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 734381b..087cacf 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,11 +17,12 @@
 package com.android.server.display;
 
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
+import static com.android.server.display.config.DisplayBrightnessMappingConfig.autoBrightnessPresetToString;
 
 import android.animation.Animator;
 import android.animation.ObjectAnimator;
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
@@ -31,8 +32,6 @@
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
 import android.hardware.display.AmbientBrightnessDayStats;
 import android.hardware.display.BrightnessChangeEvent;
@@ -45,6 +44,7 @@
 import android.metrics.LogMaker;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
@@ -56,12 +56,12 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.FloatProperty;
+import android.util.IndentingPrintWriter;
 import android.util.MathUtils;
 import android.util.MutableFloat;
 import android.util.MutableInt;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TimeUtils;
 import android.view.Display;
 
 import com.android.internal.R;
@@ -78,10 +78,15 @@
 import com.android.server.display.RampAnimator.DualRampAnimator;
 import com.android.server.display.brightness.BrightnessEvent;
 import com.android.server.display.brightness.BrightnessReason;
+import com.android.server.display.brightness.BrightnessUtils;
+import com.android.server.display.brightness.DisplayBrightnessController;
+import com.android.server.display.brightness.clamper.BrightnessClamperController;
+import com.android.server.display.brightness.strategy.AutomaticBrightnessStrategy;
 import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
 import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.Layout;
+import com.android.server.display.state.DisplayStateController;
 import com.android.server.display.utils.DebugUtils;
 import com.android.server.display.utils.SensorUtils;
 import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
@@ -119,12 +124,12 @@
     private static final String SCREEN_ON_BLOCKED_TRACE_NAME = "Screen on blocked";
     private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked";
 
-    private static final String TAG = "DisplayPowerController";
+    private static final String TAG = "DisplayPowerController2";
     // To enable these logs, run:
-    // 'adb shell setprop persist.log.tag.DisplayPowerController DEBUG && adb reboot'
+    // 'adb shell setprop persist.log.tag.DisplayPowerController2 DEBUG && adb reboot'
     private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
-
-    private static final boolean DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT = false;
+    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
@@ -138,36 +143,28 @@
     private static final int COLOR_FADE_OFF_ANIMATION_DURATION_MILLIS = 400;
 
     private static final int MSG_UPDATE_POWER_STATE = 1;
-    private static final int MSG_PROXIMITY_SENSOR_DEBOUNCED = 2;
-    private static final int MSG_SCREEN_ON_UNBLOCKED = 3;
-    private static final int MSG_SCREEN_OFF_UNBLOCKED = 4;
-    private static final int MSG_CONFIGURE_BRIGHTNESS = 5;
-    private static final int MSG_SET_TEMPORARY_BRIGHTNESS = 6;
-    private static final int MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT = 7;
-    private static final int MSG_IGNORE_PROXIMITY = 8;
-    private static final int MSG_STOP = 9;
-    private static final int MSG_UPDATE_BRIGHTNESS = 10;
-    private static final int MSG_UPDATE_RBC = 11;
-    private static final int MSG_BRIGHTNESS_RAMP_DONE = 12;
-    private static final int MSG_STATSD_HBM_BRIGHTNESS = 13;
-    private static final int MSG_SWITCH_USER = 14;
-    private static final int MSG_BOOT_COMPLETED = 15;
-    private static final int MSG_SET_DWBC_STRONG_MODE = 16;
-    private static final int MSG_SET_DWBC_COLOR_OVERRIDE = 17;
-    private static final int MSG_SET_DWBC_LOGGING_ENABLED = 18;
+    private static final int MSG_SCREEN_ON_UNBLOCKED = 2;
+    private static final int MSG_SCREEN_OFF_UNBLOCKED = 3;
+    private static final int MSG_CONFIGURE_BRIGHTNESS = 4;
+    private static final int MSG_SET_TEMPORARY_BRIGHTNESS = 5;
+    private static final int MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT = 6;
+    private static final int MSG_STOP = 7;
+    private static final int MSG_UPDATE_BRIGHTNESS = 8;
+    private static final int MSG_UPDATE_RBC = 9;
+    private static final int MSG_BRIGHTNESS_RAMP_DONE = 10;
+    private static final int MSG_STATSD_HBM_BRIGHTNESS = 11;
+    private static final int MSG_SWITCH_USER = 12;
+    private static final int MSG_BOOT_COMPLETED = 13;
+    private static final int MSG_SET_DWBC_STRONG_MODE = 14;
+    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;
 
-    private static final int PROXIMITY_UNKNOWN = -1;
-    private static final int PROXIMITY_NEGATIVE = 0;
-    private static final int PROXIMITY_POSITIVE = 1;
 
-    // Proximity sensor debounce delay in milliseconds for positive or negative transitions.
-    private static final int PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY = 0;
-    private static final int PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY = 250;
 
     private static final int BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS = 500;
 
-    // Trigger proximity if distance is less than 5 cm.
-    private static final float TYPICAL_PROXIMITY_THRESHOLD = 5.0f;
 
     // State machine constants for tracking initial brightness ramp skipping when enabled.
     private static final int RAMP_STATE_SKIP_NONE = 0;
@@ -181,6 +178,7 @@
     private static final int REPORTED_TO_POLICY_SCREEN_TURNING_OFF = 3;
 
     private static final int RINGBUFFER_MAX = 100;
+    private static final int RINGBUFFER_RBC_MAX = 20;
 
     private static final float[] BRIGHTNESS_RANGE_BOUNDARIES = {
         0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80,
@@ -236,10 +234,6 @@
     // Our handler.
     private final DisplayControllerHandler mHandler;
 
-    // Asynchronous callbacks into the power manager service.
-    // Only invoked from the handler thread while no locks are held.
-    private final DisplayPowerCallbacks mCallbacks;
-
     // Battery stats.
     @Nullable
     private final IBatteryStats mBatteryStats;
@@ -253,10 +247,10 @@
     // The display blanker.
     private final DisplayBlanker mBlanker;
 
-    // The LogicalDisplay tied to this DisplayPowerController.
+    // The LogicalDisplay tied to this DisplayPowerController2.
     private final LogicalDisplay mLogicalDisplay;
 
-    // The ID of the LogicalDisplay tied to this DisplayPowerController.
+    // The ID of the LogicalDisplay tied to this DisplayPowerController2.
     private final int mDisplayId;
 
     // The ID of the display which this display follows for brightness purposes.
@@ -272,34 +266,12 @@
     // Tracker for brightness settings changes.
     private final SettingsObserver mSettingsObserver;
 
-    // The proximity sensor, or null if not available or needed.
-    private Sensor mProximitySensor;
-
     // The doze screen brightness.
     private final float mScreenBrightnessDozeConfig;
 
-    // The dim screen brightness.
-    private final float mScreenBrightnessDimConfig;
-
-    // The minimum dim amount to use if the screen brightness is already below
-    // mScreenBrightnessDimConfig.
-    private final float mScreenBrightnessMinimumDimAmount;
-
-    private final float mScreenBrightnessDefault;
-
     // True if auto-brightness should be used.
     private boolean mUseSoftwareAutoBrightnessConfig;
 
-    // True if should use light sensor to automatically determine doze screen brightness.
-    private final boolean mAllowAutoBrightnessWhileDozingConfig;
-
-    // True if we want to persist the brightness value in nits even if the underlying display
-    // device changes.
-    private final boolean mPersistBrightnessNitsForDefaultDisplay;
-
-    // True if the brightness config has changed and the short-term model needs to be reset
-    private boolean mShouldResetShortTermModel;
-
     // Whether or not the color fade on screen on / off is enabled.
     private final boolean mColorFadeEnabled;
 
@@ -340,10 +312,6 @@
     @GuardedBy("mLock")
     private DisplayPowerRequest mPendingRequestLocked;
 
-    // True if a request has been made to wait for the proximity sensor to go negative.
-    @GuardedBy("mLock")
-    private boolean mPendingWaitForNegativeProximityLocked;
-
     // True if the pending power request or wait for negative proximity flag
     // has been changed since the last update occurred.
     @GuardedBy("mLock")
@@ -370,67 +338,36 @@
     // Must only be accessed on the handler thread.
     private DisplayPowerState mPowerState;
 
-    // True if the device should wait for negative proximity sensor before
-    // waking up the screen.  This is set to false as soon as a negative
-    // proximity sensor measurement is observed or when the device is forced to
-    // go to sleep by the user.  While true, the screen remains off.
-    private boolean mWaitingForNegativeProximity;
 
-    // True if the device should not take into account the proximity sensor
-    // until either the proximity sensor state changes, or there is no longer a
-    // request to listen to proximity sensor.
-    private boolean mIgnoreProximityUntilChanged;
-
-    // The actual proximity sensor threshold value.
-    private float mProximityThreshold;
-
-    // Set to true if the proximity sensor listener has been registered
-    // with the sensor manager.
-    private boolean mProximitySensorEnabled;
-
-    // The debounced proximity sensor state.
-    private int mProximity = PROXIMITY_UNKNOWN;
-
-    // The raw non-debounced proximity sensor state.
-    private int mPendingProximity = PROXIMITY_UNKNOWN;
-    private long mPendingProximityDebounceTime = -1; // -1 if fully debounced
-
-    // True if the screen was turned off because of the proximity sensor.
-    // When the screen turns on again, we report user activity to the power manager.
-    private boolean mScreenOffBecauseOfProximity;
 
     // The currently active screen on unblocker.  This field is non-null whenever
     // 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
     // turning off the screen.
     private boolean mPendingScreenOff;
 
-    // True if we have unfinished business and are holding a suspend blocker.
-    private boolean mUnfinishedBusiness;
-
     // 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;
 
-    // Remembers whether certain kinds of brightness adjustments
-    // were recently applied so that we can decide how to transition.
-    private boolean mAppliedAutoBrightness;
     private boolean mAppliedDimming;
-    private boolean mAppliedLowPower;
-    private boolean mAppliedScreenBrightnessOverride;
-    private boolean mAppliedTemporaryBrightness;
-    private boolean mAppliedTemporaryAutoBrightnessAdjustment;
-    private boolean mAppliedBrightnessBoost;
+
     private boolean mAppliedThrottling;
 
     // Reason for which the brightness was last changed. See {@link BrightnessReason} for more
@@ -456,7 +393,7 @@
     private final boolean mSkipScreenOnBrightnessRamp;
 
     // Display white balance components.
-    // Critical methods must be called on DPC handler thread.
+    // Critical methods must be called on DPC2 handler thread.
     @Nullable
     private final DisplayWhiteBalanceSettings mDisplayWhiteBalanceSettings;
     @Nullable
@@ -468,21 +405,39 @@
 
     private final BrightnessRangeController mBrightnessRangeController;
 
-    @Nullable
-    private final HighBrightnessModeMetadata mHighBrightnessModeMetadata;
-
     private final BrightnessThrottler mBrightnessThrottler;
 
-    private final BrightnessSetting mBrightnessSetting;
+    private final BrightnessClamperController mBrightnessClamperController;
 
     private final Runnable mOnBrightnessChangeRunnable;
 
     private final BrightnessEvent mLastBrightnessEvent;
     private final BrightnessEvent mTempBrightnessEvent;
 
+    private final DisplayBrightnessController mDisplayBrightnessController;
+
     // Keeps a record of brightness changes for dumpsys.
     private RingBuffer<BrightnessEvent> mBrightnessEventRingBuffer;
 
+    // Keeps a record of rbc changes for dumpsys.
+    private final RingBuffer<BrightnessEvent> mRbcEventRingBuffer =
+            new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_RBC_MAX);
+
+    // Controls and tracks all the wakelocks that are acquired/released by the system. Also acts as
+    // a medium of communication between this class and the PowerManagerService.
+    private final WakelockController mWakelockController;
+
+    // Tracks and manages the proximity state of the associated display.
+    private final DisplayPowerProximityStateController mDisplayPowerProximityStateController;
+
+    // Tracks and manages the display state of the associated display.
+    private final DisplayStateController mDisplayStateController;
+
+
+    // Responsible for evaluating and tracking the automatic brightness relevant states.
+    // Todo: This is a temporary workaround. Ideally DPC2 should never talk to the strategies
+    private final AutomaticBrightnessStrategy mAutomaticBrightnessStrategy;
+
     // A record of state for skipping brightness ramps.
     private int mSkipRampState = RAMP_STATE_SKIP_NONE;
 
@@ -500,81 +455,18 @@
     private Sensor mLightSensor;
     private Sensor mScreenOffBrightnessSensor;
 
-    // The current brightness configuration.
-    @Nullable
-    private BrightnessConfiguration mBrightnessConfiguration;
-
-    // The last brightness that was set by the user and not temporary. Set to
-    // PowerManager.BRIGHTNESS_INVALID_FLOAT when a brightness has yet to be recorded.
-    private float mLastUserSetScreenBrightness = Float.NaN;
-
-    // The screen brightness setting has changed but not taken effect yet. If this is different
-    // from the current screen brightness setting then this is coming from something other than us
-    // and should be considered a user interaction.
-    private float mPendingScreenBrightnessSetting;
-
-    // The last observed screen brightness setting, either set by us or by the settings app on
-    // behalf of the user.
-    private float mCurrentScreenBrightnessSetting;
-
-    // The temporary screen brightness. Typically set when a user is interacting with the
-    // brightness slider but hasn't settled on a choice yet. Set to
-    // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary brightness set.
-    private float mTemporaryScreenBrightness;
-
-    // This brightness value is set in concurrent displays mode. It is the brightness value
-    // of the lead display that this DPC should follow.
-    private float mBrightnessToFollow;
-
-    // Indicates whether we should ramp slowly to the brightness value to follow.
-    private boolean mBrightnessToFollowSlowChange;
-
-    // The last auto brightness adjustment that was set by the user and not temporary. Set to
-    // Float.NaN when an auto-brightness adjustment hasn't been recorded yet.
-    private float mAutoBrightnessAdjustment;
-
-    // The pending auto brightness adjustment that will take effect on the next power state update.
-    private float mPendingAutoBrightnessAdjustment;
-
-    // The temporary auto brightness adjustment. Typically set when a user is interacting with the
-    // adjustment slider but hasn't settled on a choice yet. Set to
-    // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary adjustment set.
-    private float mTemporaryAutoBrightnessAdjustment;
-
-    private boolean mUseAutoBrightness;
-
     private boolean mIsRbcActive;
 
-    // Whether there's a callback to tell listeners the display has changed scheduled to run. When
-    // true it implies a wakelock is being held to guarantee the update happens before we collapse
-    // into suspend and so needs to be cleaned up if the thread is exiting.
-    // Should only be accessed on the Handler thread.
-    private boolean mOnStateChangedPending;
-
-    // Count of proximity messages currently on this DPC's Handler. Used to keep track of how many
-    // suspend blocker acquisitions are pending when shutting down this DPC.
-    // Should only be accessed on the Handler thread.
-    private int mOnProximityPositiveMessages;
-    private int mOnProximityNegativeMessages;
-
     // Animators.
     private ObjectAnimator mColorFadeOnAnimator;
     private ObjectAnimator mColorFadeOffAnimator;
     private DualRampAnimator<DisplayPowerState> mScreenBrightnessRampAnimator;
-    private BrightnessSetting.BrightnessSettingListener mBrightnessSettingListener;
 
-    // True if this DisplayPowerController has been stopped and should no longer be running.
+    // True if this DisplayPowerController2 has been stopped and should no longer be running.
     private boolean mStopped;
 
     private DisplayDeviceConfig mDisplayDeviceConfig;
 
-    // Identifiers for suspend blocker acquisition requests
-    private final String mSuspendBlockerIdUnfinishedBusiness;
-    private final String mSuspendBlockerIdOnStateChanged;
-    private final String mSuspendBlockerIdProxPositive;
-    private final String mSuspendBlockerIdProxNegative;
-    private final String mSuspendBlockerIdProxDebounce;
-
     private boolean mIsEnabled;
     private boolean mIsInTransition;
     private boolean mIsDisplayInternal;
@@ -585,13 +477,13 @@
     // DPCs following the brightness of this DPC. This is used in concurrent displays mode - there
     // is one lead display, the additional displays follow the brightness value of the lead display.
     @GuardedBy("mLock")
-    private final SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers =
-            new SparseArray<>();
+    private SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers =
+            new SparseArray();
 
     private boolean mBootCompleted;
     private final DisplayManagerFlags mFlags;
-    private int mDozeStateOverride = Display.STATE_UNKNOWN;
-    private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
+
+    private DisplayOffloadSession mDisplayOffloadSession;
 
     /**
      * Creates the display power controller.
@@ -607,26 +499,28 @@
         mClock = mInjector.getClock();
         mLogicalDisplay = logicalDisplay;
         mDisplayId = mLogicalDisplay.getDisplayIdLocked();
-        mTag = TAG + "[" + mDisplayId + "]";
-        mHighBrightnessModeMetadata = hbmMetadata;
-        mSuspendBlockerIdUnfinishedBusiness = getSuspendBlockerUnfinishedBusinessId(mDisplayId);
-        mSuspendBlockerIdOnStateChanged = getSuspendBlockerOnStateChangedId(mDisplayId);
-        mSuspendBlockerIdProxPositive = getSuspendBlockerProxPositiveId(mDisplayId);
-        mSuspendBlockerIdProxNegative = getSuspendBlockerProxNegativeId(mDisplayId);
-        mSuspendBlockerIdProxDebounce = getSuspendBlockerProxDebounceId(mDisplayId);
-
-        mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
-        mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
-        mDisplayStatsId = mUniqueDisplayId.hashCode();
+        mSensorManager = sensorManager;
+        mHandler = new DisplayControllerHandler(handler.getLooper());
+        mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked()
+                .getDisplayDeviceConfig();
         mIsEnabled = logicalDisplay.isEnabledLocked();
         mIsInTransition = logicalDisplay.isInTransitionLocked();
         mIsDisplayInternal = logicalDisplay.getPrimaryDisplayDeviceLocked()
                 .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL;
-        mHandler = new DisplayControllerHandler(handler.getLooper());
-        mLastBrightnessEvent = new BrightnessEvent(mDisplayId);
-        mTempBrightnessEvent = new BrightnessEvent(mDisplayId);
+        mWakelockController = mInjector.getWakelockController(mDisplayId, callbacks);
+        mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(
+                mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
+                () -> updatePowerState(), mDisplayId, mSensorManager);
+        mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);
+        mTag = TAG + "[" + mDisplayId + "]";
         mThermalBrightnessThrottlingDataId =
                 logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
+        mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
+        mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
+        mDisplayStatsId = mUniqueDisplayId.hashCode();
+
+        mLastBrightnessEvent = new BrightnessEvent(mDisplayId);
+        mTempBrightnessEvent = new BrightnessEvent(mDisplayId);
 
         if (mDisplayId == Display.DEFAULT_DISPLAY) {
             mBatteryStats = BatteryStatsService.getService();
@@ -635,14 +529,10 @@
         }
 
         mSettingsObserver = new SettingsObserver(mHandler);
-        mCallbacks = callbacks;
-        mSensorManager = sensorManager;
         mWindowManagerPolicy = LocalServices.getService(WindowManagerPolicy.class);
         mBlanker = blanker;
         mContext = context;
         mBrightnessTracker = brightnessTracker;
-        // TODO: b/186428377 update brightness setting when display changes
-        mBrightnessSetting = brightnessSetting;
         mOnBrightnessChangeRunnable = onBrightnessChangeRunnable;
 
         PowerManager pm = context.getSystemService(PowerManager.class);
@@ -650,30 +540,12 @@
         final Resources resources = context.getResources();
 
         // DOZE AND DIM SETTINGS
-        mScreenBrightnessDozeConfig = clampAbsoluteBrightness(
+        mScreenBrightnessDozeConfig = BrightnessUtils.clampAbsoluteBrightness(
                 pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE));
-        mScreenBrightnessDimConfig = clampAbsoluteBrightness(
-                pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM));
-        mScreenBrightnessMinimumDimAmount = resources.getFloat(
-                com.android.internal.R.dimen.config_screenBrightnessMinimumDimAmountFloat);
-
-
-        // NORMAL SCREEN SETTINGS
-        mScreenBrightnessDefault = clampAbsoluteBrightness(
-                mLogicalDisplay.getDisplayInfoLocked().brightnessDefault);
-
-        mAllowAutoBrightnessWhileDozingConfig = resources.getBoolean(
-                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing);
-
-        mPersistBrightnessNitsForDefaultDisplay = resources.getBoolean(
-                com.android.internal.R.bool.config_persistBrightnessNitsForDefaultDisplay);
-
-        mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceConfig();
-
         loadBrightnessRampRates();
         mSkipScreenOnBrightnessRamp = resources.getBoolean(
-                com.android.internal.R.bool.config_skipScreenOnBrightnessRamp);
+                R.bool.config_skipScreenOnBrightnessRamp);
+
         Runnable modeChangeCallback = () -> {
             sendUpdatePowerState();
             postBrightnessChangeRunnable();
@@ -683,23 +555,38 @@
             }
         };
 
-        HighBrightnessModeController hbmController = createHbmControllerLocked(modeChangeCallback);
+        HighBrightnessModeController hbmController = createHbmControllerLocked(hbmMetadata,
+                modeChangeCallback);
+        mBrightnessThrottler = createBrightnessThrottlerLocked();
 
-        mBrightnessRangeController = new BrightnessRangeController(hbmController,
+        mBrightnessRangeController = mInjector.getBrightnessRangeController(hbmController,
                 modeChangeCallback, mDisplayDeviceConfig, mHandler, flags,
                 mDisplayDevice.getDisplayTokenLocked(),
                 mDisplayDevice.getDisplayDeviceInfoLocked());
 
-        mBrightnessThrottler = createBrightnessThrottlerLocked();
+        mDisplayBrightnessController =
+                new DisplayBrightnessController(context, null,
+                        mDisplayId, mLogicalDisplay.getDisplayInfoLocked().brightnessDefault,
+                        brightnessSetting, () -> postBrightnessChangeRunnable(),
+                        new HandlerExecutor(mHandler), flags);
 
+        mBrightnessClamperController = mInjector.getBrightnessClamperController(
+                mHandler, modeChangeCallback::run,
+                new BrightnessClamperController.DisplayDeviceData(
+                        mUniqueDisplayId,
+                        mThermalBrightnessThrottlingDataId,
+                        logicalDisplay.getPowerThrottlingDataIdLocked(),
+                        mDisplayDeviceConfig), mContext, flags);
         // Seed the cached brightness
         saveBrightnessInfo(getScreenBrightnessSetting());
+        mAutomaticBrightnessStrategy =
+                mDisplayBrightnessController.getAutomaticBrightnessStrategy();
 
         DisplayWhiteBalanceSettings displayWhiteBalanceSettings = null;
         DisplayWhiteBalanceController displayWhiteBalanceController = null;
         if (mDisplayId == Display.DEFAULT_DISPLAY) {
             try {
-                displayWhiteBalanceController = injector.getDisplayWhiteBalanceController(
+                displayWhiteBalanceController = mInjector.getDisplayWhiteBalanceController(
                         mHandler, mSensorManager, resources);
                 displayWhiteBalanceSettings = new DisplayWhiteBalanceSettings(mContext, mHandler);
                 displayWhiteBalanceSettings.setCallbacks(this);
@@ -715,21 +602,24 @@
 
         if (mDisplayId == Display.DEFAULT_DISPLAY) {
             mCdsi = LocalServices.getService(ColorDisplayServiceInternal.class);
-            boolean active = mCdsi.setReduceBrightColorsListener(new ReduceBrightColorsListener() {
-                @Override
-                public void onReduceBrightColorsActivationChanged(boolean activated,
-                        boolean userInitiated) {
-                    applyReduceBrightColorsSplineAdjustment();
+            if (mCdsi != null) {
+                boolean active = mCdsi.setReduceBrightColorsListener(
+                        new ReduceBrightColorsListener() {
+                            @Override
+                            public void onReduceBrightColorsActivationChanged(boolean activated,
+                                    boolean userInitiated) {
+                                applyReduceBrightColorsSplineAdjustment();
 
-                }
+                            }
 
-                @Override
-                public void onReduceBrightColorsStrengthChanged(int strength) {
+                            @Override
+                            public void onReduceBrightColorsStrengthChanged(int strength) {
+                                applyReduceBrightColorsSplineAdjustment();
+                            }
+                        });
+                if (active) {
                     applyReduceBrightColorsSplineAdjustment();
                 }
-            });
-            if (active) {
-                applyReduceBrightColorsSplineAdjustment();
             }
         } else {
             mCdsi = null;
@@ -737,27 +627,17 @@
 
         setUpAutoBrightness(context, handler);
 
-        mColorFadeEnabled = !ActivityManager.isLowRamDeviceStatic()
+        mColorFadeEnabled = mInjector.isColorFadeEnabled()
                 && !resources.getBoolean(
                   com.android.internal.R.bool.config_displayColorFadeDisabled);
         mColorFadeFadesConfig = resources.getBoolean(
-                com.android.internal.R.bool.config_animateScreenLights);
+                R.bool.config_animateScreenLights);
 
         mDisplayBlanksAfterDozeConfig = resources.getBoolean(
-                com.android.internal.R.bool.config_displayBlanksAfterDoze);
+                R.bool.config_displayBlanksAfterDoze);
 
         mBrightnessBucketsInDozeConfig = resources.getBoolean(
-                com.android.internal.R.bool.config_displayBrightnessBucketsInDoze);
-
-        loadProximitySensor();
-
-        loadNitBasedBrightnessSetting();
-        mBrightnessToFollow = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        mAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting();
-        mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        mTemporaryAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        mPendingAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+                R.bool.config_displayBrightnessBucketsInDoze);
 
         mBootCompleted = bootCompleted;
     }
@@ -785,7 +665,7 @@
      */
     @Override
     public boolean isProximitySensorAvailable() {
-        return mProximitySensor != null;
+        return mDisplayPowerProximityStateController.isProximitySensorAvailable();
     }
 
     /**
@@ -818,64 +698,6 @@
         }
     }
 
-    @Override
-    public int getDisplayId() {
-        return mDisplayId;
-    }
-
-    @Override
-    public int getLeadDisplayId() {
-        return mLeadDisplayId;
-    }
-
-    @Override
-    public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
-            boolean slowChange) {
-        mBrightnessRangeController.onAmbientLuxChange(ambientLux);
-        if (nits == BrightnessMappingStrategy.INVALID_NITS) {
-            mBrightnessToFollow = leadDisplayBrightness;
-        } else {
-            float brightness = getBrightnessFromNits(nits);
-            if (isValidBrightnessValue(brightness)) {
-                mBrightnessToFollow = brightness;
-            } else {
-                // The device does not support nits
-                mBrightnessToFollow = leadDisplayBrightness;
-            }
-        }
-        mBrightnessToFollowSlowChange = slowChange;
-        sendUpdatePowerState();
-    }
-
-    @Override
-    public void addDisplayBrightnessFollower(@NonNull DisplayPowerControllerInterface follower) {
-        synchronized (mLock) {
-            mDisplayBrightnessFollowers.append(follower.getDisplayId(), follower);
-            sendUpdatePowerStateLocked();
-        }
-    }
-
-    @Override
-    public void removeDisplayBrightnessFollower(@NonNull DisplayPowerControllerInterface follower) {
-        synchronized (mLock) {
-            mDisplayBrightnessFollowers.remove(follower.getDisplayId());
-            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
-                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
-                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void clearDisplayBrightnessFollowersLocked() {
-        for (int i = 0; i < mDisplayBrightnessFollowers.size(); i++) {
-            DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i);
-            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
-                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
-                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
-        }
-        mDisplayBrightnessFollowers.clear();
-    }
-
     @Nullable
     @Override
     public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats(
@@ -923,13 +745,8 @@
                 return true;
             }
 
-            boolean changed = false;
-
-            if (waitForNegativeProximity
-                    && !mPendingWaitForNegativeProximityLocked) {
-                mPendingWaitForNegativeProximityLocked = true;
-                changed = true;
-            }
+            boolean changed = mDisplayPowerProximityStateController
+                    .setPendingWaitForNegativeProximityLocked(waitForNegativeProximity);
 
             if (mPendingRequestLocked == null) {
                 mPendingRequestLocked = new DisplayPowerRequest(request);
@@ -953,18 +770,23 @@
 
     @Override
     public void overrideDozeScreenState(int displayState) {
-        synchronized (mLock) {
-            if (mDisplayOffloadSession == null ||
-                    !DisplayOffloadSession.isSupportedOffloadState(displayState)) {
+        mHandler.postAtTime(() -> {
+            if (mDisplayOffloadSession == null
+                    || !(DisplayOffloadSession.isSupportedOffloadState(displayState)
+                    || displayState == Display.STATE_UNKNOWN)) {
                 return;
             }
-            mDozeStateOverride = displayState;
+            mDisplayStateController.overrideDozeScreenState(displayState);
             sendUpdatePowerState();
-        }
+        }, mClock.uptimeMillis());
     }
 
     @Override
     public void setDisplayOffloadSession(DisplayOffloadSession session) {
+        if (session == mDisplayOffloadSession) {
+            return;
+        }
+        unblockScreenOnByDisplayOffload();
         mDisplayOffloadSession = session;
     }
 
@@ -981,14 +803,14 @@
      * when displays get swapped on foldable devices.  For example, different brightness properties
      * of each display need to be properly reflected in AutomaticBrightnessController.
      *
-     * Make sure DisplayManagerService.mSyncRoot is held when this is called
+     * Make sure DisplayManagerService.mSyncRoot lock is held when this is called
      */
     @Override
     public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata, int leadDisplayId) {
         mLeadDisplayId = leadDisplayId;
         final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
         if (device == null) {
-            Slog.wtf(mTag, "Display Device is null in DisplayPowerController for display: "
+            Slog.wtf(mTag, "Display Device is null in DisplayPowerController2 for display: "
                     + mLogicalDisplay.getDisplayIdLocked());
             return;
         }
@@ -1004,6 +826,9 @@
                 .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL;
         final String thermalBrightnessThrottlingDataId =
                 mLogicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
+        final String powerThrottlingDataId =
+                mLogicalDisplay.getPowerThrottlingDataIdLocked();
+
         mHandler.postAtTime(() -> {
             boolean changed = false;
             if (mDisplayDevice != device) {
@@ -1014,9 +839,9 @@
                 mDisplayDeviceConfig = config;
                 mThermalBrightnessThrottlingDataId = thermalBrightnessThrottlingDataId;
                 loadFromDisplayDeviceConfig(token, info, hbmMetadata);
-                loadNitBasedBrightnessSetting();
+                mDisplayPowerProximityStateController.notifyDisplayDeviceChanged(config);
 
-                /// Since the underlying display-device changed, we really don't know the
+                // Since the underlying display-device changed, we really don't know the
                 // last command that was sent to change it's state. Let's assume it is unknown so
                 // that we trigger a change immediately.
                 mPowerState.resetScreenState();
@@ -1034,7 +859,16 @@
                 mIsEnabled = isEnabled;
                 mIsInTransition = isInTransition;
             }
+
             mIsDisplayInternal = isDisplayInternal;
+            // using local variables here, when mBrightnessThrottler is removed,
+            // mThermalBrightnessThrottlingDataId could be removed as well
+            // changed = true will be not needed - clampers are maintaining their state and
+            // will call updatePowerState if needed.
+            mBrightnessClamperController.onDisplayChanged(
+                    new BrightnessClamperController.DisplayDeviceData(uniqueId,
+                        thermalBrightnessThrottlingDataId, powerThrottlingDataId, config));
+
             if (changed) {
                 updatePowerState();
             }
@@ -1044,7 +878,7 @@
     /**
      * Unregisters all listeners and interrupts all running threads; halting future work.
      *
-     * This method should be called when the DisplayPowerController is no longer in use; i.e. when
+     * This method should be called when the DisplayPowerController2 is no longer in use; i.e. when
      * the {@link #mDisplayId display} has been removed.
      */
     @Override
@@ -1060,9 +894,7 @@
                 mAutomaticBrightnessController.stop();
             }
 
-            if (mBrightnessSetting != null) {
-                mBrightnessSetting.unregisterListener(mBrightnessSettingListener);
-            }
+            mDisplayBrightnessController.stop();
 
             mContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
         }
@@ -1073,11 +905,11 @@
         // All properties that depend on the associated DisplayDevice and the DDC must be
         // updated here.
         loadBrightnessRampRates();
-        loadProximitySensor();
         loadNitsRange(mContext.getResources());
         setUpAutoBrightness(mContext, mHandler);
         reloadReduceBrightColours();
         setAnimatorRampSpeeds(/* isIdleMode= */ false);
+
         mBrightnessRangeController.loadFromConfig(hbmMetadata, token, info, mDisplayDeviceConfig);
         mBrightnessThrottler.loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(
                 mDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId(),
@@ -1126,22 +958,30 @@
         noteScreenBrightness(mPowerState.getScreenBrightness());
 
         // Initialize all of the brightness tracking state
-        final float brightness = convertToAdjustedNits(mPowerState.getScreenBrightness());
+        final float brightness = mDisplayBrightnessController.convertToAdjustedNits(
+                mPowerState.getScreenBrightness());
         if (mBrightnessTracker != null && brightness >= PowerManager.BRIGHTNESS_MIN) {
             mBrightnessTracker.start(brightness);
         }
-        mBrightnessSettingListener = brightnessValue -> {
+
+        BrightnessSetting.BrightnessSettingListener brightnessSettingListener = brightnessValue -> {
             Message msg = mHandler.obtainMessage(MSG_UPDATE_BRIGHTNESS, brightnessValue);
             mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
         };
+        mDisplayBrightnessController
+                .registerBrightnessSettingChangeListener(brightnessSettingListener);
 
-        mBrightnessSetting.registerListener(mBrightnessSettingListener);
         mContext.getContentResolver().registerContentObserver(
                 Settings.System.getUriFor(Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ),
                 false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
         mContext.getContentResolver().registerContentObserver(
                 Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE),
                 false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
+        if (mFlags.areAutoBrightnessModesEnabled()) {
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_FOR_ALS),
+                    /* notifyForDescendants= */ false, mSettingsObserver, UserHandle.USER_CURRENT);
+        }
         handleBrightnessModeChange();
     }
 
@@ -1165,12 +1005,21 @@
         if (isIdleScreenBrightnessEnabled) {
             BrightnessMappingStrategy idleModeBrightnessMapper =
                     BrightnessMappingStrategy.create(context, mDisplayDeviceConfig,
-                            AUTO_BRIGHTNESS_MODE_IDLE, mDisplayWhiteBalanceController);
+                            AUTO_BRIGHTNESS_MODE_IDLE,
+                            mDisplayWhiteBalanceController);
             if (idleModeBrightnessMapper != null) {
-                brightnessMappers.append(AUTO_BRIGHTNESS_MODE_IDLE, idleModeBrightnessMapper);
+                brightnessMappers.append(AUTO_BRIGHTNESS_MODE_IDLE,
+                        idleModeBrightnessMapper);
             }
         }
 
+        BrightnessMappingStrategy dozeModeBrightnessMapper =
+                BrightnessMappingStrategy.create(context, mDisplayDeviceConfig,
+                        AUTO_BRIGHTNESS_MODE_DOZE, mDisplayWhiteBalanceController);
+        if (mFlags.areAutoBrightnessModesEnabled() && dozeModeBrightnessMapper != null) {
+            brightnessMappers.put(AUTO_BRIGHTNESS_MODE_DOZE, dozeModeBrightnessMapper);
+        }
+
         float userLux = BrightnessMappingStrategy.INVALID_LUX;
         float userNits = BrightnessMappingStrategy.INVALID_NITS;
         if (mAutomaticBrightnessController != null) {
@@ -1180,7 +1029,7 @@
 
         if (defaultModeBrightnessMapper != null) {
             final float dozeScaleFactor = context.getResources().getFraction(
-                    com.android.internal.R.fraction.config_screenAutoBrightnessDozeScaleFactor,
+                    R.fraction.config_screenAutoBrightnessDozeScaleFactor,
                     1, 1);
 
             // Ambient Lux - Active Mode Brightness Thresholds
@@ -1264,14 +1113,14 @@
             long darkeningLightDebounceIdle = mDisplayDeviceConfig
                     .getAutoBrightnessDarkeningLightDebounceIdle();
             boolean autoBrightnessResetAmbientLuxAfterWarmUp = context.getResources().getBoolean(
-                    com.android.internal.R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
+                    R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
 
             int lightSensorWarmUpTimeConfig = context.getResources().getInteger(
-                    com.android.internal.R.integer.config_lightSensorWarmupTime);
+                    R.integer.config_lightSensorWarmupTime);
             int lightSensorRate = context.getResources().getInteger(
-                    com.android.internal.R.integer.config_autoBrightnessLightSensorRate);
+                    R.integer.config_autoBrightnessLightSensorRate);
             int initialLightSensorRate = context.getResources().getInteger(
-                    com.android.internal.R.integer.config_autoBrightnessInitialLightSensorRate);
+                    R.integer.config_autoBrightnessInitialLightSensorRate);
             if (initialLightSensorRate == -1) {
                 initialLightSensorRate = lightSensorRate;
             } else if (initialLightSensorRate > lightSensorRate) {
@@ -1301,7 +1150,11 @@
                     screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
                     mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
                     mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits);
+            mDisplayBrightnessController.setAutomaticBrightnessController(
+                    mAutomaticBrightnessController);
 
+            mAutomaticBrightnessStrategy
+                    .setAutomaticBrightnessController(mAutomaticBrightnessController);
             mBrightnessEventRingBuffer =
                     new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX);
 
@@ -1351,7 +1204,7 @@
         } else {
             Slog.w(mTag, "Screen brightness nits configuration is unavailable; falling back");
             mNitsRange = BrightnessMappingStrategy.getFloatArray(resources
-                    .obtainTypedArray(com.android.internal.R.array.config_screenBrightnessNits));
+                    .obtainTypedArray(R.array.config_screenBrightnessNits));
         }
     }
 
@@ -1369,7 +1222,6 @@
             mAutomaticBrightnessController.switchMode(mode);
             setAnimatorRampSpeeds(isIdle);
         }
-
         Message msg = mHandler.obtainMessage();
         msg.what = MSG_SET_DWBC_STRONG_MODE;
         msg.arg1 = isIdle ? 1 : 0;
@@ -1421,28 +1273,14 @@
 
     /** Clean up all resources that are accessed via the {@link #mHandler} thread. */
     private void cleanupHandlerThreadAfterStop() {
-        setProximitySensorEnabled(false);
+        mDisplayPowerProximityStateController.cleanup();
         mBrightnessRangeController.stop();
         mBrightnessThrottler.stop();
+        mBrightnessClamperController.stop();
         mHandler.removeCallbacksAndMessages(null);
 
         // Release any outstanding wakelocks we're still holding because of pending messages.
-        if (mUnfinishedBusiness) {
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness);
-            mUnfinishedBusiness = false;
-        }
-        if (mOnStateChangedPending) {
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged);
-            mOnStateChangedPending = false;
-        }
-        for (int i = 0; i < mOnProximityPositiveMessages; i++) {
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive);
-        }
-        mOnProximityPositiveMessages = 0;
-        for (int i = 0; i < mOnProximityNegativeMessages; i++) {
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative);
-        }
-        mOnProximityNegativeMessages = 0;
+        mWakelockController.releaseAll();
 
         final float brightness = mPowerState != null
                 ? mPowerState.getScreenBrightness()
@@ -1457,10 +1295,10 @@
         if (mScreenOffBrightnessSensorController != null) {
             mScreenOffBrightnessSensorController.stop();
         }
+
         if (mDisplayWhiteBalanceController != null) {
             mDisplayWhiteBalanceController.setEnabled(false);
         }
-
     }
 
     // Call from handler thread
@@ -1476,7 +1314,6 @@
         final boolean mustNotify;
         final int previousPolicy;
         boolean mustInitialize = false;
-        int brightnessAdjustmentFlags = 0;
         mBrightnessReasonTemp.set(null);
         mTempBrightnessEvent.reset();
         SparseArray<DisplayPowerControllerInterface> displayBrightnessFollowers;
@@ -1491,7 +1328,7 @@
 
             if (mPowerRequest == null) {
                 mPowerRequest = new DisplayPowerRequest(mPendingRequestLocked);
-                updatePendingProximityRequestsLocked();
+                mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
                 mPendingRequestChangedLocked = false;
                 mustInitialize = true;
                 // Assume we're on and bright until told otherwise, since that's the state we turn
@@ -1500,7 +1337,7 @@
             } else if (mPendingRequestChangedLocked) {
                 previousPolicy = mPowerRequest.policy;
                 mPowerRequest.copyFrom(mPendingRequestLocked);
-                updatePendingProximityRequestsLocked();
+                mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
                 mPendingRequestChangedLocked = false;
                 mDisplayReadyLocked = false;
             } else {
@@ -1512,105 +1349,8 @@
             displayBrightnessFollowers = mDisplayBrightnessFollowers.clone();
         }
 
-        // Compute the basic display state using the policy.
-        // We might override this below based on other factors.
-        // Initialise brightness as invalid.
-        int state;
-        float brightnessState = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        boolean performScreenOffTransition = false;
-        switch (mPowerRequest.policy) {
-            case DisplayPowerRequest.POLICY_OFF:
-                state = Display.STATE_OFF;
-                performScreenOffTransition = true;
-                break;
-            case DisplayPowerRequest.POLICY_DOZE:
-                if (mDozeStateOverride != Display.STATE_UNKNOWN) {
-                    state = mDozeStateOverride;
-                } else if (mPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
-                    state = mPowerRequest.dozeScreenState;
-                } else {
-                    state = Display.STATE_DOZE;
-                }
-                if (!mAllowAutoBrightnessWhileDozingConfig) {
-                    brightnessState = mPowerRequest.dozeScreenBrightness;
-                    mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE);
-                }
-                break;
-            case DisplayPowerRequest.POLICY_DIM:
-            case DisplayPowerRequest.POLICY_BRIGHT:
-            default:
-                state = Display.STATE_ON;
-                break;
-        }
-        assert (state != Display.STATE_UNKNOWN);
-
-        if (mScreenOffBrightnessSensorController != null) {
-            mScreenOffBrightnessSensorController.setLightSensorEnabled(mUseAutoBrightness
-                    && mIsEnabled && (state == Display.STATE_OFF || (state == Display.STATE_DOZE
-                    && !mAllowAutoBrightnessWhileDozingConfig))
-                    && mLeadDisplayId == Layout.NO_LEAD_DISPLAY);
-        }
-
-        boolean skipRampBecauseOfProximityChangeToNegative = false;
-        // Apply the proximity sensor.
-        if (mProximitySensor != null) {
-            if (mPowerRequest.useProximitySensor && state != Display.STATE_OFF) {
-                // At this point the policy says that the screen should be on, but we've been
-                // asked to listen to the prox sensor to adjust the display state, so lets make
-                // sure the sensor is on.
-                setProximitySensorEnabled(true);
-                if (!mScreenOffBecauseOfProximity
-                        && mProximity == PROXIMITY_POSITIVE
-                        && !mIgnoreProximityUntilChanged) {
-                    // Prox sensor already reporting "near" so we should turn off the screen.
-                    // Also checked that we aren't currently set to ignore the proximity sensor
-                    // temporarily.
-                    mScreenOffBecauseOfProximity = true;
-                    sendOnProximityPositiveWithWakelock();
-                }
-            } else if (mWaitingForNegativeProximity
-                    && mScreenOffBecauseOfProximity
-                    && mProximity == PROXIMITY_POSITIVE
-                    && state != Display.STATE_OFF) {
-                // The policy says that we should have the screen on, but it's off due to the prox
-                // and we've been asked to wait until the screen is far from the user to turn it
-                // back on. Let keep the prox sensor on so we can tell when it's far again.
-                setProximitySensorEnabled(true);
-            } else {
-                // We haven't been asked to use the prox sensor and we're not waiting on the screen
-                // to turn back on...so lets shut down the prox sensor.
-                setProximitySensorEnabled(false);
-                mWaitingForNegativeProximity = false;
-            }
-
-            if (mScreenOffBecauseOfProximity
-                    && (mProximity != PROXIMITY_POSITIVE || mIgnoreProximityUntilChanged)) {
-                // The screen *was* off due to prox being near, but now it's "far" so lets turn
-                // the screen back on.  Also turn it back on if we've been asked to ignore the
-                // prox sensor temporarily.
-                mScreenOffBecauseOfProximity = false;
-                skipRampBecauseOfProximityChangeToNegative = true;
-                sendOnProximityNegativeWithWakelock();
-            }
-        } else {
-            setProximitySensorEnabled(false);
-            mWaitingForNegativeProximity = false;
-            mIgnoreProximityUntilChanged = false;
-
-            if (mScreenOffBecauseOfProximity) {
-                // The screen *was* off due to prox being near, but now there's no prox sensor, so
-                // let's turn the screen back on.
-                mScreenOffBecauseOfProximity = false;
-                skipRampBecauseOfProximityChangeToNegative = true;
-                sendOnProximityNegativeWithWakelock();
-            }
-        }
-
-        if (!mIsEnabled
-                || mIsInTransition
-                || mScreenOffBecauseOfProximity) {
-            state = Display.STATE_OFF;
-        }
+        int state = mDisplayStateController
+                .updateDisplayState(mPowerRequest, mIsEnabled, mIsInTransition);
 
         // Initialize things the first time the power state is changed.
         if (mustInitialize) {
@@ -1620,157 +1360,100 @@
         // Animate the screen state change unless already animating.
         // The transition may be deferred, so after this point we will use the
         // actual state instead of the desired one.
-        final int oldState = mPowerState.getScreenState();
-        animateScreenStateChange(state, performScreenOffTransition);
+        animateScreenStateChange(state, mDisplayStateController.shouldPerformScreenOffTransition());
         state = mPowerState.getScreenState();
-        boolean slowChange = false;
 
-        if (state == Display.STATE_OFF) {
-            brightnessState = PowerManager.BRIGHTNESS_OFF_FLOAT;
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_SCREEN_OFF);
+        // Switch to doze auto-brightness mode if needed
+        if (mFlags.areAutoBrightnessModesEnabled() && mAutomaticBrightnessController != null
+                && !mAutomaticBrightnessController.isInIdleMode()) {
+            setAutomaticScreenBrightnessMode(Display.isDozeState(state)
+                    ? AUTO_BRIGHTNESS_MODE_DOZE : AUTO_BRIGHTNESS_MODE_DEFAULT);
         }
 
-        if (Float.isNaN(brightnessState) && isValidBrightnessValue(mBrightnessToFollow)) {
-            brightnessState = mBrightnessToFollow;
-            slowChange = mBrightnessToFollowSlowChange;
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_FOLLOWER);
+        final boolean userSetBrightnessChanged = mDisplayBrightnessController
+                .updateUserSetScreenBrightness();
+
+        DisplayBrightnessState displayBrightnessState = mDisplayBrightnessController
+                .updateBrightness(mPowerRequest, state);
+        float brightnessState = displayBrightnessState.getBrightness();
+        float rawBrightnessState = displayBrightnessState.getBrightness();
+        mBrightnessReasonTemp.set(displayBrightnessState.getBrightnessReason());
+        boolean slowChange = displayBrightnessState.isSlowChange();
+        // custom transition duration
+        float customAnimationRate = displayBrightnessState.getCustomAnimationRate();
+
+        // Set up the ScreenOff controller used when coming out of SCREEN_OFF and the ALS sensor
+        // doesn't yet have a valid lux value to use with auto-brightness.
+        if (mScreenOffBrightnessSensorController != null) {
+            mScreenOffBrightnessSensorController
+                    .setLightSensorEnabled(displayBrightnessState.getShouldUseAutoBrightness()
+                    && mIsEnabled && (state == Display.STATE_OFF
+                    || (state == Display.STATE_DOZE
+                    && !mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()))
+                    && mLeadDisplayId == Layout.NO_LEAD_DISPLAY);
         }
 
-        if ((Float.isNaN(brightnessState))
-                && isValidBrightnessValue(mPowerRequest.screenBrightnessOverride)) {
-            brightnessState = mPowerRequest.screenBrightnessOverride;
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_OVERRIDE);
-            mAppliedScreenBrightnessOverride = true;
-        } else {
-            mAppliedScreenBrightnessOverride = false;
-        }
-
-        final boolean autoBrightnessEnabledInDoze =
-                mAllowAutoBrightnessWhileDozingConfig && Display.isDozeState(state);
-        final boolean autoBrightnessEnabled = mUseAutoBrightness
-                && (state == Display.STATE_ON || autoBrightnessEnabledInDoze)
-                && mBrightnessReasonTemp.getReason() != BrightnessReason.REASON_OVERRIDE
-                && mAutomaticBrightnessController != null;
-        final boolean autoBrightnessDisabledDueToDisplayOff = mUseAutoBrightness
-                && !(state == Display.STATE_ON || autoBrightnessEnabledInDoze);
-        final int autoBrightnessState = autoBrightnessEnabled
-                    && mBrightnessReasonTemp.getReason() != BrightnessReason.REASON_FOLLOWER
-                ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
-                : autoBrightnessDisabledDueToDisplayOff
-                        ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE
-                        : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED;
-
-        final boolean userSetBrightnessChanged = updateUserSetScreenBrightness();
-
-        // Use the temporary screen brightness if there isn't an override, either from
-        // WindowManager or based on the display state.
-        if (isValidBrightnessValue(mTemporaryScreenBrightness)) {
-            brightnessState = mTemporaryScreenBrightness;
-            mAppliedTemporaryBrightness = true;
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_TEMPORARY);
-        } else {
-            mAppliedTemporaryBrightness = false;
-        }
-
-        final boolean autoBrightnessAdjustmentChanged = updateAutoBrightnessAdjustment();
-
-        // Use the autobrightness adjustment override if set.
-        final float autoBrightnessAdjustment;
-        if (!Float.isNaN(mTemporaryAutoBrightnessAdjustment)) {
-            autoBrightnessAdjustment = mTemporaryAutoBrightnessAdjustment;
-            brightnessAdjustmentFlags = BrightnessReason.ADJUSTMENT_AUTO_TEMP;
-            mAppliedTemporaryAutoBrightnessAdjustment = true;
-        } else {
-            autoBrightnessAdjustment = mAutoBrightnessAdjustment;
-            brightnessAdjustmentFlags = BrightnessReason.ADJUSTMENT_AUTO;
-            mAppliedTemporaryAutoBrightnessAdjustment = false;
-        }
-        // Apply brightness boost.
-        // We do this here after deciding whether auto-brightness is enabled so that we don't
-        // disable the light sensor during this temporary state.  That way when boost ends we will
-        // be able to resume normal auto-brightness behavior without any delay.
-        if (mPowerRequest.boostScreenBrightness
-                && brightnessState != PowerManager.BRIGHTNESS_OFF_FLOAT) {
-            brightnessState = PowerManager.BRIGHTNESS_MAX;
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_BOOST);
-            mAppliedBrightnessBoost = true;
-        } else {
-            mAppliedBrightnessBoost = false;
-        }
+        // Take note if the short term model was already active before applying the current
+        // request changes.
+        final boolean wasShortTermModelActive =
+                mAutomaticBrightnessStrategy.isShortTermModelActive();
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(state,
+                mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig(),
+                mBrightnessReasonTemp.getReason(), mPowerRequest.policy,
+                mDisplayBrightnessController.getLastUserSetScreenBrightness(),
+                userSetBrightnessChanged);
 
         // If the brightness is already set then it's been overridden by something other than the
         // user, or is a temporary adjustment.
         boolean userInitiatedChange = (Float.isNaN(brightnessState))
-                && (autoBrightnessAdjustmentChanged || userSetBrightnessChanged);
-        boolean wasShortTermModelActive = false;
-        // Configure auto-brightness.
-        if (mAutomaticBrightnessController != null) {
-            wasShortTermModelActive = mAutomaticBrightnessController.hasUserDataPoints();
-            mAutomaticBrightnessController.configure(autoBrightnessState,
-                    mBrightnessConfiguration,
-                    mLastUserSetScreenBrightness,
-                    userSetBrightnessChanged, autoBrightnessAdjustment,
-                    autoBrightnessAdjustmentChanged, mPowerRequest.policy,
-                    mShouldResetShortTermModel);
-            mShouldResetShortTermModel = false;
-        }
-        mBrightnessRangeController.setAutoBrightnessEnabled(autoBrightnessEnabled
+                && (mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged()
+                || userSetBrightnessChanged);
+
+        mBrightnessRangeController.setAutoBrightnessEnabled(
+                mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
-                : autoBrightnessDisabledDueToDisplayOff
+                : mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff()
                         ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE
                         : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
 
-        if (mBrightnessTracker != null) {
-            mBrightnessTracker.setShouldCollectColorSample(mBrightnessConfiguration != null
-                    && mBrightnessConfiguration.shouldCollectColorSamples());
-        }
-
-        boolean updateScreenBrightnessSetting = false;
-        float rawBrightnessState = brightnessState;
-
+        boolean updateScreenBrightnessSetting =
+                displayBrightnessState.shouldUpdateScreenBrightnessSetting();
+        float currentBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness();
         // Apply auto-brightness.
+        int brightnessAdjustmentFlags = 0;
         if (Float.isNaN(brightnessState)) {
-            float newAutoBrightnessAdjustment = autoBrightnessAdjustment;
-            if (autoBrightnessEnabled) {
-                rawBrightnessState = mAutomaticBrightnessController
-                        .getRawAutomaticScreenBrightness();
-                brightnessState = mAutomaticBrightnessController.getAutomaticScreenBrightness(
+            if (mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()) {
+                brightnessState = mAutomaticBrightnessStrategy.getAutomaticScreenBrightness(
                         mTempBrightnessEvent);
-                newAutoBrightnessAdjustment =
-                        mAutomaticBrightnessController.getAutomaticScreenBrightnessAdjustment();
-            }
-            if (isValidBrightnessValue(brightnessState)
-                    || brightnessState == PowerManager.BRIGHTNESS_OFF_FLOAT) {
-                // Use current auto-brightness value and slowly adjust to changes.
-                brightnessState = clampScreenBrightness(brightnessState);
-                if (mAppliedAutoBrightness && !autoBrightnessAdjustmentChanged) {
-                    slowChange = true; // slowly adapt to auto-brightness
+                if (BrightnessUtils.isValidBrightnessValue(brightnessState)
+                        || brightnessState == PowerManager.BRIGHTNESS_OFF_FLOAT) {
+                    rawBrightnessState = mAutomaticBrightnessController
+                            .getRawAutomaticScreenBrightness();
+                    brightnessState = clampScreenBrightness(brightnessState);
+                    // slowly adapt to auto-brightness
+                    // TODO(b/253226419): slowChange should be decided by strategy.updateBrightness
+                    slowChange = mAutomaticBrightnessStrategy.hasAppliedAutoBrightness()
+                            && !mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged();
+                    brightnessAdjustmentFlags =
+                            mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags();
+                    updateScreenBrightnessSetting = currentBrightnessSetting != brightnessState;
+                    mAutomaticBrightnessStrategy.setAutoBrightnessApplied(true);
+                    mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC);
+                    if (mScreenOffBrightnessSensorController != null) {
+                        mScreenOffBrightnessSensorController.setLightSensorEnabled(false);
+                    }
+                } else {
+                    mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
                 }
-                updateScreenBrightnessSetting = mCurrentScreenBrightnessSetting != brightnessState;
-                mAppliedAutoBrightness = true;
-                mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC);
-                if (mScreenOffBrightnessSensorController != null) {
-                    mScreenOffBrightnessSensorController.setLightSensorEnabled(false);
-                }
-            } else {
-                mAppliedAutoBrightness = false;
-            }
-            if (autoBrightnessAdjustment != newAutoBrightnessAdjustment) {
-                // If the autobrightness controller has decided to change the adjustment value
-                // used, make sure that's reflected in settings.
-                putAutoBrightnessAdjustmentSetting(newAutoBrightnessAdjustment);
-            } else {
-                // Adjustment values resulted in no change
-                brightnessAdjustmentFlags = 0;
             }
         } else {
             // Any non-auto-brightness values such as override or temporary should still be subject
             // to clamping so that they don't go beyond the current max as specified by HBM
             // Controller.
             brightnessState = clampScreenBrightness(brightnessState);
-            mAppliedAutoBrightness = false;
-            brightnessAdjustmentFlags = 0;
+            mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
         }
+
         // Use default brightness when dozing unless overridden.
         if ((Float.isNaN(brightnessState))
                 && Display.isDozeState(state)) {
@@ -1781,14 +1464,15 @@
 
         // The ALS is not available yet - use the screen off sensor to determine the initial
         // brightness
-        if (Float.isNaN(brightnessState) && autoBrightnessEnabled
+        if (Float.isNaN(brightnessState) && mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()
                 && mScreenOffBrightnessSensorController != null) {
             rawBrightnessState =
                     mScreenOffBrightnessSensorController.getAutomaticScreenBrightness();
             brightnessState = rawBrightnessState;
-            if (isValidBrightnessValue(brightnessState)) {
+            if (BrightnessUtils.isValidBrightnessValue(brightnessState)) {
                 brightnessState = clampScreenBrightness(brightnessState);
-                updateScreenBrightnessSetting = mCurrentScreenBrightnessSetting != brightnessState;
+                updateScreenBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness()
+                        != brightnessState;
                 mBrightnessReasonTemp.setReason(
                         BrightnessReason.REASON_SCREEN_OFF_BRIGHTNESS_SENSOR);
             }
@@ -1796,9 +1480,9 @@
 
         // Apply manual brightness.
         if (Float.isNaN(brightnessState)) {
-            rawBrightnessState = mCurrentScreenBrightnessSetting;
+            rawBrightnessState = currentBrightnessSetting;
             brightnessState = clampScreenBrightness(rawBrightnessState);
-            if (brightnessState != mCurrentScreenBrightnessSetting) {
+            if (brightnessState != currentBrightnessSetting) {
                 // The manually chosen screen brightness is outside of the currently allowed
                 // range (i.e., high-brightness-mode), make sure we tell the rest of the system
                 // by updating the setting.
@@ -1811,7 +1495,8 @@
                 : mAutomaticBrightnessController.getAmbientLux();
         for (int i = 0; i < displayBrightnessFollowers.size(); i++) {
             DisplayPowerControllerInterface follower = displayBrightnessFollowers.valueAt(i);
-            follower.setBrightnessToFollow(rawBrightnessState, convertToNits(rawBrightnessState),
+            follower.setBrightnessToFollow(rawBrightnessState,
+                    mDisplayBrightnessController.convertToNits(rawBrightnessState),
                     ambientLux, slowChange);
         }
 
@@ -1823,64 +1508,25 @@
         // Note throttling effectively changes the allowed brightness range, so, similarly to HBM,
         // we broadcast this change through setting.
         final float unthrottledBrightnessState = brightnessState;
-        if (mBrightnessThrottler.isThrottled()) {
-            mTempBrightnessEvent.setThermalMax(mBrightnessThrottler.getBrightnessCap());
-            brightnessState = Math.min(brightnessState, mBrightnessThrottler.getBrightnessCap());
-            mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_THROTTLED);
-            if (!mAppliedThrottling) {
-                // Brightness throttling is needed, so do so quickly.
-                // Later, when throttling is removed, we let other mechanisms decide on speed.
-                slowChange = false;
-            }
-            mAppliedThrottling = true;
-        } else if (mAppliedThrottling) {
-            mAppliedThrottling = false;
-        }
+        DisplayBrightnessState clampedState = mBrightnessClamperController.clamp(mPowerRequest,
+                brightnessState, slowChange);
+
+        brightnessState = clampedState.getBrightness();
+        slowChange = clampedState.isSlowChange();
+        // faster rate wins, at this point customAnimationRate == -1, strategy does not control
+        // customAnimationRate. Should be revisited if strategy start setting this value
+        customAnimationRate = Math.max(customAnimationRate, clampedState.getCustomAnimationRate());
+        mBrightnessReasonTemp.addModifier(clampedState.getBrightnessReason().getModifier());
 
         if (updateScreenBrightnessSetting) {
             // Tell the rest of the system about the new brightness in case we had to change it
             // for things like auto-brightness or high-brightness-mode. Note that we do this
-            // before applying the low power or dim transformations so that the slider
-            // accurately represents the full possible range, even if they range changes what
-            // it means in absolute terms.
-            updateScreenBrightnessSetting(brightnessState);
-        }
-
-        // Apply dimming by at least some minimum amount when user activity
-        // timeout is about to expire.
-        if (mPowerRequest.policy == DisplayPowerRequest.POLICY_DIM) {
-            if (brightnessState > PowerManager.BRIGHTNESS_MIN) {
-                brightnessState = Math.max(
-                        Math.min(brightnessState - mScreenBrightnessMinimumDimAmount,
-                                mScreenBrightnessDimConfig),
-                        PowerManager.BRIGHTNESS_MIN);
-                mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_DIMMED);
-            }
-            if (!mAppliedDimming) {
-                slowChange = false;
-            }
-            mAppliedDimming = true;
-        } else if (mAppliedDimming) {
-            slowChange = false;
-            mAppliedDimming = false;
-        }
-        // If low power mode is enabled, scale brightness by screenLowPowerBrightnessFactor
-        // as long as it is above the minimum threshold.
-        if (mPowerRequest.lowPowerMode) {
-            if (brightnessState > PowerManager.BRIGHTNESS_MIN) {
-                final float brightnessFactor =
-                        Math.min(mPowerRequest.screenLowPowerBrightnessFactor, 1);
-                final float lowPowerBrightnessFloat = (brightnessState * brightnessFactor);
-                brightnessState = Math.max(lowPowerBrightnessFloat, PowerManager.BRIGHTNESS_MIN);
-                mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_LOW_POWER);
-            }
-            if (!mAppliedLowPower) {
-                slowChange = false;
-            }
-            mAppliedLowPower = true;
-        } else if (mAppliedLowPower) {
-            slowChange = false;
-            mAppliedLowPower = false;
+            // only considering maxBrightness (ignoring brightness modifiers like low power or dim)
+            // so that the slider accurately represents the full possible range,
+            // even if they range changes what it means in absolute terms.
+            mDisplayBrightnessController.updateScreenBrightnessSetting(
+                    MathUtils.constrain(unthrottledBrightnessState,
+                            clampedState.getMinBrightness(), clampedState.getMaxBrightness()));
         }
 
         // The current brightness to use has been calculated at this point, and HbmController should
@@ -1889,13 +1535,15 @@
         // brightness sources (such as an app override) are not saved to the setting, but should be
         // reflected in HBM calculations.
         mBrightnessRangeController.onBrightnessChanged(brightnessState, unthrottledBrightnessState,
-                mBrightnessThrottler.getBrightnessMaxReason());
+                mBrightnessClamperController.getBrightnessMaxReason());
 
         // Animate the screen brightness when the screen is on or dozing.
-        // Skip the animation when the screen is off.
+        // Skip the animation when the screen is off or suspended.
         boolean brightnessAdjusted = false;
         final boolean brightnessIsTemporary =
-                mAppliedTemporaryBrightness || mAppliedTemporaryAutoBrightnessAdjustment;
+                (mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_TEMPORARY)
+                        || mAutomaticBrightnessStrategy
+                        .isTemporaryAutoBrightnessAdjustmentApplied();
         if (!mPendingScreenOff) {
             if (mSkipScreenOnBrightnessRamp) {
                 if (state == Display.STATE_ON) {
@@ -1916,7 +1564,8 @@
             }
 
             final boolean initialRampSkip = (state == Display.STATE_ON && mSkipRampState
-                    != RAMP_STATE_SKIP_NONE) || skipRampBecauseOfProximityChangeToNegative;
+                    != RAMP_STATE_SKIP_NONE) || mDisplayPowerProximityStateController
+                    .shouldSkipRampBecauseOfProximityChangeToNegative();
             // While dozing, sometimes the brightness is split into buckets. Rather than animating
             // through the buckets, which is unlikely to be smooth in the first place, just jump
             // right to the suggested brightness.
@@ -1950,13 +1599,25 @@
                 // We want to scale HDR brightness level with the SDR level, we also need to restore
                 // SDR brightness immediately when entering dim or low power mode.
                 animateValue = mBrightnessRangeController.getHdrBrightnessValue();
+                customAnimationRate = Math.max(customAnimationRate,
+                        mBrightnessRangeController.getHdrTransitionRate());
                 mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_HDR);
             }
 
+            // if doze or suspend state is requested, we want to finish brightnes animation fast
+            // to allow state animation to start
+            if (mPowerRequest.policy == DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE
+                    && (mPowerRequest.dozeScreenState == Display.STATE_UNKNOWN  // dozing
+                    || mPowerRequest.dozeScreenState == Display.STATE_DOZE_SUSPEND
+                    || mPowerRequest.dozeScreenState == Display.STATE_ON_SUSPEND)) {
+                customAnimationRate = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET;
+                slowChange = false;
+            }
+
             final float currentBrightness = mPowerState.getScreenBrightness();
             final float currentSdrBrightness = mPowerState.getSdrScreenBrightness();
 
-            if (isValidBrightnessValue(animateValue)
+            if (BrightnessUtils.isValidBrightnessValue(animateValue)
                     && (animateValue != currentBrightness
                     || sdrAnimateValue != currentSdrBrightness)) {
                 boolean skipAnimation = initialRampSkip || hasBrightnessBuckets
@@ -1986,6 +1647,9 @@
                 if (skipAnimation) {
                     animateScreenBrightness(animateValue, sdrAnimateValue,
                             SCREEN_ANIMATION_RATE_MINIMUM);
+                } else if (customAnimationRate > 0) {
+                    animateScreenBrightness(animateValue, sdrAnimateValue,
+                            customAnimationRate, /* ignoreAnimationLimits = */true);
                 } else {
                     boolean isIncreasing = animateValue > currentBrightness;
                     final float rampSpeed;
@@ -2007,13 +1671,15 @@
             }
 
             notifyBrightnessTrackerChanged(brightnessState, userInitiatedChange,
-                    wasShortTermModelActive, autoBrightnessEnabled, brightnessIsTemporary);
+                    wasShortTermModelActive, mAutomaticBrightnessStrategy.isAutoBrightnessEnabled(),
+                    brightnessIsTemporary, displayBrightnessState.getShouldUseAutoBrightness());
 
             // We save the brightness info *after* the brightness setting has been changed and
             // adjustments made so that the brightness info reflects the latest value.
-            brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting(), animateValue);
+            brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting(),
+                    animateValue, clampedState);
         } else {
-            brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting());
+            brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting(), clampedState);
         }
 
         // Only notify if the brightness adjustment is not temporary (i.e. slider has been released)
@@ -2049,13 +1715,20 @@
                 ? mCdsi.getReduceBrightColorsStrength() : -1);
         mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor);
         mTempBrightnessEvent.setWasShortTermModelActive(wasShortTermModelActive);
-        mTempBrightnessEvent.setAutomaticBrightnessEnabled(mUseAutoBrightness);
+        mTempBrightnessEvent.setDisplayBrightnessStrategyName(displayBrightnessState
+                .getDisplayBrightnessStrategyName());
+        mTempBrightnessEvent.setAutomaticBrightnessEnabled(
+                displayBrightnessState.getShouldUseAutoBrightness());
         // Temporary is what we use during slider interactions. We avoid logging those so that
         // we don't spam logcat when the slider is being used.
         boolean tempToTempTransition =
                 mTempBrightnessEvent.getReason().getReason() == BrightnessReason.REASON_TEMPORARY
                         && mLastBrightnessEvent.getReason().getReason()
                         == BrightnessReason.REASON_TEMPORARY;
+        // Purely for dumpsys;
+        final boolean isRbcEvent =
+                mLastBrightnessEvent.isRbcEnabled() != mTempBrightnessEvent.isRbcEnabled();
+
         if ((!mTempBrightnessEvent.equalsMainData(mLastBrightnessEvent) && !tempToTempTransition)
                 || brightnessAdjustmentFlags != 0) {
             mTempBrightnessEvent.setInitialBrightness(mLastBrightnessEvent.getBrightness());
@@ -2075,6 +1748,10 @@
             if (mBrightnessEventRingBuffer != null) {
                 mBrightnessEventRingBuffer.append(newEvent);
             }
+            if (isRbcEvent) {
+                mRbcEventRingBuffer.append(newEvent);
+            }
+
         }
 
         // Update display white-balance.
@@ -2092,6 +1769,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);
@@ -2106,12 +1784,8 @@
         }
 
         // Grab a wake lock if we have unfinished business.
-        if (!finished && !mUnfinishedBusiness) {
-            if (DEBUG) {
-                Slog.d(mTag, "Unfinished business...");
-            }
-            mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness);
-            mUnfinishedBusiness = true;
+        if (!finished) {
+            mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
         }
 
         // Notify the power manager when ready.
@@ -2130,12 +1804,8 @@
         }
 
         // Release the wake lock when we have no unfinished business.
-        if (finished && mUnfinishedBusiness) {
-            if (DEBUG) {
-                Slog.d(mTag, "Finished business...");
-            }
-            mUnfinishedBusiness = false;
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness);
+        if (finished) {
+            mWakelockController.releaseWakelock(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
         }
 
         // Record if dozing for future comparison.
@@ -2166,9 +1836,9 @@
 
     private void setDwbcLoggingEnabled(int arg) {
         if (mDisplayWhiteBalanceController != null) {
-            final boolean shouldEnable = (arg == 1);
-            mDisplayWhiteBalanceController.setLoggingEnabled(shouldEnable);
-            mDisplayWhiteBalanceSettings.setLoggingEnabled(shouldEnable);
+            final boolean enabled = (arg == 1);
+            mDisplayWhiteBalanceController.setLoggingEnabled(enabled);
+            mDisplayWhiteBalanceSettings.setLoggingEnabled(enabled);
         }
     }
 
@@ -2183,7 +1853,7 @@
      */
     @Override
     public void ignoreProximitySensorUntilChanged() {
-        mHandler.sendEmptyMessage(MSG_IGNORE_PROXIMITY);
+        mDisplayPowerProximityStateController.ignoreProximitySensorUntilChanged();
     }
 
     @Override
@@ -2210,21 +1880,27 @@
 
     @Override
     public void setBrightnessFromOffload(float brightness) {
-        // The old DPC is no longer supported
+        Message msg = mHandler.obtainMessage(MSG_SET_BRIGHTNESS_FROM_OFFLOAD,
+                Float.floatToIntBits(brightness), 0 /*unused*/);
+        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
     }
 
     @Override
     public float[] getAutoBrightnessLevels(
             @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
-        // The old DPC is no longer supported
-        return null;
+        int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
+                Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL, UserHandle.USER_CURRENT);
+        return mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(mode, preset);
     }
 
     @Override
     public float[] getAutoBrightnessLuxLevels(
             @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
-        // The old DPC is no longer supported
-        return null;
+        int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
+                Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL, UserHandle.USER_CURRENT);
+        return mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(mode, preset);
     }
 
     @Override
@@ -2241,18 +1917,29 @@
         }
     }
 
-    private boolean saveBrightnessInfo(float brightness) {
-        return saveBrightnessInfo(brightness, brightness);
+    @Override
+    public void onBootCompleted() {
+        Message msg = mHandler.obtainMessage(MSG_BOOT_COMPLETED);
+        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
     }
 
-    private boolean saveBrightnessInfo(float brightness, float adjustedBrightness) {
+    private boolean saveBrightnessInfo(float brightness) {
+        return saveBrightnessInfo(brightness, /* state= */ null);
+    }
+
+    private boolean saveBrightnessInfo(float brightness, @Nullable DisplayBrightnessState state) {
+        return saveBrightnessInfo(brightness, brightness, state);
+    }
+
+    private boolean saveBrightnessInfo(float brightness, float adjustedBrightness,
+            @Nullable DisplayBrightnessState state) {
         synchronized (mCachedBrightnessInfo) {
-            final float minBrightness = Math.min(
-                    mBrightnessRangeController.getCurrentBrightnessMin(),
-                    mBrightnessThrottler.getBrightnessCap());
+            float stateMax = state != null ? state.getMaxBrightness() : PowerManager.BRIGHTNESS_MAX;
+            float stateMin = state != null ? state.getMinBrightness() : PowerManager.BRIGHTNESS_MAX;
+            final float minBrightness = Math.max(stateMin, Math.min(
+                    mBrightnessRangeController.getCurrentBrightnessMin(), stateMax));
             final float maxBrightness = Math.min(
-                    mBrightnessRangeController.getCurrentBrightnessMax(),
-                    mBrightnessThrottler.getBrightnessCap());
+                    mBrightnessRangeController.getCurrentBrightnessMax(), stateMax);
             boolean changed = false;
 
             changed |=
@@ -2275,8 +1962,7 @@
                             mBrightnessRangeController.getTransitionPoint());
             changed |=
                     mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason,
-                            mBrightnessThrottler.getBrightnessMaxReason());
-
+                            mBrightnessClamperController.getBrightnessMaxReason());
             return changed;
         }
     }
@@ -2288,27 +1974,18 @@
     }
 
     private HighBrightnessModeController createHbmControllerLocked(
-            Runnable modeChangeCallback) {
-        final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
-        final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig();
-        final IBinder displayToken =
-                mLogicalDisplay.getPrimaryDisplayDeviceLocked().getDisplayTokenLocked();
-        final String displayUniqueId =
-                mLogicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
+            HighBrightnessModeMetadata hbmMetadata, Runnable modeChangeCallback) {
+        final DisplayDeviceConfig ddConfig = mDisplayDevice.getDisplayDeviceConfig();
+        final IBinder displayToken = mDisplayDevice.getDisplayTokenLocked();
+        final String displayUniqueId = mDisplayDevice.getUniqueId();
         final DisplayDeviceConfig.HighBrightnessModeData hbmData =
                 ddConfig != null ? ddConfig.getHighBrightnessModeData() : null;
-        final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
+        final DisplayDeviceInfo info = mDisplayDevice.getDisplayDeviceInfoLocked();
         return mInjector.getHighBrightnessModeController(mHandler, info.width, info.height,
                 displayToken, displayUniqueId, PowerManager.BRIGHTNESS_MIN,
-                PowerManager.BRIGHTNESS_MAX, hbmData,
-                new HighBrightnessModeController.HdrBrightnessDeviceConfig() {
-                    @Override
-                    public float getHdrBrightnessFromSdr(
-                            float sdrBrightness, float maxDesiredHdrSdrRatio) {
-                        return mDisplayDeviceConfig.getHdrBrightnessFromSdr(
-                                sdrBrightness, maxDesiredHdrSdrRatio);
-                    }
-                }, modeChangeCallback, mHighBrightnessModeMetadata, mContext);
+                PowerManager.BRIGHTNESS_MAX, hbmData, (sdrBrightness, maxDesiredHdrSdrRatio) ->
+                        mDisplayDeviceConfig.getHdrBrightnessFromSdr(sdrBrightness,
+                                maxDesiredHdrSdrRatio), modeChangeCallback, hbmMetadata, mContext);
     }
 
     private BrightnessThrottler createBrightnessThrottlerLocked() {
@@ -2359,18 +2036,72 @@
         }
     }
 
+    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 && !mScreenOffBecauseOfProximity) {
+            if (isOff && !mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
                 if (mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_ON
                         || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) {
                     setReportedScreenState(REPORTED_TO_POLICY_SCREEN_TURNING_OFF);
@@ -2383,8 +2114,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";
@@ -2410,7 +2142,7 @@
         // it is only removed once the window manager tells us that the activity has
         // finished drawing underneath.
         if (isOff && mReportedScreenStateToPolicy != REPORTED_TO_POLICY_SCREEN_OFF
-                && !mScreenOffBecauseOfProximity) {
+                && !mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
             setReportedScreenState(REPORTED_TO_POLICY_SCREEN_OFF);
             unblockScreenOn();
             mWindowManagerPolicy.screenTurnedOff(mDisplayId, mIsInTransition);
@@ -2436,12 +2168,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() {
@@ -2456,18 +2192,6 @@
                 mDisplayDeviceConfig.getScreenOffBrightnessSensor(), SensorUtils.NO_FALLBACK);
     }
 
-    private void loadProximitySensor() {
-        if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT || mDisplayId != Display.DEFAULT_DISPLAY) {
-            return;
-        }
-        mProximitySensor = SensorUtils.findSensor(mSensorManager,
-                mDisplayDeviceConfig.getProximitySensor(), Sensor.TYPE_PROXIMITY);
-        if (mProximitySensor != null) {
-            mProximityThreshold = Math.min(mProximitySensor.getMaximumRange(),
-                    TYPICAL_PROXIMITY_THRESHOLD);
-        }
-    }
-
     private float clampScreenBrightness(float value) {
         if (Float.isNaN(value)) {
             value = PowerManager.BRIGHTNESS_MIN;
@@ -2476,18 +2200,18 @@
                 mBrightnessRangeController.getCurrentBrightnessMax());
     }
 
-    // Checks whether the brightness is within the valid brightness range, not including off.
-    private boolean isValidBrightnessValue(float brightness) {
-        return brightness >= PowerManager.BRIGHTNESS_MIN
-                && brightness <= PowerManager.BRIGHTNESS_MAX;
+    private void animateScreenBrightness(float target, float sdrTarget, float rate) {
+        animateScreenBrightness(target, sdrTarget, rate, /* ignoreAnimationLimits = */false);
     }
 
-    private void animateScreenBrightness(float target, float sdrTarget, float rate) {
+    private void animateScreenBrightness(float target, float sdrTarget, float rate,
+            boolean ignoreAnimationLimits) {
         if (DEBUG) {
             Slog.d(mTag, "Animating brightness: target=" + target + ", sdrTarget=" + sdrTarget
                     + ", rate=" + rate);
         }
-        if (mScreenBrightnessRampAnimator.animateTo(target, sdrTarget, rate, false)) {
+        if (mScreenBrightnessRampAnimator.animateTo(target, sdrTarget, rate,
+                ignoreAnimationLimits)) {
             Trace.traceCounter(Trace.TRACE_TAG_POWER, "TargetScreenBrightness", (int) target);
 
             String propertyKey = "debug.tracing.screen_brightness";
@@ -2655,102 +2379,11 @@
 
     private final Runnable mCleanListener = this::sendUpdatePowerState;
 
-    private void setProximitySensorEnabled(boolean enable) {
-        if (enable) {
-            if (!mProximitySensorEnabled) {
-                // Register the listener.
-                // Proximity sensor state already cleared initially.
-                mProximitySensorEnabled = true;
-                mIgnoreProximityUntilChanged = false;
-                mSensorManager.registerListener(mProximitySensorListener, mProximitySensor,
-                        SensorManager.SENSOR_DELAY_NORMAL, mHandler);
-            }
-        } else {
-            if (mProximitySensorEnabled) {
-                // Unregister the listener.
-                // Clear the proximity sensor state for next time.
-                mProximitySensorEnabled = false;
-                mProximity = PROXIMITY_UNKNOWN;
-                mIgnoreProximityUntilChanged = false;
-                mPendingProximity = PROXIMITY_UNKNOWN;
-                mHandler.removeMessages(MSG_PROXIMITY_SENSOR_DEBOUNCED);
-                mSensorManager.unregisterListener(mProximitySensorListener);
-                clearPendingProximityDebounceTime(); // release wake lock (must be last)
-            }
-        }
-    }
-
-    private void handleProximitySensorEvent(long time, boolean positive) {
-        if (mProximitySensorEnabled) {
-            if (mPendingProximity == PROXIMITY_NEGATIVE && !positive) {
-                return; // no change
-            }
-            if (mPendingProximity == PROXIMITY_POSITIVE && positive) {
-                return; // no change
-            }
-
-            // Only accept a proximity sensor reading if it remains
-            // stable for the entire debounce delay.  We hold a wake lock while
-            // debouncing the sensor.
-            mHandler.removeMessages(MSG_PROXIMITY_SENSOR_DEBOUNCED);
-            if (positive) {
-                mPendingProximity = PROXIMITY_POSITIVE;
-                setPendingProximityDebounceTime(
-                        time + PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY); // acquire wake lock
-            } else {
-                mPendingProximity = PROXIMITY_NEGATIVE;
-                setPendingProximityDebounceTime(
-                        time + PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY); // acquire wake lock
-            }
-
-            // Debounce the new sensor reading.
-            debounceProximitySensor();
-        }
-    }
-
-    private void debounceProximitySensor() {
-        if (mProximitySensorEnabled
-                && mPendingProximity != PROXIMITY_UNKNOWN
-                && mPendingProximityDebounceTime >= 0) {
-            final long now = mClock.uptimeMillis();
-            if (mPendingProximityDebounceTime <= now) {
-                if (mProximity != mPendingProximity) {
-                    // if the status of the sensor changed, stop ignoring.
-                    mIgnoreProximityUntilChanged = false;
-                    Slog.i(mTag, "No longer ignoring proximity [" + mPendingProximity + "]");
-                }
-                // Sensor reading accepted.  Apply the change then release the wake lock.
-                mProximity = mPendingProximity;
-                updatePowerState();
-                clearPendingProximityDebounceTime(); // release wake lock (must be last)
-            } else {
-                // Need to wait a little longer.
-                // Debounce again later.  We continue holding a wake lock while waiting.
-                Message msg = mHandler.obtainMessage(MSG_PROXIMITY_SENSOR_DEBOUNCED);
-                mHandler.sendMessageAtTime(msg, mPendingProximityDebounceTime);
-            }
-        }
-    }
-
-    private void clearPendingProximityDebounceTime() {
-        if (mPendingProximityDebounceTime >= 0) {
-            mPendingProximityDebounceTime = -1;
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxDebounce);
-        }
-    }
-
-    private void setPendingProximityDebounceTime(long debounceTime) {
-        if (mPendingProximityDebounceTime < 0) {
-            mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxDebounce);
-        }
-        mPendingProximityDebounceTime = debounceTime;
-    }
-
     private void sendOnStateChangedWithWakelock() {
-        if (!mOnStateChangedPending) {
-            mOnStateChangedPending = true;
-            mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdOnStateChanged);
-            mHandler.post(mOnStateChangedRunnable);
+        boolean wakeLockAcquired = mWakelockController.acquireWakelock(
+                WakelockController.WAKE_LOCK_STATE_CHANGED);
+        if (wakeLockAcquired) {
+            mHandler.post(mWakelockController.getOnStateChangedRunnable());
         }
     }
 
@@ -2762,12 +2395,15 @@
     }
 
     private void handleSettingsChange(boolean userSwitch) {
-        mPendingScreenBrightnessSetting = getScreenBrightnessSetting();
-        mPendingAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting();
+        mDisplayBrightnessController
+                .setPendingScreenBrightness(mDisplayBrightnessController
+                        .getScreenBrightnessSetting());
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(userSwitch);
         if (userSwitch) {
             // Don't treat user switches as user initiated change.
-            setCurrentScreenBrightness(mPendingScreenBrightnessSetting);
-            updateAutoBrightnessAdjustment();
+            mDisplayBrightnessController
+                    .setAndNotifyCurrentScreenBrightness(mDisplayBrightnessController
+                            .getPendingScreenBrightness());
             if (mAutomaticBrightnessController != null) {
                 mAutomaticBrightnessController.resetShortTermModel();
             }
@@ -2781,129 +2417,59 @@
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT);
         mHandler.postAtTime(() -> {
-            mUseAutoBrightness = screenBrightnessModeSetting
-                    == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC;
+            mAutomaticBrightnessStrategy.setUseAutoBrightness(screenBrightnessModeSetting
+                    == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
             updatePowerState();
         }, mClock.uptimeMillis());
     }
 
-    private float getAutoBrightnessAdjustmentSetting() {
-        final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(),
-                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT);
-        return Float.isNaN(adj) ? 0.0f : clampAutoBrightnessAdjustment(adj);
-    }
 
     @Override
     public float getScreenBrightnessSetting() {
-        float brightness = mBrightnessSetting.getBrightness();
-        if (Float.isNaN(brightness)) {
-            brightness = mScreenBrightnessDefault;
-        }
-        return clampAbsoluteBrightness(brightness);
-    }
-
-    private void loadNitBasedBrightnessSetting() {
-        if (mDisplayId == Display.DEFAULT_DISPLAY && mPersistBrightnessNitsForDefaultDisplay) {
-            float brightnessNitsForDefaultDisplay =
-                    mBrightnessSetting.getBrightnessNitsForDefaultDisplay();
-            if (brightnessNitsForDefaultDisplay >= 0) {
-                float brightnessForDefaultDisplay = getBrightnessFromNits(
-                        brightnessNitsForDefaultDisplay);
-                if (isValidBrightnessValue(brightnessForDefaultDisplay)) {
-                    mBrightnessSetting.setBrightness(brightnessForDefaultDisplay);
-                    mCurrentScreenBrightnessSetting = brightnessForDefaultDisplay;
-                    return;
-                }
-            }
-        }
-        mCurrentScreenBrightnessSetting = getScreenBrightnessSetting();
+        return mDisplayBrightnessController.getScreenBrightnessSetting();
     }
 
     @Override
     public void setBrightness(float brightnessValue, int userSerial) {
-        // Update the setting, which will eventually call back into DPC to have us actually update
-        // the display with the new value.
-        float clampedBrightnessValue = clampScreenBrightness(brightnessValue);
-        mBrightnessSetting.setUserSerial(userSerial);
-        mBrightnessSetting.setBrightness(clampedBrightnessValue);
-        if (mDisplayId == Display.DEFAULT_DISPLAY && mPersistBrightnessNitsForDefaultDisplay) {
-            float nits = convertToNits(clampedBrightnessValue);
-            if (nits >= 0) {
-                mBrightnessSetting.setBrightnessNitsForDefaultDisplay(nits);
-            }
-        }
+        mDisplayBrightnessController.setBrightness(clampScreenBrightness(brightnessValue),
+                userSerial);
     }
 
     @Override
-    public void onBootCompleted() {
-        Message msg = mHandler.obtainMessage(MSG_BOOT_COMPLETED);
-        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
+    public int getDisplayId() {
+        return mDisplayId;
     }
 
-    private void updateScreenBrightnessSetting(float brightnessValue) {
-        if (!isValidBrightnessValue(brightnessValue)
-                || brightnessValue == mCurrentScreenBrightnessSetting) {
-            return;
-        }
-        setCurrentScreenBrightness(brightnessValue);
-        setBrightness(brightnessValue);
+    @Override
+    public int getLeadDisplayId() {
+        return mLeadDisplayId;
     }
 
-    private void setCurrentScreenBrightness(float brightnessValue) {
-        if (brightnessValue != mCurrentScreenBrightnessSetting) {
-            mCurrentScreenBrightnessSetting = brightnessValue;
-            postBrightnessChangeRunnable();
+    @Override
+    public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
+            boolean slowChange) {
+        mBrightnessRangeController.onAmbientLuxChange(ambientLux);
+        if (nits == BrightnessMappingStrategy.INVALID_NITS) {
+            mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness, slowChange);
+        } else {
+            float brightness = mDisplayBrightnessController.getBrightnessFromNits(nits);
+            if (BrightnessUtils.isValidBrightnessValue(brightness)) {
+                mDisplayBrightnessController.setBrightnessToFollow(brightness, slowChange);
+            } else {
+                // The device does not support nits
+                mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness,
+                        slowChange);
+            }
         }
-    }
-
-    private void putAutoBrightnessAdjustmentSetting(float adjustment) {
-        if (mDisplayId == Display.DEFAULT_DISPLAY) {
-            mAutoBrightnessAdjustment = adjustment;
-            Settings.System.putFloatForUser(mContext.getContentResolver(),
-                    Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, adjustment,
-                    UserHandle.USER_CURRENT);
-        }
-    }
-
-    private boolean updateAutoBrightnessAdjustment() {
-        if (Float.isNaN(mPendingAutoBrightnessAdjustment)) {
-            return false;
-        }
-        if (mAutoBrightnessAdjustment == mPendingAutoBrightnessAdjustment) {
-            mPendingAutoBrightnessAdjustment = Float.NaN;
-            return false;
-        }
-        mAutoBrightnessAdjustment = mPendingAutoBrightnessAdjustment;
-        mPendingAutoBrightnessAdjustment = Float.NaN;
-        mTemporaryAutoBrightnessAdjustment = Float.NaN;
-        return true;
-    }
-
-    // We want to return true if the user has set the screen brightness.
-    // RBC on, off, and intensity changes will return false.
-    // Slider interactions whilst in RBC will return true, just as when in non-rbc.
-    private boolean updateUserSetScreenBrightness() {
-        if ((Float.isNaN(mPendingScreenBrightnessSetting)
-                || mPendingScreenBrightnessSetting < 0.0f)) {
-            return false;
-        }
-        if (mCurrentScreenBrightnessSetting == mPendingScreenBrightnessSetting) {
-            mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-            mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-            return false;
-        }
-        setCurrentScreenBrightness(mPendingScreenBrightnessSetting);
-        mLastUserSetScreenBrightness = mPendingScreenBrightnessSetting;
-        mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        return true;
+        sendUpdatePowerState();
     }
 
     private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated,
             boolean wasShortTermModelActive, boolean autobrightnessEnabled,
-            boolean brightnessIsTemporary) {
-        final float brightnessInNits = convertToAdjustedNits(brightness);
+            boolean brightnessIsTemporary, boolean shouldUseAutoBrightness) {
 
+        final float brightnessInNits =
+                mDisplayBrightnessController.convertToAdjustedNits(brightness);
         // Don't report brightness to brightnessTracker:
         // If brightness is temporary (ie the slider has not been released)
         // or if we are in idle screen brightness mode.
@@ -2915,12 +2481,13 @@
                 || mAutomaticBrightnessController.isInIdleMode()
                 || !autobrightnessEnabled
                 || mBrightnessTracker == null
-                || !mUseAutoBrightness
+                || !shouldUseAutoBrightness
                 || brightnessInNits < 0.0f) {
             return;
         }
 
-        if (userInitiated && !mAutomaticBrightnessController.hasValidAmbientLux()) {
+        if (userInitiated && (mAutomaticBrightnessController == null
+                || !mAutomaticBrightnessController.hasValidAmbientLux())) {
             // If we don't have a valid lux reading we can't report a valid
             // slider event so notify as if the system changed the brightness.
             userInitiated = false;
@@ -2939,96 +2506,33 @@
                 mAutomaticBrightnessController.getLastSensorTimestamps());
     }
 
-    private float convertToNits(float brightness) {
-        if (mAutomaticBrightnessController == null) {
-            return BrightnessMappingStrategy.INVALID_NITS;
+    @Override
+    public void addDisplayBrightnessFollower(DisplayPowerControllerInterface follower) {
+        synchronized (mLock) {
+            mDisplayBrightnessFollowers.append(follower.getDisplayId(), follower);
+            sendUpdatePowerStateLocked();
         }
-        return mAutomaticBrightnessController.convertToNits(brightness);
     }
 
-    private float convertToAdjustedNits(float brightness) {
-        if (mAutomaticBrightnessController == null) {
-            return BrightnessMappingStrategy.INVALID_NITS;
+    @Override
+    public void removeDisplayBrightnessFollower(DisplayPowerControllerInterface follower) {
+        synchronized (mLock) {
+            mDisplayBrightnessFollowers.remove(follower.getDisplayId());
+            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
+                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
+                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
         }
-        return mAutomaticBrightnessController.convertToAdjustedNits(brightness);
-    }
-
-    private float getBrightnessFromNits(float nits) {
-        if (mAutomaticBrightnessController == null) {
-            return PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        }
-        return mAutomaticBrightnessController.getBrightnessFromNits(nits);
     }
 
     @GuardedBy("mLock")
-    private void updatePendingProximityRequestsLocked() {
-        mWaitingForNegativeProximity |= mPendingWaitForNegativeProximityLocked;
-        mPendingWaitForNegativeProximityLocked = false;
-
-        if (mIgnoreProximityUntilChanged) {
-            // Also, lets stop waiting for negative proximity if we're ignoring it.
-            mWaitingForNegativeProximity = false;
+    private void clearDisplayBrightnessFollowersLocked() {
+        for (int i = 0; i < mDisplayBrightnessFollowers.size(); i++) {
+            DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i);
+            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
+                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
+                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
         }
-    }
-
-    private void ignoreProximitySensorUntilChangedInternal() {
-        if (!mIgnoreProximityUntilChanged
-                && mProximity == PROXIMITY_POSITIVE) {
-            // Only ignore if it is still reporting positive (near)
-            mIgnoreProximityUntilChanged = true;
-            Slog.i(mTag, "Ignoring proximity");
-            updatePowerState();
-        }
-    }
-
-    private final Runnable mOnStateChangedRunnable = new Runnable() {
-        @Override
-        public void run() {
-            mOnStateChangedPending = false;
-            mCallbacks.onStateChanged();
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged);
-        }
-    };
-
-    private void sendOnProximityPositiveWithWakelock() {
-        mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxPositive);
-        mHandler.post(mOnProximityPositiveRunnable);
-        mOnProximityPositiveMessages++;
-    }
-
-    private final Runnable mOnProximityPositiveRunnable = new Runnable() {
-        @Override
-        public void run() {
-            mOnProximityPositiveMessages--;
-            mCallbacks.onProximityPositive();
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive);
-        }
-    };
-
-    private void sendOnProximityNegativeWithWakelock() {
-        mOnProximityNegativeMessages++;
-        mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxNegative);
-        mHandler.post(mOnProximityNegativeRunnable);
-    }
-
-    private final Runnable mOnProximityNegativeRunnable = new Runnable() {
-        @Override
-        public void run() {
-            mOnProximityNegativeMessages--;
-            mCallbacks.onProximityNegative();
-            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative);
-        }
-    };
-
-    /**
-     * Indicates whether the display state is ready to update. If this is the default display, we
-     * want to update it right away so that we can draw the boot animation on it. If it is not
-     * the default display, drawing the boot animation on it would look incorrect, so we need
-     * to wait until boot is completed.
-     * @return True if the display state is ready to update
-     */
-    private boolean readyToUpdateDisplayState() {
-        return mDisplayId == Display.DEFAULT_DISPLAY || mBootCompleted;
+        mDisplayBrightnessFollowers.clear();
     }
 
     @Override
@@ -3040,31 +2544,23 @@
             pw.println("  mLeadDisplayId=" + mLeadDisplayId);
             pw.println("  mLightSensor=" + mLightSensor);
             pw.println("  mDisplayBrightnessFollowers=" + mDisplayBrightnessFollowers);
-            pw.println("  mDozeStateOverride=" + mDozeStateOverride);
 
             pw.println();
             pw.println("Display Power Controller Locked State:");
             pw.println("  mDisplayReadyLocked=" + mDisplayReadyLocked);
             pw.println("  mPendingRequestLocked=" + mPendingRequestLocked);
             pw.println("  mPendingRequestChangedLocked=" + mPendingRequestChangedLocked);
-            pw.println("  mPendingWaitForNegativeProximityLocked="
-                    + mPendingWaitForNegativeProximityLocked);
             pw.println("  mPendingUpdatePowerStateLocked=" + mPendingUpdatePowerStateLocked);
         }
 
         pw.println();
         pw.println("Display Power Controller Configuration:");
-        pw.println("  mScreenBrightnessRangeDefault=" + mScreenBrightnessDefault);
         pw.println("  mScreenBrightnessDozeConfig=" + mScreenBrightnessDozeConfig);
-        pw.println("  mScreenBrightnessDimConfig=" + mScreenBrightnessDimConfig);
         pw.println("  mUseSoftwareAutoBrightnessConfig=" + mUseSoftwareAutoBrightnessConfig);
-        pw.println("  mAllowAutoBrightnessWhileDozingConfig="
-                + mAllowAutoBrightnessWhileDozingConfig);
-        pw.println("  mPersistBrightnessNitsForDefaultDisplay="
-                + mPersistBrightnessNitsForDefaultDisplay);
         pw.println("  mSkipScreenOnBrightnessRamp=" + mSkipScreenOnBrightnessRamp);
         pw.println("  mColorFadeFadesConfig=" + mColorFadeFadesConfig);
         pw.println("  mColorFadeEnabled=" + mColorFadeEnabled);
+        pw.println("  mIsDisplayInternal=" + mIsDisplayInternal);
         synchronized (mCachedBrightnessInfo) {
             pw.println("  mCachedBrightnessInfo.brightness="
                     + mCachedBrightnessInfo.brightness.value);
@@ -3082,7 +2578,6 @@
         }
         pw.println("  mDisplayBlanksAfterDozeConfig=" + mDisplayBlanksAfterDozeConfig);
         pw.println("  mBrightnessBucketsInDozeConfig=" + mBrightnessBucketsInDozeConfig);
-
         mHandler.runWithScissors(() -> dumpLocal(pw), 1000);
     }
 
@@ -3090,35 +2585,9 @@
         pw.println();
         pw.println("Display Power Controller Thread State:");
         pw.println("  mPowerRequest=" + mPowerRequest);
-        pw.println("  mUnfinishedBusiness=" + mUnfinishedBusiness);
-        pw.println("  mWaitingForNegativeProximity=" + mWaitingForNegativeProximity);
-        pw.println("  mProximitySensor=" + mProximitySensor);
-        pw.println("  mProximitySensorEnabled=" + mProximitySensorEnabled);
-        pw.println("  mProximityThreshold=" + mProximityThreshold);
-        pw.println("  mProximity=" + proximityToString(mProximity));
-        pw.println("  mPendingProximity=" + proximityToString(mPendingProximity));
-        pw.println("  mPendingProximityDebounceTime="
-                + TimeUtils.formatUptime(mPendingProximityDebounceTime));
-        pw.println("  mScreenOffBecauseOfProximity=" + mScreenOffBecauseOfProximity);
-        pw.println("  mLastUserSetScreenBrightness=" + mLastUserSetScreenBrightness);
-        pw.println("  mPendingScreenBrightnessSetting="
-                + mPendingScreenBrightnessSetting);
-        pw.println("  mTemporaryScreenBrightness=" + mTemporaryScreenBrightness);
-        pw.println("  mBrightnessToFollow=" + mBrightnessToFollow);
-        pw.println("  mBrightnessToFollowSlowChange=" + mBrightnessToFollowSlowChange);
-        pw.println("  mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment);
         pw.println("  mBrightnessReason=" + mBrightnessReason);
-        pw.println("  mTemporaryAutoBrightnessAdjustment=" + mTemporaryAutoBrightnessAdjustment);
-        pw.println("  mPendingAutoBrightnessAdjustment=" + mPendingAutoBrightnessAdjustment);
-        pw.println("  mAppliedAutoBrightness=" + mAppliedAutoBrightness);
         pw.println("  mAppliedDimming=" + mAppliedDimming);
-        pw.println("  mAppliedLowPower=" + mAppliedLowPower);
         pw.println("  mAppliedThrottling=" + mAppliedThrottling);
-        pw.println("  mAppliedScreenBrightnessOverride=" + mAppliedScreenBrightnessOverride);
-        pw.println("  mAppliedTemporaryBrightness=" + mAppliedTemporaryBrightness);
-        pw.println("  mAppliedTemporaryAutoBrightnessAdjustment="
-                + mAppliedTemporaryAutoBrightnessAdjustment);
-        pw.println("  mAppliedBrightnessBoost=" + mAppliedBrightnessBoost);
         pw.println("  mDozing=" + mDozing);
         pw.println("  mSkipRampState=" + skipRampStateToString(mSkipRampState));
         pw.println("  mScreenOnBlockStartRealTime=" + mScreenOnBlockStartRealTime);
@@ -3129,9 +2598,8 @@
         pw.println("  mReportedToPolicy="
                 + reportedToPolicyToString(mReportedScreenStateToPolicy));
         pw.println("  mIsRbcActive=" + mIsRbcActive);
-        pw.println("  mOnStateChangePending=" + mOnStateChangedPending);
-        pw.println("  mOnProximityPositiveMessages=" + mOnProximityPositiveMessages);
-        pw.println("  mOnProximityNegativeMessages=" + mOnProximityNegativeMessages);
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "    ");
+        mAutomaticBrightnessStrategy.dump(ipw);
 
         if (mScreenBrightnessRampAnimator != null) {
             pw.println("  mScreenBrightnessRampAnimator.isAnimating()="
@@ -3156,6 +2624,8 @@
             dumpBrightnessEvents(pw);
         }
 
+        dumpRbcEvents(pw);
+
         if (mScreenOffBrightnessSensorController != null) {
             mScreenOffBrightnessSensorController.dump(pw);
         }
@@ -3173,21 +2643,30 @@
             mDisplayWhiteBalanceController.dump(pw);
             mDisplayWhiteBalanceSettings.dump(pw);
         }
-    }
 
-    private static String proximityToString(int state) {
-        switch (state) {
-            case PROXIMITY_UNKNOWN:
-                return "Unknown";
-            case PROXIMITY_NEGATIVE:
-                return "Negative";
-            case PROXIMITY_POSITIVE:
-                return "Positive";
-            default:
-                return Integer.toString(state);
+        pw.println();
+
+        if (mWakelockController != null) {
+            mWakelockController.dumpLocal(pw);
+        }
+
+        pw.println();
+        if (mDisplayBrightnessController != null) {
+            mDisplayBrightnessController.dump(pw);
+        }
+
+        pw.println();
+        if (mDisplayStateController != null) {
+            mDisplayStateController.dumpsys(pw);
+        }
+
+        pw.println();
+        if (mBrightnessClamperController != null) {
+            mBrightnessClamperController.dump(ipw);
         }
     }
 
+
     private static String reportedToPolicyToString(int state) {
         switch (state) {
             case REPORTED_TO_POLICY_SCREEN_OFF:
@@ -3228,14 +2707,20 @@
         }
     }
 
-    private static float clampAbsoluteBrightness(float value) {
-        return MathUtils.constrain(value, PowerManager.BRIGHTNESS_MIN,
-                PowerManager.BRIGHTNESS_MAX);
+    private void dumpRbcEvents(PrintWriter pw) {
+        int size = mRbcEventRingBuffer.size();
+        if (size < 1) {
+            pw.println("No Reduce Bright Colors Adjustments");
+            return;
+        }
+
+        pw.println("Reduce Bright Colors Adjustments Last " + size + " Events: ");
+        BrightnessEvent[] eventArray = mRbcEventRingBuffer.toArray();
+        for (int i = 0; i < mRbcEventRingBuffer.size(); i++) {
+            pw.println("  " + eventArray[i]);
+        }
     }
 
-    private static float clampAutoBrightnessAdjustment(float value) {
-        return MathUtils.constrain(value, -1.0f, 1.0f);
-    }
 
     private void noteScreenState(int screenState) {
         // Log screen state change with display id
@@ -3363,20 +2848,21 @@
         // It's easier to check if the brightness is at maximum level using the brightness
         // value untouched by any modifiers
         boolean brightnessIsMax = unmodifiedBrightness == event.getHbmMax();
-        float brightnessInNits = convertToAdjustedNits(event.getBrightness());
+        float brightnessInNits =
+                mDisplayBrightnessController.convertToAdjustedNits(event.getBrightness());
         float appliedLowPowerMode = event.isLowPowerModeSet() ? event.getPowerFactor() : -1f;
         int appliedRbcStrength  = event.isRbcEnabled() ? event.getRbcStrength() : -1;
         float appliedHbmMaxNits =
                 event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF
-                ? -1f : convertToAdjustedNits(event.getHbmMax());
+                ? -1f : mDisplayBrightnessController.convertToAdjustedNits(event.getHbmMax());
         // thermalCapNits set to -1 if not currently capping max brightness
         float appliedThermalCapNits =
                 event.getThermalMax() == PowerManager.BRIGHTNESS_MAX
-                ? -1f : convertToAdjustedNits(event.getThermalMax());
-
+                ? -1f : mDisplayBrightnessController.convertToAdjustedNits(event.getThermalMax());
         if (mIsDisplayInternal) {
             FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
-                    convertToAdjustedNits(event.getInitialBrightness()),
+                    mDisplayBrightnessController
+                            .convertToAdjustedNits(event.getInitialBrightness()),
                     brightnessInNits,
                     event.getLux(),
                     event.getPhysicalDisplayId(),
@@ -3393,7 +2879,8 @@
                     event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT,
                     event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR,
                     (modifier & BrightnessReason.MODIFIER_LOW_POWER) > 0,
-                    mBrightnessThrottler.getBrightnessMaxReason(),
+                    mBrightnessClamperController.getBrightnessMaxReason(),
+                    // TODO: (flc) add brightnessMinReason here too.
                     (modifier & BrightnessReason.MODIFIER_DIMMED) > 0,
                     event.isRbcEnabled(),
                     (flags & BrightnessEvent.FLAG_INVALID_LUX) > 0,
@@ -3404,6 +2891,17 @@
         }
     }
 
+    /**
+     * Indicates whether the display state is ready to update. If this is the default display, we
+     * want to update it right away so that we can draw the boot animation on it. If it is not
+     * the default display, drawing the boot animation on it would look incorrect, so we need
+     * to wait until boot is completed.
+     * @return True if the display state is ready to update
+     */
+    private boolean readyToUpdateDisplayState() {
+        return mDisplayId == Display.DEFAULT_DISPLAY || mBootCompleted;
+    }
+
     private final class DisplayControllerHandler extends Handler {
         DisplayControllerHandler(Looper looper) {
             super(looper, null, true /*async*/);
@@ -3416,10 +2914,6 @@
                     updatePowerState();
                     break;
 
-                case MSG_PROXIMITY_SENSOR_DEBOUNCED:
-                    debounceProximitySensor();
-                    break;
-
                 case MSG_SCREEN_ON_UNBLOCKED:
                     if (mPendingScreenOnUnblocker == msg.obj) {
                         unblockScreenOn();
@@ -3432,27 +2926,38 @@
                         updatePowerState();
                     }
                     break;
+                case MSG_OFFLOADING_SCREEN_ON_UNBLOCKED:
+                    if (mDisplayOffloadSession == msg.obj) {
+                        unblockScreenOnByDisplayOffload();
+                        updatePowerState();
+                    }
+                    break;
                 case MSG_CONFIGURE_BRIGHTNESS:
-                    mBrightnessConfiguration = (BrightnessConfiguration) msg.obj;
-                    mShouldResetShortTermModel = msg.arg1 == 1;
+                    BrightnessConfiguration brightnessConfiguration =
+                            (BrightnessConfiguration) msg.obj;
+                    mAutomaticBrightnessStrategy.setBrightnessConfiguration(brightnessConfiguration,
+                            msg.arg1 == 1);
+                    if (mBrightnessTracker != null) {
+                        mBrightnessTracker
+                                .setShouldCollectColorSample(brightnessConfiguration != null
+                                        && brightnessConfiguration.shouldCollectColorSamples());
+                    }
                     updatePowerState();
                     break;
 
                 case MSG_SET_TEMPORARY_BRIGHTNESS:
                     // TODO: Should we have a a timeout for the temporary brightness?
-                    mTemporaryScreenBrightness = Float.intBitsToFloat(msg.arg1);
+                    mDisplayBrightnessController
+                            .setTemporaryBrightness(Float.intBitsToFloat(msg.arg1));
                     updatePowerState();
                     break;
 
                 case MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT:
-                    mTemporaryAutoBrightnessAdjustment = Float.intBitsToFloat(msg.arg1);
+                    mAutomaticBrightnessStrategy
+                            .setTemporaryAutoBrightnessAdjustment(Float.intBitsToFloat(msg.arg1));
                     updatePowerState();
                     break;
 
-                case MSG_IGNORE_PROXIMITY:
-                    ignoreProximitySensorUntilChangedInternal();
-                    break;
-
                 case MSG_STOP:
                     cleanupHandlerThreadAfterStop();
                     break;
@@ -3500,27 +3005,15 @@
                 case MSG_SET_DWBC_LOGGING_ENABLED:
                     setDwbcLoggingEnabled(msg.arg1);
                     break;
+                case MSG_SET_BRIGHTNESS_FROM_OFFLOAD:
+                    mDisplayBrightnessController.setBrightnessFromOffload(
+                            Float.intBitsToFloat(msg.arg1));
+                    updatePowerState();
+                    break;
             }
         }
     }
 
-    private final SensorEventListener mProximitySensorListener = new SensorEventListener() {
-        @Override
-        public void onSensorChanged(SensorEvent event) {
-            if (mProximitySensorEnabled) {
-                final long time = mClock.uptimeMillis();
-                final float distance = event.values[0];
-                boolean positive = distance >= 0.0f && distance < mProximityThreshold;
-                handleProximitySensorEvent(time, positive);
-            }
-        }
-
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
-            // Not used.
-        }
-    };
-
 
     private final class SettingsObserver extends ContentObserver {
         SettingsObserver(Handler handler) {
@@ -3531,6 +3024,16 @@
         public void onChange(boolean selfChange, Uri uri) {
             if (uri.equals(Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE))) {
                 handleBrightnessModeChange();
+            } else if (uri.equals(Settings.System.getUriFor(
+                    Settings.System.SCREEN_BRIGHTNESS_FOR_ALS))) {
+                int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
+                        Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL,
+                        UserHandle.USER_CURRENT);
+                Slog.i(mTag, "Setting up auto-brightness for preset "
+                        + autoBrightnessPresetToString(preset));
+                setUpAutoBrightness(mContext, mHandler);
+                sendUpdatePowerState();
             } else {
                 handleSettingsChange(false /* userSwitch */);
             }
@@ -3581,28 +3084,6 @@
         msg.sendToTarget();
     }
 
-    @VisibleForTesting
-    String getSuspendBlockerUnfinishedBusinessId(int displayId) {
-        return "[" + displayId + "]unfinished business";
-    }
-
-    String getSuspendBlockerOnStateChangedId(int displayId) {
-        return "[" + displayId + "]on state changed";
-    }
-
-    String getSuspendBlockerProxPositiveId(int displayId) {
-        return "[" + displayId + "]prox positive";
-    }
-
-    String getSuspendBlockerProxNegativeId(int displayId) {
-        return "[" + displayId + "]prox negative";
-    }
-
-    @VisibleForTesting
-    String getSuspendBlockerProxDebounceId(int displayId) {
-        return "[" + displayId + "]prox debounce";
-    }
-
     /** Functional interface for providing time. */
     @VisibleForTesting
     interface Clock {
@@ -3629,6 +3110,20 @@
             return new DualRampAnimator(dps, firstProperty, secondProperty);
         }
 
+        WakelockController getWakelockController(int displayId,
+                DisplayPowerCallbacks displayPowerCallbacks) {
+            return new WakelockController(displayId, displayPowerCallbacks);
+        }
+
+        DisplayPowerProximityStateController getDisplayPowerProximityStateController(
+                WakelockController wakelockController, DisplayDeviceConfig displayDeviceConfig,
+                Looper looper, Runnable nudgeUpdatePowerState,
+                int displayId, SensorManager sensorManager) {
+            return new DisplayPowerProximityStateController(wakelockController, displayDeviceConfig,
+                    looper, nudgeUpdatePowerState,
+                    displayId, sensorManager, /* injector= */ null);
+        }
+
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
                 SensorManager sensorManager, Sensor lightSensor,
@@ -3711,11 +3206,32 @@
                     hbmChangeCallback, hbmMetadata, context);
         }
 
+        BrightnessRangeController getBrightnessRangeController(
+                HighBrightnessModeController hbmController, Runnable modeChangeCallback,
+                DisplayDeviceConfig displayDeviceConfig, Handler handler,
+                DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) {
+            return new BrightnessRangeController(hbmController,
+                    modeChangeCallback, displayDeviceConfig, handler, flags, displayToken, info);
+        }
+
+        BrightnessClamperController getBrightnessClamperController(Handler handler,
+                BrightnessClamperController.ClamperChangeListener clamperChangeListener,
+                BrightnessClamperController.DisplayDeviceData data, Context context,
+                DisplayManagerFlags flags) {
+
+            return new BrightnessClamperController(handler, clamperChangeListener, data, context,
+                    flags);
+        }
+
         DisplayWhiteBalanceController getDisplayWhiteBalanceController(Handler handler,
                 SensorManager sensorManager, Resources resources) {
             return DisplayWhiteBalanceFactory.create(handler,
                     sensorManager, resources);
         }
+
+        boolean isColorFadeEnabled() {
+            return !ActivityManager.isLowRamDeviceStatic();
+        }
     }
 
     static class CachedBrightnessInfo {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
deleted file mode 100644
index 2d860c0..0000000
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ /dev/null
@@ -1,3267 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display;
-
-import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
-import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
-import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
-import static com.android.server.display.config.DisplayBrightnessMappingConfig.autoBrightnessPresetToString;
-
-import android.animation.Animator;
-import android.animation.ObjectAnimator;
-import android.annotation.Nullable;
-import android.annotation.SuppressLint;
-import android.annotation.UserIdInt;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.pm.ParceledListSlice;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.hardware.Sensor;
-import android.hardware.SensorManager;
-import android.hardware.display.AmbientBrightnessDayStats;
-import android.hardware.display.BrightnessChangeEvent;
-import android.hardware.display.BrightnessConfiguration;
-import android.hardware.display.BrightnessInfo;
-import android.hardware.display.DisplayManagerInternal;
-import android.hardware.display.DisplayManagerInternal.DisplayOffloadSession;
-import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
-import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
-import android.metrics.LogMaker;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.PowerManager;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.os.SystemProperties;
-import android.os.Trace;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.util.FloatProperty;
-import android.util.IndentingPrintWriter;
-import android.util.MathUtils;
-import android.util.MutableFloat;
-import android.util.MutableInt;
-import android.util.Slog;
-import android.util.SparseArray;
-import android.view.Display;
-
-import com.android.internal.R;
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.app.IBatteryStats;
-import com.android.internal.display.BrightnessSynchronizer;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.internal.util.FrameworkStatsLog;
-import com.android.internal.util.RingBuffer;
-import com.android.server.LocalServices;
-import com.android.server.am.BatteryStatsService;
-import com.android.server.display.RampAnimator.DualRampAnimator;
-import com.android.server.display.brightness.BrightnessEvent;
-import com.android.server.display.brightness.BrightnessReason;
-import com.android.server.display.brightness.BrightnessUtils;
-import com.android.server.display.brightness.DisplayBrightnessController;
-import com.android.server.display.brightness.clamper.BrightnessClamperController;
-import com.android.server.display.brightness.strategy.AutomaticBrightnessStrategy;
-import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
-import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener;
-import com.android.server.display.feature.DisplayManagerFlags;
-import com.android.server.display.layout.Layout;
-import com.android.server.display.state.DisplayStateController;
-import com.android.server.display.utils.DebugUtils;
-import com.android.server.display.utils.SensorUtils;
-import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
-import com.android.server.display.whitebalance.DisplayWhiteBalanceFactory;
-import com.android.server.display.whitebalance.DisplayWhiteBalanceSettings;
-import com.android.server.policy.WindowManagerPolicy;
-
-import java.io.PrintWriter;
-import java.util.Objects;
-
-/**
- * Controls the power state of the display.
- *
- * Handles the proximity sensor, light sensor, and animations between states
- * including the screen off animation.
- *
- * This component acts independently of the rest of the power manager service.
- * In particular, it does not share any state and it only communicates
- * via asynchronous callbacks to inform the power manager that something has
- * changed.
- *
- * Everything this class does internally is serialized on its handler although
- * it may be accessed by other threads from the outside.
- *
- * Note that the power manager service guarantees that it will hold a suspend
- * blocker as long as the display is not ready.  So most of the work done here
- * does not need to worry about holding a suspend blocker unless it happens
- * independently of the display ready signal.
- *
- * For debugging, you can make the color fade and brightness animations run
- * slower by changing the "animator duration scale" option in Development Settings.
- */
-final class DisplayPowerController2 implements AutomaticBrightnessController.Callbacks,
-        DisplayWhiteBalanceController.Callbacks, DisplayPowerControllerInterface {
-    private static final String SCREEN_ON_BLOCKED_TRACE_NAME = "Screen on blocked";
-    private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked";
-
-    private static final String TAG = "DisplayPowerController2";
-    // 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
-    // actually turns on and starts showing new content after the call to set the
-    // screen state returns.  Playing the animation can also be somewhat slow.
-    private static final boolean USE_COLOR_FADE_ON_ANIMATION = false;
-
-    private static final float SCREEN_ANIMATION_RATE_MINIMUM = 0.0f;
-
-    private static final int COLOR_FADE_ON_ANIMATION_DURATION_MILLIS = 250;
-    private static final int COLOR_FADE_OFF_ANIMATION_DURATION_MILLIS = 400;
-
-    private static final int MSG_UPDATE_POWER_STATE = 1;
-    private static final int MSG_SCREEN_ON_UNBLOCKED = 2;
-    private static final int MSG_SCREEN_OFF_UNBLOCKED = 3;
-    private static final int MSG_CONFIGURE_BRIGHTNESS = 4;
-    private static final int MSG_SET_TEMPORARY_BRIGHTNESS = 5;
-    private static final int MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT = 6;
-    private static final int MSG_STOP = 7;
-    private static final int MSG_UPDATE_BRIGHTNESS = 8;
-    private static final int MSG_UPDATE_RBC = 9;
-    private static final int MSG_BRIGHTNESS_RAMP_DONE = 10;
-    private static final int MSG_STATSD_HBM_BRIGHTNESS = 11;
-    private static final int MSG_SWITCH_USER = 12;
-    private static final int MSG_BOOT_COMPLETED = 13;
-    private static final int MSG_SET_DWBC_STRONG_MODE = 14;
-    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;
-
-
-
-    private static final int BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS = 500;
-
-
-    // State machine constants for tracking initial brightness ramp skipping when enabled.
-    private static final int RAMP_STATE_SKIP_NONE = 0;
-    private static final int RAMP_STATE_SKIP_INITIAL = 1;
-    private static final int RAMP_STATE_SKIP_AUTOBRIGHT = 2;
-
-    private static final int REPORTED_TO_POLICY_UNREPORTED = -1;
-    private static final int REPORTED_TO_POLICY_SCREEN_OFF = 0;
-    private static final int REPORTED_TO_POLICY_SCREEN_TURNING_ON = 1;
-    private static final int REPORTED_TO_POLICY_SCREEN_ON = 2;
-    private static final int REPORTED_TO_POLICY_SCREEN_TURNING_OFF = 3;
-
-    private static final int RINGBUFFER_MAX = 100;
-    private static final int RINGBUFFER_RBC_MAX = 20;
-
-    private static final float[] BRIGHTNESS_RANGE_BOUNDARIES = {
-        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80,
-        90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1200,
-        1400, 1600, 1800, 2000, 2250, 2500, 2750, 3000};
-    private static final int[] BRIGHTNESS_RANGE_INDEX = {
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_UNKNOWN,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_0_1,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_1_2,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_2_3,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_3_4,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_4_5,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_5_6,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_6_7,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_7_8,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_8_9,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_9_10,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_10_20,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_20_30,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_30_40,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_40_50,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_50_60,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_60_70,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_70_80,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_80_90,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_90_100,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_100_200,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_200_300,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_300_400,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_400_500,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_500_600,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_600_700,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_700_800,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_800_900,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_900_1000,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_1000_1200,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_1200_1400,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_1400_1600,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_1600_1800,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_1800_2000,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_2000_2250,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_2250_2500,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_2500_2750,
-        FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_2750_3000,
-    };
-
-    private final String mTag;
-
-    private final Object mLock = new Object();
-
-    private final Context mContext;
-
-    // Our handler.
-    private final DisplayControllerHandler mHandler;
-
-    // Battery stats.
-    @Nullable
-    private final IBatteryStats mBatteryStats;
-
-    // The sensor manager.
-    private final SensorManager mSensorManager;
-
-    // The window manager policy.
-    private final WindowManagerPolicy mWindowManagerPolicy;
-
-    // The display blanker.
-    private final DisplayBlanker mBlanker;
-
-    // The LogicalDisplay tied to this DisplayPowerController2.
-    private final LogicalDisplay mLogicalDisplay;
-
-    // The ID of the LogicalDisplay tied to this DisplayPowerController2.
-    private final int mDisplayId;
-
-    // The ID of the display which this display follows for brightness purposes.
-    private int mLeadDisplayId = Layout.NO_LEAD_DISPLAY;
-
-    // The unique ID of the primary display device currently tied to this logical display
-    private String mUniqueDisplayId;
-
-    // Tracker for brightness changes.
-    @Nullable
-    private final BrightnessTracker mBrightnessTracker;
-
-    // Tracker for brightness settings changes.
-    private final SettingsObserver mSettingsObserver;
-
-    // The doze screen brightness.
-    private final float mScreenBrightnessDozeConfig;
-
-    // True if auto-brightness should be used.
-    private boolean mUseSoftwareAutoBrightnessConfig;
-
-    // Whether or not the color fade on screen on / off is enabled.
-    private final boolean mColorFadeEnabled;
-
-    @GuardedBy("mCachedBrightnessInfo")
-    private final CachedBrightnessInfo mCachedBrightnessInfo = new CachedBrightnessInfo();
-
-    private DisplayDevice mDisplayDevice;
-
-    // True if we should fade the screen while turning it off, false if we should play
-    // a stylish color fade animation instead.
-    private final boolean mColorFadeFadesConfig;
-
-    // True if we need to fake a transition to off when coming out of a doze state.
-    // Some display hardware will blank itself when coming out of doze in order to hide
-    // artifacts. For these displays we fake a transition into OFF so that policy can appropriately
-    // blank itself and begin an appropriate power on animation.
-    private final boolean mDisplayBlanksAfterDozeConfig;
-
-    // True if there are only buckets of brightness values when the display is in the doze state,
-    // rather than a full range of values. If this is true, then we'll avoid animating the screen
-    // brightness since it'd likely be multiple jarring brightness transitions instead of just one
-    // to reach the final state.
-    private final boolean mBrightnessBucketsInDozeConfig;
-
-    private final Clock mClock;
-    private final Injector mInjector;
-
-    // Maximum time a ramp animation can take.
-    private long mBrightnessRampIncreaseMaxTimeMillis;
-    private long mBrightnessRampDecreaseMaxTimeMillis;
-
-    // Maximum time a ramp animation can take in idle mode.
-    private long mBrightnessRampIncreaseMaxTimeIdleMillis;
-    private long mBrightnessRampDecreaseMaxTimeIdleMillis;
-
-    // The pending power request.
-    // Initially null until the first call to requestPowerState.
-    @GuardedBy("mLock")
-    private DisplayPowerRequest mPendingRequestLocked;
-
-    // True if the pending power request or wait for negative proximity flag
-    // has been changed since the last update occurred.
-    @GuardedBy("mLock")
-    private boolean mPendingRequestChangedLocked;
-
-    // Set to true when the important parts of the pending power request have been applied.
-    // The important parts are mainly the screen state.  Brightness changes may occur
-    // concurrently.
-    @GuardedBy("mLock")
-    private boolean mDisplayReadyLocked;
-
-    // Set to true if a power state update is required.
-    @GuardedBy("mLock")
-    private boolean mPendingUpdatePowerStateLocked;
-
-    /* The following state must only be accessed by the handler thread. */
-
-    // The currently requested power state.
-    // The power controller will progressively update its internal state to match
-    // the requested power state.  Initially null until the first update.
-    private DisplayPowerRequest mPowerRequest;
-
-    // The current power state.
-    // Must only be accessed on the handler thread.
-    private DisplayPowerState mPowerState;
-
-
-
-    // The currently active screen on unblocker.  This field is non-null whenever
-    // 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
-    // turning off the screen.
-    private boolean mPendingScreenOff;
-
-    // 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;
-
-    private boolean mAppliedDimming;
-
-    private boolean mAppliedThrottling;
-
-    // Reason for which the brightness was last changed. See {@link BrightnessReason} for more
-    // information.
-    // At the time of this writing, this value is changed within updatePowerState() only, which is
-    // limited to the thread used by DisplayControllerHandler.
-    private final BrightnessReason mBrightnessReason = new BrightnessReason();
-    private final BrightnessReason mBrightnessReasonTemp = new BrightnessReason();
-
-    // Brightness animation ramp rates in brightness units per second
-    private float mBrightnessRampRateFastDecrease;
-    private float mBrightnessRampRateFastIncrease;
-    private float mBrightnessRampRateSlowDecrease;
-    private float mBrightnessRampRateSlowIncrease;
-    private float mBrightnessRampRateSlowDecreaseIdle;
-    private float mBrightnessRampRateSlowIncreaseIdle;
-
-    // Report HBM brightness change to StatsD
-    private int mDisplayStatsId;
-    private float mLastStatsBrightness = PowerManager.BRIGHTNESS_MIN;
-
-    // Whether or not to skip the initial brightness ramps into STATE_ON.
-    private final boolean mSkipScreenOnBrightnessRamp;
-
-    // Display white balance components.
-    // Critical methods must be called on DPC2 handler thread.
-    @Nullable
-    private final DisplayWhiteBalanceSettings mDisplayWhiteBalanceSettings;
-    @Nullable
-    private final DisplayWhiteBalanceController mDisplayWhiteBalanceController;
-
-    @Nullable
-    private final ColorDisplayServiceInternal mCdsi;
-    private float[] mNitsRange;
-
-    private final BrightnessRangeController mBrightnessRangeController;
-
-    private final BrightnessThrottler mBrightnessThrottler;
-
-    private final BrightnessClamperController mBrightnessClamperController;
-
-    private final Runnable mOnBrightnessChangeRunnable;
-
-    private final BrightnessEvent mLastBrightnessEvent;
-    private final BrightnessEvent mTempBrightnessEvent;
-
-    private final DisplayBrightnessController mDisplayBrightnessController;
-
-    // Keeps a record of brightness changes for dumpsys.
-    private RingBuffer<BrightnessEvent> mBrightnessEventRingBuffer;
-
-    // Keeps a record of rbc changes for dumpsys.
-    private final RingBuffer<BrightnessEvent> mRbcEventRingBuffer =
-            new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_RBC_MAX);
-
-    // Controls and tracks all the wakelocks that are acquired/released by the system. Also acts as
-    // a medium of communication between this class and the PowerManagerService.
-    private final WakelockController mWakelockController;
-
-    // Tracks and manages the proximity state of the associated display.
-    private final DisplayPowerProximityStateController mDisplayPowerProximityStateController;
-
-    // Tracks and manages the display state of the associated display.
-    private final DisplayStateController mDisplayStateController;
-
-
-    // Responsible for evaluating and tracking the automatic brightness relevant states.
-    // Todo: This is a temporary workaround. Ideally DPC2 should never talk to the strategies
-    private final AutomaticBrightnessStrategy mAutomaticBrightnessStrategy;
-
-    // A record of state for skipping brightness ramps.
-    private int mSkipRampState = RAMP_STATE_SKIP_NONE;
-
-    // The first autobrightness value set when entering RAMP_STATE_SKIP_INITIAL.
-    private float mInitialAutoBrightness;
-
-    // The controller for the automatic brightness level.
-    @Nullable
-    private AutomaticBrightnessController mAutomaticBrightnessController;
-
-    // The controller for the sensor used to estimate ambient lux while the display is off.
-    @Nullable
-    private ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController;
-
-    private Sensor mLightSensor;
-    private Sensor mScreenOffBrightnessSensor;
-
-    private boolean mIsRbcActive;
-
-    // Animators.
-    private ObjectAnimator mColorFadeOnAnimator;
-    private ObjectAnimator mColorFadeOffAnimator;
-    private DualRampAnimator<DisplayPowerState> mScreenBrightnessRampAnimator;
-
-    // True if this DisplayPowerController2 has been stopped and should no longer be running.
-    private boolean mStopped;
-
-    private DisplayDeviceConfig mDisplayDeviceConfig;
-
-    private boolean mIsEnabled;
-    private boolean mIsInTransition;
-    private boolean mIsDisplayInternal;
-
-    // The id of the thermal brightness throttling policy that should be used.
-    private String mThermalBrightnessThrottlingDataId;
-
-    // DPCs following the brightness of this DPC. This is used in concurrent displays mode - there
-    // is one lead display, the additional displays follow the brightness value of the lead display.
-    @GuardedBy("mLock")
-    private SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers =
-            new SparseArray();
-
-    private boolean mBootCompleted;
-    private final DisplayManagerFlags mFlags;
-
-    private DisplayOffloadSession mDisplayOffloadSession;
-
-    /**
-     * Creates the display power controller.
-     */
-    DisplayPowerController2(Context context, Injector injector,
-            DisplayPowerCallbacks callbacks, Handler handler,
-            SensorManager sensorManager, DisplayBlanker blanker, LogicalDisplay logicalDisplay,
-            BrightnessTracker brightnessTracker, BrightnessSetting brightnessSetting,
-            Runnable onBrightnessChangeRunnable, HighBrightnessModeMetadata hbmMetadata,
-            boolean bootCompleted, DisplayManagerFlags flags) {
-        mFlags = flags;
-        mInjector = injector != null ? injector : new Injector();
-        mClock = mInjector.getClock();
-        mLogicalDisplay = logicalDisplay;
-        mDisplayId = mLogicalDisplay.getDisplayIdLocked();
-        mSensorManager = sensorManager;
-        mHandler = new DisplayControllerHandler(handler.getLooper());
-        mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceConfig();
-        mIsEnabled = logicalDisplay.isEnabledLocked();
-        mIsInTransition = logicalDisplay.isInTransitionLocked();
-        mIsDisplayInternal = logicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL;
-        mWakelockController = mInjector.getWakelockController(mDisplayId, callbacks);
-        mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(
-                mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
-                () -> updatePowerState(), mDisplayId, mSensorManager);
-        mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);
-        mTag = TAG + "[" + mDisplayId + "]";
-        mThermalBrightnessThrottlingDataId =
-                logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
-        mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
-        mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
-        mDisplayStatsId = mUniqueDisplayId.hashCode();
-
-        mLastBrightnessEvent = new BrightnessEvent(mDisplayId);
-        mTempBrightnessEvent = new BrightnessEvent(mDisplayId);
-
-        if (mDisplayId == Display.DEFAULT_DISPLAY) {
-            mBatteryStats = BatteryStatsService.getService();
-        } else {
-            mBatteryStats = null;
-        }
-
-        mSettingsObserver = new SettingsObserver(mHandler);
-        mWindowManagerPolicy = LocalServices.getService(WindowManagerPolicy.class);
-        mBlanker = blanker;
-        mContext = context;
-        mBrightnessTracker = brightnessTracker;
-        mOnBrightnessChangeRunnable = onBrightnessChangeRunnable;
-
-        PowerManager pm = context.getSystemService(PowerManager.class);
-
-        final Resources resources = context.getResources();
-
-        // DOZE AND DIM SETTINGS
-        mScreenBrightnessDozeConfig = BrightnessUtils.clampAbsoluteBrightness(
-                pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE));
-        loadBrightnessRampRates();
-        mSkipScreenOnBrightnessRamp = resources.getBoolean(
-                R.bool.config_skipScreenOnBrightnessRamp);
-
-        Runnable modeChangeCallback = () -> {
-            sendUpdatePowerState();
-            postBrightnessChangeRunnable();
-            // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern.
-            if (mAutomaticBrightnessController != null) {
-                mAutomaticBrightnessController.update();
-            }
-        };
-
-        HighBrightnessModeController hbmController = createHbmControllerLocked(hbmMetadata,
-                modeChangeCallback);
-        mBrightnessThrottler = createBrightnessThrottlerLocked();
-
-        mBrightnessRangeController = mInjector.getBrightnessRangeController(hbmController,
-                modeChangeCallback, mDisplayDeviceConfig, mHandler, flags,
-                mDisplayDevice.getDisplayTokenLocked(),
-                mDisplayDevice.getDisplayDeviceInfoLocked());
-
-        mDisplayBrightnessController =
-                new DisplayBrightnessController(context, null,
-                        mDisplayId, mLogicalDisplay.getDisplayInfoLocked().brightnessDefault,
-                        brightnessSetting, () -> postBrightnessChangeRunnable(),
-                        new HandlerExecutor(mHandler), flags);
-
-        mBrightnessClamperController = mInjector.getBrightnessClamperController(
-                mHandler, modeChangeCallback::run,
-                new BrightnessClamperController.DisplayDeviceData(
-                        mUniqueDisplayId,
-                        mThermalBrightnessThrottlingDataId,
-                        logicalDisplay.getPowerThrottlingDataIdLocked(),
-                        mDisplayDeviceConfig), mContext, flags);
-        // Seed the cached brightness
-        saveBrightnessInfo(getScreenBrightnessSetting());
-        mAutomaticBrightnessStrategy =
-                mDisplayBrightnessController.getAutomaticBrightnessStrategy();
-
-        DisplayWhiteBalanceSettings displayWhiteBalanceSettings = null;
-        DisplayWhiteBalanceController displayWhiteBalanceController = null;
-        if (mDisplayId == Display.DEFAULT_DISPLAY) {
-            try {
-                displayWhiteBalanceController = mInjector.getDisplayWhiteBalanceController(
-                        mHandler, mSensorManager, resources);
-                displayWhiteBalanceSettings = new DisplayWhiteBalanceSettings(mContext, mHandler);
-                displayWhiteBalanceSettings.setCallbacks(this);
-                displayWhiteBalanceController.setCallbacks(this);
-            } catch (Exception e) {
-                Slog.e(mTag, "failed to set up display white-balance: " + e);
-            }
-        }
-        mDisplayWhiteBalanceSettings = displayWhiteBalanceSettings;
-        mDisplayWhiteBalanceController = displayWhiteBalanceController;
-
-        loadNitsRange(resources);
-
-        if (mDisplayId == Display.DEFAULT_DISPLAY) {
-            mCdsi = LocalServices.getService(ColorDisplayServiceInternal.class);
-            if (mCdsi != null) {
-                boolean active = mCdsi.setReduceBrightColorsListener(
-                        new ReduceBrightColorsListener() {
-                            @Override
-                            public void onReduceBrightColorsActivationChanged(boolean activated,
-                                    boolean userInitiated) {
-                                applyReduceBrightColorsSplineAdjustment();
-
-                            }
-
-                            @Override
-                            public void onReduceBrightColorsStrengthChanged(int strength) {
-                                applyReduceBrightColorsSplineAdjustment();
-                            }
-                        });
-                if (active) {
-                    applyReduceBrightColorsSplineAdjustment();
-                }
-            }
-        } else {
-            mCdsi = null;
-        }
-
-        setUpAutoBrightness(context, handler);
-
-        mColorFadeEnabled = mInjector.isColorFadeEnabled()
-                && !resources.getBoolean(
-                  com.android.internal.R.bool.config_displayColorFadeDisabled);
-        mColorFadeFadesConfig = resources.getBoolean(
-                R.bool.config_animateScreenLights);
-
-        mDisplayBlanksAfterDozeConfig = resources.getBoolean(
-                R.bool.config_displayBlanksAfterDoze);
-
-        mBrightnessBucketsInDozeConfig = resources.getBoolean(
-                R.bool.config_displayBrightnessBucketsInDoze);
-
-        mBootCompleted = bootCompleted;
-    }
-
-    private void applyReduceBrightColorsSplineAdjustment() {
-        mHandler.obtainMessage(MSG_UPDATE_RBC).sendToTarget();
-        sendUpdatePowerState();
-    }
-
-    private void handleRbcChanged() {
-        if (mAutomaticBrightnessController == null) {
-            return;
-        }
-
-        float[] adjustedNits = new float[mNitsRange.length];
-        for (int i = 0; i < mNitsRange.length; i++) {
-            adjustedNits[i] = mCdsi.getReduceBrightColorsAdjustedBrightnessNits(mNitsRange[i]);
-        }
-        mIsRbcActive = mCdsi.isReduceBrightColorsActivated();
-        mAutomaticBrightnessController.recalculateSplines(mIsRbcActive, adjustedNits);
-    }
-
-    /**
-     * Returns true if the proximity sensor screen-off function is available.
-     */
-    @Override
-    public boolean isProximitySensorAvailable() {
-        return mDisplayPowerProximityStateController.isProximitySensorAvailable();
-    }
-
-    /**
-     * Get the {@link BrightnessChangeEvent}s for the specified user.
-     *
-     * @param userId         userId to fetch data for
-     * @param includePackage if false will null out the package name in events
-     */
-    @Nullable
-    @Override
-    public ParceledListSlice<BrightnessChangeEvent> getBrightnessEvents(
-            @UserIdInt int userId, boolean includePackage) {
-        if (mBrightnessTracker == null) {
-            return null;
-        }
-        return mBrightnessTracker.getEvents(userId, includePackage);
-    }
-
-    @Override
-    public void onSwitchUser(@UserIdInt int newUserId) {
-        Message msg = mHandler.obtainMessage(MSG_SWITCH_USER, newUserId);
-        mHandler.sendMessage(msg);
-    }
-
-    private void handleOnSwitchUser(@UserIdInt int newUserId) {
-        handleSettingsChange(true /* userSwitch */);
-        handleBrightnessModeChange();
-        if (mBrightnessTracker != null) {
-            mBrightnessTracker.onSwitchUser(newUserId);
-        }
-    }
-
-    @Nullable
-    @Override
-    public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats(
-            @UserIdInt int userId) {
-        if (mBrightnessTracker == null) {
-            return null;
-        }
-        return mBrightnessTracker.getAmbientBrightnessStats(userId);
-    }
-
-    /**
-     * Persist the brightness slider events and ambient brightness stats to disk.
-     */
-    @Override
-    public void persistBrightnessTrackerState() {
-        if (mBrightnessTracker != null) {
-            mBrightnessTracker.persistBrightnessTrackerState();
-        }
-    }
-
-    /**
-     * Requests a new power state.
-     * The controller makes a copy of the provided object and then
-     * begins adjusting the power state to match what was requested.
-     *
-     * @param request                  The requested power state.
-     * @param waitForNegativeProximity If true, issues a request to wait for
-     *                                 negative proximity before turning the screen back on,
-     *                                 assuming the screen
-     *                                 was turned off by the proximity sensor.
-     * @return True if display is ready, false if there are important changes that must
-     * be made asynchronously (such as turning the screen on), in which case the caller
-     * should grab a wake lock, watch for {@link DisplayPowerCallbacks#onStateChanged()}
-     * then try the request again later until the state converges.
-     */
-    public boolean requestPowerState(DisplayPowerRequest request,
-            boolean waitForNegativeProximity) {
-        if (DEBUG) {
-            Slog.d(mTag, "requestPowerState: "
-                    + request + ", waitForNegativeProximity=" + waitForNegativeProximity);
-        }
-
-        synchronized (mLock) {
-            if (mStopped) {
-                return true;
-            }
-
-            boolean changed = mDisplayPowerProximityStateController
-                    .setPendingWaitForNegativeProximityLocked(waitForNegativeProximity);
-
-            if (mPendingRequestLocked == null) {
-                mPendingRequestLocked = new DisplayPowerRequest(request);
-                changed = true;
-            } else if (!mPendingRequestLocked.equals(request)) {
-                mPendingRequestLocked.copyFrom(request);
-                changed = true;
-            }
-
-            if (changed) {
-                mDisplayReadyLocked = false;
-                if (!mPendingRequestChangedLocked) {
-                    mPendingRequestChangedLocked = true;
-                    sendUpdatePowerStateLocked();
-                }
-            }
-
-            return mDisplayReadyLocked;
-        }
-    }
-
-    @Override
-    public void overrideDozeScreenState(int displayState) {
-        mHandler.postAtTime(() -> {
-            if (mDisplayOffloadSession == null
-                    || !(DisplayOffloadSession.isSupportedOffloadState(displayState)
-                    || displayState == Display.STATE_UNKNOWN)) {
-                return;
-            }
-            mDisplayStateController.overrideDozeScreenState(displayState);
-            sendUpdatePowerState();
-        }, mClock.uptimeMillis());
-    }
-
-    @Override
-    public void setDisplayOffloadSession(DisplayOffloadSession session) {
-        if (session == mDisplayOffloadSession) {
-            return;
-        }
-        unblockScreenOnByDisplayOffload();
-        mDisplayOffloadSession = session;
-    }
-
-    @Override
-    public BrightnessConfiguration getDefaultBrightnessConfiguration() {
-        if (mAutomaticBrightnessController == null) {
-            return null;
-        }
-        return mAutomaticBrightnessController.getDefaultConfig();
-    }
-
-    /**
-     * Notified when the display is changed. We use this to apply any changes that might be needed
-     * when displays get swapped on foldable devices.  For example, different brightness properties
-     * of each display need to be properly reflected in AutomaticBrightnessController.
-     *
-     * Make sure DisplayManagerService.mSyncRoot lock is held when this is called
-     */
-    @Override
-    public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata, int leadDisplayId) {
-        mLeadDisplayId = leadDisplayId;
-        final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
-        if (device == null) {
-            Slog.wtf(mTag, "Display Device is null in DisplayPowerController2 for display: "
-                    + mLogicalDisplay.getDisplayIdLocked());
-            return;
-        }
-
-        final String uniqueId = device.getUniqueId();
-        final DisplayDeviceConfig config = device.getDisplayDeviceConfig();
-        final IBinder token = device.getDisplayTokenLocked();
-        final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
-        final boolean isEnabled = mLogicalDisplay.isEnabledLocked();
-        final boolean isInTransition = mLogicalDisplay.isInTransitionLocked();
-        final boolean isDisplayInternal = mLogicalDisplay.getPrimaryDisplayDeviceLocked() != null
-                && mLogicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL;
-        final String thermalBrightnessThrottlingDataId =
-                mLogicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
-        final String powerThrottlingDataId =
-                mLogicalDisplay.getPowerThrottlingDataIdLocked();
-
-        mHandler.postAtTime(() -> {
-            boolean changed = false;
-            if (mDisplayDevice != device) {
-                changed = true;
-                mDisplayDevice = device;
-                mUniqueDisplayId = uniqueId;
-                mDisplayStatsId = mUniqueDisplayId.hashCode();
-                mDisplayDeviceConfig = config;
-                mThermalBrightnessThrottlingDataId = thermalBrightnessThrottlingDataId;
-                loadFromDisplayDeviceConfig(token, info, hbmMetadata);
-                mDisplayPowerProximityStateController.notifyDisplayDeviceChanged(config);
-
-                // Since the underlying display-device changed, we really don't know the
-                // last command that was sent to change it's state. Let's assume it is unknown so
-                // that we trigger a change immediately.
-                mPowerState.resetScreenState();
-            } else if (!Objects.equals(mThermalBrightnessThrottlingDataId,
-                    thermalBrightnessThrottlingDataId)) {
-                changed = true;
-                mThermalBrightnessThrottlingDataId = thermalBrightnessThrottlingDataId;
-                mBrightnessThrottler.loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(
-                        config.getThermalBrightnessThrottlingDataMapByThrottlingId(),
-                        mThermalBrightnessThrottlingDataId,
-                        mUniqueDisplayId);
-            }
-            if (mIsEnabled != isEnabled || mIsInTransition != isInTransition) {
-                changed = true;
-                mIsEnabled = isEnabled;
-                mIsInTransition = isInTransition;
-            }
-
-            mIsDisplayInternal = isDisplayInternal;
-            // using local variables here, when mBrightnessThrottler is removed,
-            // mThermalBrightnessThrottlingDataId could be removed as well
-            // changed = true will be not needed - clampers are maintaining their state and
-            // will call updatePowerState if needed.
-            mBrightnessClamperController.onDisplayChanged(
-                    new BrightnessClamperController.DisplayDeviceData(uniqueId,
-                        thermalBrightnessThrottlingDataId, powerThrottlingDataId, config));
-
-            if (changed) {
-                updatePowerState();
-            }
-        }, mClock.uptimeMillis());
-    }
-
-    /**
-     * Unregisters all listeners and interrupts all running threads; halting future work.
-     *
-     * This method should be called when the DisplayPowerController2 is no longer in use; i.e. when
-     * the {@link #mDisplayId display} has been removed.
-     */
-    @Override
-    public void stop() {
-        synchronized (mLock) {
-            clearDisplayBrightnessFollowersLocked();
-
-            mStopped = true;
-            Message msg = mHandler.obtainMessage(MSG_STOP);
-            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-
-            if (mAutomaticBrightnessController != null) {
-                mAutomaticBrightnessController.stop();
-            }
-
-            mDisplayBrightnessController.stop();
-
-            mContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
-        }
-    }
-
-    private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info,
-            HighBrightnessModeMetadata hbmMetadata) {
-        // All properties that depend on the associated DisplayDevice and the DDC must be
-        // updated here.
-        loadBrightnessRampRates();
-        loadNitsRange(mContext.getResources());
-        setUpAutoBrightness(mContext, mHandler);
-        reloadReduceBrightColours();
-        setAnimatorRampSpeeds(/* isIdleMode= */ false);
-
-        mBrightnessRangeController.loadFromConfig(hbmMetadata, token, info, mDisplayDeviceConfig);
-        mBrightnessThrottler.loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(
-                mDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId(),
-                mThermalBrightnessThrottlingDataId, mUniqueDisplayId);
-    }
-
-    private void sendUpdatePowerState() {
-        synchronized (mLock) {
-            sendUpdatePowerStateLocked();
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void sendUpdatePowerStateLocked() {
-        if (!mStopped && !mPendingUpdatePowerStateLocked) {
-            mPendingUpdatePowerStateLocked = true;
-            Message msg = mHandler.obtainMessage(MSG_UPDATE_POWER_STATE);
-            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-        }
-    }
-
-    private void initialize(int displayState) {
-        mPowerState = mInjector.getDisplayPowerState(mBlanker,
-                mColorFadeEnabled ? new ColorFade(mDisplayId) : null, mDisplayId, displayState);
-
-        if (mColorFadeEnabled) {
-            mColorFadeOnAnimator = ObjectAnimator.ofFloat(
-                    mPowerState, DisplayPowerState.COLOR_FADE_LEVEL, 0.0f, 1.0f);
-            mColorFadeOnAnimator.setDuration(COLOR_FADE_ON_ANIMATION_DURATION_MILLIS);
-            mColorFadeOnAnimator.addListener(mAnimatorListener);
-
-            mColorFadeOffAnimator = ObjectAnimator.ofFloat(
-                    mPowerState, DisplayPowerState.COLOR_FADE_LEVEL, 1.0f, 0.0f);
-            mColorFadeOffAnimator.setDuration(COLOR_FADE_OFF_ANIMATION_DURATION_MILLIS);
-            mColorFadeOffAnimator.addListener(mAnimatorListener);
-        }
-
-        mScreenBrightnessRampAnimator = mInjector.getDualRampAnimator(mPowerState,
-                DisplayPowerState.SCREEN_BRIGHTNESS_FLOAT,
-                DisplayPowerState.SCREEN_SDR_BRIGHTNESS_FLOAT);
-        setAnimatorRampSpeeds(mAutomaticBrightnessController != null
-                && mAutomaticBrightnessController.isInIdleMode());
-        mScreenBrightnessRampAnimator.setListener(mRampAnimatorListener);
-
-        noteScreenState(mPowerState.getScreenState());
-        noteScreenBrightness(mPowerState.getScreenBrightness());
-
-        // Initialize all of the brightness tracking state
-        final float brightness = mDisplayBrightnessController.convertToAdjustedNits(
-                mPowerState.getScreenBrightness());
-        if (mBrightnessTracker != null && brightness >= PowerManager.BRIGHTNESS_MIN) {
-            mBrightnessTracker.start(brightness);
-        }
-
-        BrightnessSetting.BrightnessSettingListener brightnessSettingListener = brightnessValue -> {
-            Message msg = mHandler.obtainMessage(MSG_UPDATE_BRIGHTNESS, brightnessValue);
-            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-        };
-        mDisplayBrightnessController
-                .registerBrightnessSettingChangeListener(brightnessSettingListener);
-
-        mContext.getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ),
-                false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
-        mContext.getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE),
-                false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
-        if (mFlags.areAutoBrightnessModesEnabled()) {
-            mContext.getContentResolver().registerContentObserver(
-                    Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_FOR_ALS),
-                    /* notifyForDescendants= */ false, mSettingsObserver, UserHandle.USER_CURRENT);
-        }
-        handleBrightnessModeChange();
-    }
-
-    private void setUpAutoBrightness(Context context, Handler handler) {
-        mUseSoftwareAutoBrightnessConfig = mDisplayDeviceConfig.isAutoBrightnessAvailable();
-
-        if (!mUseSoftwareAutoBrightnessConfig) {
-            return;
-        }
-
-        SparseArray<BrightnessMappingStrategy> brightnessMappers = new SparseArray<>();
-
-        BrightnessMappingStrategy defaultModeBrightnessMapper =
-                mInjector.getDefaultModeBrightnessMapper(context, mDisplayDeviceConfig,
-                        mDisplayWhiteBalanceController);
-        brightnessMappers.append(AUTO_BRIGHTNESS_MODE_DEFAULT,
-                defaultModeBrightnessMapper);
-
-        final boolean isIdleScreenBrightnessEnabled = context.getResources().getBoolean(
-                R.bool.config_enableIdleScreenBrightnessMode);
-        if (isIdleScreenBrightnessEnabled) {
-            BrightnessMappingStrategy idleModeBrightnessMapper =
-                    BrightnessMappingStrategy.create(context, mDisplayDeviceConfig,
-                            AUTO_BRIGHTNESS_MODE_IDLE,
-                            mDisplayWhiteBalanceController);
-            if (idleModeBrightnessMapper != null) {
-                brightnessMappers.append(AUTO_BRIGHTNESS_MODE_IDLE,
-                        idleModeBrightnessMapper);
-            }
-        }
-
-        BrightnessMappingStrategy dozeModeBrightnessMapper =
-                BrightnessMappingStrategy.create(context, mDisplayDeviceConfig,
-                        AUTO_BRIGHTNESS_MODE_DOZE, mDisplayWhiteBalanceController);
-        if (mFlags.areAutoBrightnessModesEnabled() && dozeModeBrightnessMapper != null) {
-            brightnessMappers.put(AUTO_BRIGHTNESS_MODE_DOZE, dozeModeBrightnessMapper);
-        }
-
-        float userLux = BrightnessMappingStrategy.INVALID_LUX;
-        float userNits = BrightnessMappingStrategy.INVALID_NITS;
-        if (mAutomaticBrightnessController != null) {
-            userLux = mAutomaticBrightnessController.getUserLux();
-            userNits = mAutomaticBrightnessController.getUserNits();
-        }
-
-        if (defaultModeBrightnessMapper != null) {
-            final float dozeScaleFactor = context.getResources().getFraction(
-                    R.fraction.config_screenAutoBrightnessDozeScaleFactor,
-                    1, 1);
-
-            // Ambient Lux - Active Mode Brightness Thresholds
-            float[] ambientBrighteningThresholds =
-                    mDisplayDeviceConfig.getAmbientBrighteningPercentages();
-            float[] ambientDarkeningThresholds =
-                    mDisplayDeviceConfig.getAmbientDarkeningPercentages();
-            float[] ambientBrighteningLevels =
-                    mDisplayDeviceConfig.getAmbientBrighteningLevels();
-            float[] ambientDarkeningLevels =
-                    mDisplayDeviceConfig.getAmbientDarkeningLevels();
-            float ambientDarkeningMinThreshold =
-                    mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
-            float ambientBrighteningMinThreshold =
-                    mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
-            HysteresisLevels ambientBrightnessThresholds = mInjector.getHysteresisLevels(
-                    ambientBrighteningThresholds, ambientDarkeningThresholds,
-                    ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
-                    ambientBrighteningMinThreshold);
-
-            // Display - Active Mode Brightness Thresholds
-            float[] screenBrighteningThresholds =
-                    mDisplayDeviceConfig.getScreenBrighteningPercentages();
-            float[] screenDarkeningThresholds =
-                    mDisplayDeviceConfig.getScreenDarkeningPercentages();
-            float[] screenBrighteningLevels =
-                    mDisplayDeviceConfig.getScreenBrighteningLevels();
-            float[] screenDarkeningLevels =
-                    mDisplayDeviceConfig.getScreenDarkeningLevels();
-            float screenDarkeningMinThreshold =
-                    mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
-            float screenBrighteningMinThreshold =
-                    mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
-            HysteresisLevels screenBrightnessThresholds = mInjector.getHysteresisLevels(
-                    screenBrighteningThresholds, screenDarkeningThresholds,
-                    screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
-                    screenBrighteningMinThreshold, true);
-
-            // Ambient Lux - Idle Screen Brightness Thresholds
-            float ambientDarkeningMinThresholdIdle =
-                    mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle();
-            float ambientBrighteningMinThresholdIdle =
-                    mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle();
-            float[] ambientBrighteningThresholdsIdle =
-                    mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle();
-            float[] ambientDarkeningThresholdsIdle =
-                    mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle();
-            float[] ambientBrighteningLevelsIdle =
-                    mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
-            float[] ambientDarkeningLevelsIdle =
-                    mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
-            HysteresisLevels ambientBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
-                    ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
-                    ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
-                    ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
-
-            // Display - Idle Screen Brightness Thresholds
-            float screenDarkeningMinThresholdIdle =
-                    mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
-            float screenBrighteningMinThresholdIdle =
-                    mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
-            float[] screenBrighteningThresholdsIdle =
-                    mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle();
-            float[] screenDarkeningThresholdsIdle =
-                    mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle();
-            float[] screenBrighteningLevelsIdle =
-                    mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
-            float[] screenDarkeningLevelsIdle =
-                    mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
-            HysteresisLevels screenBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
-                    screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
-                    screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
-                    screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
-
-            long brighteningLightDebounce = mDisplayDeviceConfig
-                    .getAutoBrightnessBrighteningLightDebounce();
-            long darkeningLightDebounce = mDisplayDeviceConfig
-                    .getAutoBrightnessDarkeningLightDebounce();
-            long brighteningLightDebounceIdle = mDisplayDeviceConfig
-                    .getAutoBrightnessBrighteningLightDebounceIdle();
-            long darkeningLightDebounceIdle = mDisplayDeviceConfig
-                    .getAutoBrightnessDarkeningLightDebounceIdle();
-            boolean autoBrightnessResetAmbientLuxAfterWarmUp = context.getResources().getBoolean(
-                    R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
-
-            int lightSensorWarmUpTimeConfig = context.getResources().getInteger(
-                    R.integer.config_lightSensorWarmupTime);
-            int lightSensorRate = context.getResources().getInteger(
-                    R.integer.config_autoBrightnessLightSensorRate);
-            int initialLightSensorRate = context.getResources().getInteger(
-                    R.integer.config_autoBrightnessInitialLightSensorRate);
-            if (initialLightSensorRate == -1) {
-                initialLightSensorRate = lightSensorRate;
-            } else if (initialLightSensorRate > lightSensorRate) {
-                Slog.w(mTag, "Expected config_autoBrightnessInitialLightSensorRate ("
-                        + initialLightSensorRate + ") to be less than or equal to "
-                        + "config_autoBrightnessLightSensorRate (" + lightSensorRate + ").");
-            }
-
-            loadAmbientLightSensor();
-            // BrightnessTracker should only use one light sensor, we want to use the light sensor
-            // from the default display and not e.g. temporary displays when switching layouts.
-            if (mBrightnessTracker != null && mDisplayId == Display.DEFAULT_DISPLAY) {
-                mBrightnessTracker.setLightSensor(mLightSensor);
-            }
-
-            if (mAutomaticBrightnessController != null) {
-                mAutomaticBrightnessController.stop();
-            }
-            mAutomaticBrightnessController = mInjector.getAutomaticBrightnessController(
-                    this, handler.getLooper(), mSensorManager, mLightSensor,
-                    brightnessMappers, lightSensorWarmUpTimeConfig, PowerManager.BRIGHTNESS_MIN,
-                    PowerManager.BRIGHTNESS_MAX, dozeScaleFactor, lightSensorRate,
-                    initialLightSensorRate, brighteningLightDebounce, darkeningLightDebounce,
-                    brighteningLightDebounceIdle, darkeningLightDebounceIdle,
-                    autoBrightnessResetAmbientLuxAfterWarmUp, ambientBrightnessThresholds,
-                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
-                    screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
-                    mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
-                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits);
-            mDisplayBrightnessController.setAutomaticBrightnessController(
-                    mAutomaticBrightnessController);
-
-            mAutomaticBrightnessStrategy
-                    .setAutomaticBrightnessController(mAutomaticBrightnessController);
-            mBrightnessEventRingBuffer =
-                    new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX);
-
-            if (mScreenOffBrightnessSensorController != null) {
-                mScreenOffBrightnessSensorController.stop();
-                mScreenOffBrightnessSensorController = null;
-            }
-            loadScreenOffBrightnessSensor();
-            int[] sensorValueToLux = mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux();
-            if (mScreenOffBrightnessSensor != null && sensorValueToLux != null) {
-                mScreenOffBrightnessSensorController =
-                        mInjector.getScreenOffBrightnessSensorController(
-                                mSensorManager,
-                                mScreenOffBrightnessSensor,
-                                mHandler,
-                                SystemClock::uptimeMillis,
-                                sensorValueToLux,
-                                defaultModeBrightnessMapper);
-            }
-        } else {
-            mUseSoftwareAutoBrightnessConfig = false;
-        }
-    }
-
-    private void loadBrightnessRampRates() {
-        mBrightnessRampRateFastDecrease = mDisplayDeviceConfig.getBrightnessRampFastDecrease();
-        mBrightnessRampRateFastIncrease = mDisplayDeviceConfig.getBrightnessRampFastIncrease();
-        mBrightnessRampRateSlowDecrease = mDisplayDeviceConfig.getBrightnessRampSlowDecrease();
-        mBrightnessRampRateSlowIncrease = mDisplayDeviceConfig.getBrightnessRampSlowIncrease();
-        mBrightnessRampRateSlowDecreaseIdle =
-                mDisplayDeviceConfig.getBrightnessRampSlowDecreaseIdle();
-        mBrightnessRampRateSlowIncreaseIdle =
-                mDisplayDeviceConfig.getBrightnessRampSlowIncreaseIdle();
-        mBrightnessRampDecreaseMaxTimeMillis =
-                mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis();
-        mBrightnessRampIncreaseMaxTimeMillis =
-                mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis();
-        mBrightnessRampDecreaseMaxTimeIdleMillis =
-                mDisplayDeviceConfig.getBrightnessRampDecreaseMaxIdleMillis();
-        mBrightnessRampIncreaseMaxTimeIdleMillis =
-                mDisplayDeviceConfig.getBrightnessRampIncreaseMaxIdleMillis();
-    }
-
-    private void loadNitsRange(Resources resources) {
-        if (mDisplayDeviceConfig != null && mDisplayDeviceConfig.getNits() != null) {
-            mNitsRange = mDisplayDeviceConfig.getNits();
-        } else {
-            Slog.w(mTag, "Screen brightness nits configuration is unavailable; falling back");
-            mNitsRange = BrightnessMappingStrategy.getFloatArray(resources
-                    .obtainTypedArray(R.array.config_screenBrightnessNits));
-        }
-    }
-
-    private void reloadReduceBrightColours() {
-        if (mCdsi != null && mCdsi.isReduceBrightColorsActivated()) {
-            applyReduceBrightColorsSplineAdjustment();
-        }
-    }
-
-    @Override
-    public void setAutomaticScreenBrightnessMode(
-            @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
-        boolean isIdle = mode == AUTO_BRIGHTNESS_MODE_IDLE;
-        if (mAutomaticBrightnessController != null) {
-            mAutomaticBrightnessController.switchMode(mode);
-            setAnimatorRampSpeeds(isIdle);
-        }
-        Message msg = mHandler.obtainMessage();
-        msg.what = MSG_SET_DWBC_STRONG_MODE;
-        msg.arg1 = isIdle ? 1 : 0;
-        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-    }
-
-    private void setAnimatorRampSpeeds(boolean isIdle) {
-        if (mScreenBrightnessRampAnimator == null) {
-            return;
-        }
-        if (mFlags.isAdaptiveTone1Enabled() && isIdle) {
-            mScreenBrightnessRampAnimator.setAnimationTimeLimits(
-                    mBrightnessRampIncreaseMaxTimeIdleMillis,
-                    mBrightnessRampDecreaseMaxTimeIdleMillis);
-        } else {
-            mScreenBrightnessRampAnimator.setAnimationTimeLimits(
-                    mBrightnessRampIncreaseMaxTimeMillis,
-                    mBrightnessRampDecreaseMaxTimeMillis);
-        }
-    }
-
-    private final Animator.AnimatorListener mAnimatorListener = new Animator.AnimatorListener() {
-        @Override
-        public void onAnimationStart(Animator animation) {
-        }
-
-        @Override
-        public void onAnimationEnd(Animator animation) {
-            sendUpdatePowerState();
-        }
-
-        @Override
-        public void onAnimationRepeat(Animator animation) {
-        }
-
-        @Override
-        public void onAnimationCancel(Animator animation) {
-        }
-    };
-
-    private final RampAnimator.Listener mRampAnimatorListener = new RampAnimator.Listener() {
-        @Override
-        public void onAnimationEnd() {
-            sendUpdatePowerState();
-            Message msg = mHandler.obtainMessage(MSG_BRIGHTNESS_RAMP_DONE);
-            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-        }
-    };
-
-    /** Clean up all resources that are accessed via the {@link #mHandler} thread. */
-    private void cleanupHandlerThreadAfterStop() {
-        mDisplayPowerProximityStateController.cleanup();
-        mBrightnessRangeController.stop();
-        mBrightnessThrottler.stop();
-        mBrightnessClamperController.stop();
-        mHandler.removeCallbacksAndMessages(null);
-
-        // Release any outstanding wakelocks we're still holding because of pending messages.
-        mWakelockController.releaseAll();
-
-        final float brightness = mPowerState != null
-                ? mPowerState.getScreenBrightness()
-                : PowerManager.BRIGHTNESS_MIN;
-        reportStats(brightness);
-
-        if (mPowerState != null) {
-            mPowerState.stop();
-            mPowerState = null;
-        }
-
-        if (mScreenOffBrightnessSensorController != null) {
-            mScreenOffBrightnessSensorController.stop();
-        }
-
-        if (mDisplayWhiteBalanceController != null) {
-            mDisplayWhiteBalanceController.setEnabled(false);
-        }
-    }
-
-    // Call from handler thread
-    private void updatePowerState() {
-        Trace.traceBegin(Trace.TRACE_TAG_POWER,
-                "DisplayPowerController#updatePowerState");
-        updatePowerStateInternal();
-        Trace.traceEnd(Trace.TRACE_TAG_POWER);
-    }
-
-    private void updatePowerStateInternal() {
-        // Update the power state request.
-        final boolean mustNotify;
-        final int previousPolicy;
-        boolean mustInitialize = false;
-        mBrightnessReasonTemp.set(null);
-        mTempBrightnessEvent.reset();
-        SparseArray<DisplayPowerControllerInterface> displayBrightnessFollowers;
-        synchronized (mLock) {
-            if (mStopped) {
-                return;
-            }
-            mPendingUpdatePowerStateLocked = false;
-            if (mPendingRequestLocked == null) {
-                return; // wait until first actual power request
-            }
-
-            if (mPowerRequest == null) {
-                mPowerRequest = new DisplayPowerRequest(mPendingRequestLocked);
-                mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
-                mPendingRequestChangedLocked = false;
-                mustInitialize = true;
-                // Assume we're on and bright until told otherwise, since that's the state we turn
-                // on in.
-                previousPolicy = DisplayPowerRequest.POLICY_BRIGHT;
-            } else if (mPendingRequestChangedLocked) {
-                previousPolicy = mPowerRequest.policy;
-                mPowerRequest.copyFrom(mPendingRequestLocked);
-                mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
-                mPendingRequestChangedLocked = false;
-                mDisplayReadyLocked = false;
-            } else {
-                previousPolicy = mPowerRequest.policy;
-            }
-
-            mustNotify = !mDisplayReadyLocked;
-
-            displayBrightnessFollowers = mDisplayBrightnessFollowers.clone();
-        }
-
-        int state = mDisplayStateController
-                .updateDisplayState(mPowerRequest, mIsEnabled, mIsInTransition);
-
-        // Initialize things the first time the power state is changed.
-        if (mustInitialize) {
-            initialize(readyToUpdateDisplayState() ? state : Display.STATE_UNKNOWN);
-        }
-
-        // Animate the screen state change unless already animating.
-        // The transition may be deferred, so after this point we will use the
-        // actual state instead of the desired one.
-        animateScreenStateChange(state, mDisplayStateController.shouldPerformScreenOffTransition());
-        state = mPowerState.getScreenState();
-
-        // Switch to doze auto-brightness mode if needed
-        if (mFlags.areAutoBrightnessModesEnabled() && mAutomaticBrightnessController != null
-                && !mAutomaticBrightnessController.isInIdleMode()) {
-            setAutomaticScreenBrightnessMode(Display.isDozeState(state)
-                    ? AUTO_BRIGHTNESS_MODE_DOZE : AUTO_BRIGHTNESS_MODE_DEFAULT);
-        }
-
-        final boolean userSetBrightnessChanged = mDisplayBrightnessController
-                .updateUserSetScreenBrightness();
-
-        DisplayBrightnessState displayBrightnessState = mDisplayBrightnessController
-                .updateBrightness(mPowerRequest, state);
-        float brightnessState = displayBrightnessState.getBrightness();
-        float rawBrightnessState = displayBrightnessState.getBrightness();
-        mBrightnessReasonTemp.set(displayBrightnessState.getBrightnessReason());
-        boolean slowChange = displayBrightnessState.isSlowChange();
-        // custom transition duration
-        float customAnimationRate = displayBrightnessState.getCustomAnimationRate();
-
-        // Set up the ScreenOff controller used when coming out of SCREEN_OFF and the ALS sensor
-        // doesn't yet have a valid lux value to use with auto-brightness.
-        if (mScreenOffBrightnessSensorController != null) {
-            mScreenOffBrightnessSensorController
-                    .setLightSensorEnabled(displayBrightnessState.getShouldUseAutoBrightness()
-                    && mIsEnabled && (state == Display.STATE_OFF
-                    || (state == Display.STATE_DOZE
-                    && !mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()))
-                    && mLeadDisplayId == Layout.NO_LEAD_DISPLAY);
-        }
-
-        // Take note if the short term model was already active before applying the current
-        // request changes.
-        final boolean wasShortTermModelActive =
-                mAutomaticBrightnessStrategy.isShortTermModelActive();
-        mAutomaticBrightnessStrategy.setAutoBrightnessState(state,
-                mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig(),
-                mBrightnessReasonTemp.getReason(), mPowerRequest.policy,
-                mDisplayBrightnessController.getLastUserSetScreenBrightness(),
-                userSetBrightnessChanged);
-
-        // If the brightness is already set then it's been overridden by something other than the
-        // user, or is a temporary adjustment.
-        boolean userInitiatedChange = (Float.isNaN(brightnessState))
-                && (mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged()
-                || userSetBrightnessChanged);
-
-        mBrightnessRangeController.setAutoBrightnessEnabled(
-                mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()
-                ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
-                : mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff()
-                        ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE
-                        : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
-
-        boolean updateScreenBrightnessSetting =
-                displayBrightnessState.shouldUpdateScreenBrightnessSetting();
-        float currentBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness();
-        // Apply auto-brightness.
-        int brightnessAdjustmentFlags = 0;
-        if (Float.isNaN(brightnessState)) {
-            if (mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()) {
-                brightnessState = mAutomaticBrightnessStrategy.getAutomaticScreenBrightness(
-                        mTempBrightnessEvent);
-                if (BrightnessUtils.isValidBrightnessValue(brightnessState)
-                        || brightnessState == PowerManager.BRIGHTNESS_OFF_FLOAT) {
-                    rawBrightnessState = mAutomaticBrightnessController
-                            .getRawAutomaticScreenBrightness();
-                    brightnessState = clampScreenBrightness(brightnessState);
-                    // slowly adapt to auto-brightness
-                    // TODO(b/253226419): slowChange should be decided by strategy.updateBrightness
-                    slowChange = mAutomaticBrightnessStrategy.hasAppliedAutoBrightness()
-                            && !mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged();
-                    brightnessAdjustmentFlags =
-                            mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags();
-                    updateScreenBrightnessSetting = currentBrightnessSetting != brightnessState;
-                    mAutomaticBrightnessStrategy.setAutoBrightnessApplied(true);
-                    mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC);
-                    if (mScreenOffBrightnessSensorController != null) {
-                        mScreenOffBrightnessSensorController.setLightSensorEnabled(false);
-                    }
-                } else {
-                    mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
-                }
-            }
-        } else {
-            // Any non-auto-brightness values such as override or temporary should still be subject
-            // to clamping so that they don't go beyond the current max as specified by HBM
-            // Controller.
-            brightnessState = clampScreenBrightness(brightnessState);
-            mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
-        }
-
-        // Use default brightness when dozing unless overridden.
-        if ((Float.isNaN(brightnessState))
-                && Display.isDozeState(state)) {
-            rawBrightnessState = mScreenBrightnessDozeConfig;
-            brightnessState = clampScreenBrightness(rawBrightnessState);
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT);
-        }
-
-        // The ALS is not available yet - use the screen off sensor to determine the initial
-        // brightness
-        if (Float.isNaN(brightnessState) && mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()
-                && mScreenOffBrightnessSensorController != null) {
-            rawBrightnessState =
-                    mScreenOffBrightnessSensorController.getAutomaticScreenBrightness();
-            brightnessState = rawBrightnessState;
-            if (BrightnessUtils.isValidBrightnessValue(brightnessState)) {
-                brightnessState = clampScreenBrightness(brightnessState);
-                updateScreenBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness()
-                        != brightnessState;
-                mBrightnessReasonTemp.setReason(
-                        BrightnessReason.REASON_SCREEN_OFF_BRIGHTNESS_SENSOR);
-            }
-        }
-
-        // Apply manual brightness.
-        if (Float.isNaN(brightnessState)) {
-            rawBrightnessState = currentBrightnessSetting;
-            brightnessState = clampScreenBrightness(rawBrightnessState);
-            if (brightnessState != currentBrightnessSetting) {
-                // The manually chosen screen brightness is outside of the currently allowed
-                // range (i.e., high-brightness-mode), make sure we tell the rest of the system
-                // by updating the setting.
-                updateScreenBrightnessSetting = true;
-            }
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_MANUAL);
-        }
-
-        float ambientLux = mAutomaticBrightnessController == null ? 0
-                : mAutomaticBrightnessController.getAmbientLux();
-        for (int i = 0; i < displayBrightnessFollowers.size(); i++) {
-            DisplayPowerControllerInterface follower = displayBrightnessFollowers.valueAt(i);
-            follower.setBrightnessToFollow(rawBrightnessState,
-                    mDisplayBrightnessController.convertToNits(rawBrightnessState),
-                    ambientLux, slowChange);
-        }
-
-        // Now that a desired brightness has been calculated, apply brightness throttling. The
-        // dimming and low power transformations that follow can only dim brightness further.
-        //
-        // We didn't do this earlier through brightness clamping because we need to know both
-        // unthrottled (unclamped/ideal) and throttled brightness levels for subsequent operations.
-        // Note throttling effectively changes the allowed brightness range, so, similarly to HBM,
-        // we broadcast this change through setting.
-        final float unthrottledBrightnessState = brightnessState;
-        DisplayBrightnessState clampedState = mBrightnessClamperController.clamp(mPowerRequest,
-                brightnessState, slowChange);
-
-        brightnessState = clampedState.getBrightness();
-        slowChange = clampedState.isSlowChange();
-        // faster rate wins, at this point customAnimationRate == -1, strategy does not control
-        // customAnimationRate. Should be revisited if strategy start setting this value
-        customAnimationRate = Math.max(customAnimationRate, clampedState.getCustomAnimationRate());
-        mBrightnessReasonTemp.addModifier(clampedState.getBrightnessReason().getModifier());
-
-        if (updateScreenBrightnessSetting) {
-            // Tell the rest of the system about the new brightness in case we had to change it
-            // for things like auto-brightness or high-brightness-mode. Note that we do this
-            // only considering maxBrightness (ignoring brightness modifiers like low power or dim)
-            // so that the slider accurately represents the full possible range,
-            // even if they range changes what it means in absolute terms.
-            mDisplayBrightnessController.updateScreenBrightnessSetting(
-                    MathUtils.constrain(unthrottledBrightnessState,
-                            clampedState.getMinBrightness(), clampedState.getMaxBrightness()));
-        }
-
-        // The current brightness to use has been calculated at this point, and HbmController should
-        // be notified so that it can accurately calculate HDR or HBM levels. We specifically do it
-        // here instead of having HbmController listen to the brightness setting because certain
-        // brightness sources (such as an app override) are not saved to the setting, but should be
-        // reflected in HBM calculations.
-        mBrightnessRangeController.onBrightnessChanged(brightnessState, unthrottledBrightnessState,
-                mBrightnessClamperController.getBrightnessMaxReason());
-
-        // Animate the screen brightness when the screen is on or dozing.
-        // Skip the animation when the screen is off or suspended.
-        boolean brightnessAdjusted = false;
-        final boolean brightnessIsTemporary =
-                (mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_TEMPORARY)
-                        || mAutomaticBrightnessStrategy
-                        .isTemporaryAutoBrightnessAdjustmentApplied();
-        if (!mPendingScreenOff) {
-            if (mSkipScreenOnBrightnessRamp) {
-                if (state == Display.STATE_ON) {
-                    if (mSkipRampState == RAMP_STATE_SKIP_NONE && mDozing) {
-                        mInitialAutoBrightness = brightnessState;
-                        mSkipRampState = RAMP_STATE_SKIP_INITIAL;
-                    } else if (mSkipRampState == RAMP_STATE_SKIP_INITIAL
-                            && mUseSoftwareAutoBrightnessConfig
-                            && !BrightnessSynchronizer.floatEquals(brightnessState,
-                            mInitialAutoBrightness)) {
-                        mSkipRampState = RAMP_STATE_SKIP_AUTOBRIGHT;
-                    } else if (mSkipRampState == RAMP_STATE_SKIP_AUTOBRIGHT) {
-                        mSkipRampState = RAMP_STATE_SKIP_NONE;
-                    }
-                } else {
-                    mSkipRampState = RAMP_STATE_SKIP_NONE;
-                }
-            }
-
-            final boolean initialRampSkip = (state == Display.STATE_ON && mSkipRampState
-                    != RAMP_STATE_SKIP_NONE) || mDisplayPowerProximityStateController
-                    .shouldSkipRampBecauseOfProximityChangeToNegative();
-            // While dozing, sometimes the brightness is split into buckets. Rather than animating
-            // through the buckets, which is unlikely to be smooth in the first place, just jump
-            // right to the suggested brightness.
-            final boolean hasBrightnessBuckets =
-                    Display.isDozeState(state) && mBrightnessBucketsInDozeConfig;
-            // If the color fade is totally covering the screen then we can change the backlight
-            // level without it being a noticeable jump since any actual content isn't yet visible.
-            final boolean isDisplayContentVisible =
-                    mColorFadeEnabled && mPowerState.getColorFadeLevel() == 1.0f;
-            // We only want to animate the brightness if it is between 0.0f and 1.0f.
-            // brightnessState can contain the values -1.0f and NaN, which we do not want to
-            // animate to. To avoid this, we check the value first.
-            // If the brightnessState is off (-1.0f) we still want to animate to the minimum
-            // brightness (0.0f) to accommodate for LED displays, which can appear bright to the
-            // user even when the display is all black. We also clamp here in case some
-            // transformations to the brightness have pushed it outside of the currently
-            // allowed range.
-            float animateValue = clampScreenBrightness(brightnessState);
-
-            // If there are any HDR layers on the screen, we have a special brightness value that we
-            // use instead. We still preserve the calculated brightness for Standard Dynamic Range
-            // (SDR) layers, but the main brightness value will be the one for HDR.
-            float sdrAnimateValue = animateValue;
-            // TODO(b/216365040): The decision to prevent HBM for HDR in low power mode should be
-            // done in HighBrightnessModeController.
-            if (mBrightnessRangeController.getHighBrightnessMode()
-                    == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR
-                    && (mBrightnessReasonTemp.getModifier() & BrightnessReason.MODIFIER_DIMMED) == 0
-                    && (mBrightnessReasonTemp.getModifier() & BrightnessReason.MODIFIER_LOW_POWER)
-                    == 0) {
-                // We want to scale HDR brightness level with the SDR level, we also need to restore
-                // SDR brightness immediately when entering dim or low power mode.
-                animateValue = mBrightnessRangeController.getHdrBrightnessValue();
-                customAnimationRate = Math.max(customAnimationRate,
-                        mBrightnessRangeController.getHdrTransitionRate());
-                mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_HDR);
-            }
-
-            // if doze or suspend state is requested, we want to finish brightnes animation fast
-            // to allow state animation to start
-            if (mPowerRequest.policy == DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE
-                    && (mPowerRequest.dozeScreenState == Display.STATE_UNKNOWN  // dozing
-                    || mPowerRequest.dozeScreenState == Display.STATE_DOZE_SUSPEND
-                    || mPowerRequest.dozeScreenState == Display.STATE_ON_SUSPEND)) {
-                customAnimationRate = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET;
-                slowChange = false;
-            }
-
-            final float currentBrightness = mPowerState.getScreenBrightness();
-            final float currentSdrBrightness = mPowerState.getSdrScreenBrightness();
-
-            if (BrightnessUtils.isValidBrightnessValue(animateValue)
-                    && (animateValue != currentBrightness
-                    || sdrAnimateValue != currentSdrBrightness)) {
-                boolean skipAnimation = initialRampSkip || hasBrightnessBuckets
-                        || !isDisplayContentVisible || brightnessIsTemporary;
-                final boolean isHdrOnlyChange = BrightnessSynchronizer.floatEquals(
-                        sdrAnimateValue, currentSdrBrightness);
-                if (mFlags.isFastHdrTransitionsEnabled() && !skipAnimation && isHdrOnlyChange) {
-                    // SDR brightness is unchanged, so animate quickly as this is only impacting
-                    // a likely minority amount of display content
-                    // ie, the highlights of an HDR video or UltraHDR image
-                    slowChange = false;
-
-                    // Going from HDR to no HDR; visually this should be a "no-op" anyway
-                    // as the remaining SDR content's brightness should be holding steady
-                    // due to the sdr brightness not shifting
-                    if (BrightnessSynchronizer.floatEquals(sdrAnimateValue, animateValue)) {
-                        skipAnimation = true;
-                    }
-
-                    // Going from no HDR to HDR; visually this is a significant scene change
-                    // and the animation just prevents advanced clients from doing their own
-                    // handling of enter/exit animations if they would like to do such a thing
-                    if (BrightnessSynchronizer.floatEquals(sdrAnimateValue, currentBrightness)) {
-                        skipAnimation = true;
-                    }
-                }
-                if (skipAnimation) {
-                    animateScreenBrightness(animateValue, sdrAnimateValue,
-                            SCREEN_ANIMATION_RATE_MINIMUM);
-                } else if (customAnimationRate > 0) {
-                    animateScreenBrightness(animateValue, sdrAnimateValue,
-                            customAnimationRate, /* ignoreAnimationLimits = */true);
-                } else {
-                    boolean isIncreasing = animateValue > currentBrightness;
-                    final float rampSpeed;
-                    final boolean idle = mAutomaticBrightnessController != null
-                            && mAutomaticBrightnessController.isInIdleMode();
-                    if (isIncreasing && slowChange) {
-                        rampSpeed = idle ? mBrightnessRampRateSlowIncreaseIdle
-                                : mBrightnessRampRateSlowIncrease;
-                    } else if (isIncreasing && !slowChange) {
-                        rampSpeed = mBrightnessRampRateFastIncrease;
-                    } else if (!isIncreasing && slowChange) {
-                        rampSpeed = idle ? mBrightnessRampRateSlowDecreaseIdle
-                                : mBrightnessRampRateSlowDecrease;
-                    } else {
-                        rampSpeed = mBrightnessRampRateFastDecrease;
-                    }
-                    animateScreenBrightness(animateValue, sdrAnimateValue, rampSpeed);
-                }
-            }
-
-            notifyBrightnessTrackerChanged(brightnessState, userInitiatedChange,
-                    wasShortTermModelActive, mAutomaticBrightnessStrategy.isAutoBrightnessEnabled(),
-                    brightnessIsTemporary, displayBrightnessState.getShouldUseAutoBrightness());
-
-            // We save the brightness info *after* the brightness setting has been changed and
-            // adjustments made so that the brightness info reflects the latest value.
-            brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting(),
-                    animateValue, clampedState);
-        } else {
-            brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting(), clampedState);
-        }
-
-        // Only notify if the brightness adjustment is not temporary (i.e. slider has been released)
-        if (brightnessAdjusted && !brightnessIsTemporary) {
-            postBrightnessChangeRunnable();
-        }
-
-        // Log any changes to what is currently driving the brightness setting.
-        if (!mBrightnessReasonTemp.equals(mBrightnessReason) || brightnessAdjustmentFlags != 0) {
-            Slog.v(mTag, "Brightness [" + brightnessState + "] reason changing to: '"
-                    + mBrightnessReasonTemp.toString(brightnessAdjustmentFlags)
-                    + "', previous reason: '" + mBrightnessReason + "'.");
-            mBrightnessReason.set(mBrightnessReasonTemp);
-        } else if (mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_MANUAL
-                && userSetBrightnessChanged) {
-            Slog.v(mTag, "Brightness [" + brightnessState + "] manual adjustment.");
-        }
-
-
-        // Log brightness events when a detail of significance has changed. Generally this is the
-        // brightness itself changing, but also includes data like HBM cap, thermal throttling
-        // brightness cap, RBC state, etc.
-        mTempBrightnessEvent.setTime(System.currentTimeMillis());
-        mTempBrightnessEvent.setBrightness(brightnessState);
-        mTempBrightnessEvent.setPhysicalDisplayId(mUniqueDisplayId);
-        mTempBrightnessEvent.setReason(mBrightnessReason);
-        mTempBrightnessEvent.setHbmMax(mBrightnessRangeController.getCurrentBrightnessMax());
-        mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode());
-        mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags()
-                | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0)
-                | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0));
-        mTempBrightnessEvent.setRbcStrength(mCdsi != null
-                ? mCdsi.getReduceBrightColorsStrength() : -1);
-        mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor);
-        mTempBrightnessEvent.setWasShortTermModelActive(wasShortTermModelActive);
-        mTempBrightnessEvent.setDisplayBrightnessStrategyName(displayBrightnessState
-                .getDisplayBrightnessStrategyName());
-        mTempBrightnessEvent.setAutomaticBrightnessEnabled(
-                displayBrightnessState.getShouldUseAutoBrightness());
-        // Temporary is what we use during slider interactions. We avoid logging those so that
-        // we don't spam logcat when the slider is being used.
-        boolean tempToTempTransition =
-                mTempBrightnessEvent.getReason().getReason() == BrightnessReason.REASON_TEMPORARY
-                        && mLastBrightnessEvent.getReason().getReason()
-                        == BrightnessReason.REASON_TEMPORARY;
-        // Purely for dumpsys;
-        final boolean isRbcEvent =
-                mLastBrightnessEvent.isRbcEnabled() != mTempBrightnessEvent.isRbcEnabled();
-
-        if ((!mTempBrightnessEvent.equalsMainData(mLastBrightnessEvent) && !tempToTempTransition)
-                || brightnessAdjustmentFlags != 0) {
-            mTempBrightnessEvent.setInitialBrightness(mLastBrightnessEvent.getBrightness());
-            mLastBrightnessEvent.copyFrom(mTempBrightnessEvent);
-            BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent);
-            // Adjustment flags (and user-set flag) only get added after the equality checks since
-            // they are transient.
-            newEvent.setAdjustmentFlags(brightnessAdjustmentFlags);
-            newEvent.setFlags(newEvent.getFlags() | (userSetBrightnessChanged
-                    ? BrightnessEvent.FLAG_USER_SET : 0));
-            Slog.i(mTag, newEvent.toString(/* includeTime= */ false));
-
-            if (userSetBrightnessChanged
-                    || newEvent.getReason().getReason() != BrightnessReason.REASON_TEMPORARY) {
-                logBrightnessEvent(newEvent, unthrottledBrightnessState);
-            }
-            if (mBrightnessEventRingBuffer != null) {
-                mBrightnessEventRingBuffer.append(newEvent);
-            }
-            if (isRbcEvent) {
-                mRbcEventRingBuffer.append(newEvent);
-            }
-
-        }
-
-        // Update display white-balance.
-        if (mDisplayWhiteBalanceController != null) {
-            if (state == Display.STATE_ON && mDisplayWhiteBalanceSettings.isEnabled()) {
-                mDisplayWhiteBalanceController.setEnabled(true);
-                mDisplayWhiteBalanceController.updateDisplayColorTemperature();
-            } else {
-                mDisplayWhiteBalanceController.setEnabled(false);
-            }
-        }
-
-        // Determine whether the display is ready for use in the newly requested state.
-        // Note that we do not wait for the brightness ramp animation to complete before
-        // 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);
-        final boolean finished = ready
-                && !mScreenBrightnessRampAnimator.isAnimating();
-
-        // Notify policy about screen turned on.
-        if (ready && state != Display.STATE_OFF
-                && mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_TURNING_ON) {
-            setReportedScreenState(REPORTED_TO_POLICY_SCREEN_ON);
-            mWindowManagerPolicy.screenTurnedOn(mDisplayId);
-        }
-
-        // Grab a wake lock if we have unfinished business.
-        if (!finished) {
-            mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
-        }
-
-        // Notify the power manager when ready.
-        if (ready && mustNotify) {
-            // Send state change.
-            synchronized (mLock) {
-                if (!mPendingRequestChangedLocked) {
-                    mDisplayReadyLocked = true;
-
-                    if (DEBUG) {
-                        Slog.d(mTag, "Display ready!");
-                    }
-                }
-            }
-            sendOnStateChangedWithWakelock();
-        }
-
-        // Release the wake lock when we have no unfinished business.
-        if (finished) {
-            mWakelockController.releaseWakelock(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
-        }
-
-        // Record if dozing for future comparison.
-        mDozing = state != Display.STATE_ON;
-
-        if (previousPolicy != mPowerRequest.policy) {
-            logDisplayPolicyChanged(mPowerRequest.policy);
-        }
-    }
-
-    private void setDwbcOverride(float cct) {
-        if (mDisplayWhiteBalanceController != null) {
-            mDisplayWhiteBalanceController.setAmbientColorTemperatureOverride(cct);
-            // The ambient color temperature override is only applied when the ambient color
-            // temperature changes or is updated, so it doesn't necessarily change the screen color
-            // temperature immediately. So, let's make it!
-            // We can call this directly, since we're already on the handler thread.
-            updatePowerState();
-        }
-    }
-
-    private void setDwbcStrongMode(int arg) {
-        if (mDisplayWhiteBalanceController != null) {
-            final boolean isIdle = (arg == 1);
-            mDisplayWhiteBalanceController.setStrongModeEnabled(isIdle);
-        }
-    }
-
-    private void setDwbcLoggingEnabled(int arg) {
-        if (mDisplayWhiteBalanceController != null) {
-            final boolean enabled = (arg == 1);
-            mDisplayWhiteBalanceController.setLoggingEnabled(enabled);
-            mDisplayWhiteBalanceSettings.setLoggingEnabled(enabled);
-        }
-    }
-
-    @Override
-    public void updateBrightness() {
-        sendUpdatePowerState();
-    }
-
-    /**
-     * Ignores the proximity sensor until the sensor state changes, but only if the sensor is
-     * currently enabled and forcing the screen to be dark.
-     */
-    @Override
-    public void ignoreProximitySensorUntilChanged() {
-        mDisplayPowerProximityStateController.ignoreProximitySensorUntilChanged();
-    }
-
-    @Override
-    public void setBrightnessConfiguration(BrightnessConfiguration c,
-            boolean shouldResetShortTermModel) {
-        Message msg = mHandler.obtainMessage(MSG_CONFIGURE_BRIGHTNESS,
-                shouldResetShortTermModel ? 1 : 0, /* unused */ 0, c);
-        msg.sendToTarget();
-    }
-
-    @Override
-    public void setTemporaryBrightness(float brightness) {
-        Message msg = mHandler.obtainMessage(MSG_SET_TEMPORARY_BRIGHTNESS,
-                Float.floatToIntBits(brightness), 0 /*unused*/);
-        msg.sendToTarget();
-    }
-
-    @Override
-    public void setTemporaryAutoBrightnessAdjustment(float adjustment) {
-        Message msg = mHandler.obtainMessage(MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT,
-                Float.floatToIntBits(adjustment), 0 /*unused*/);
-        msg.sendToTarget();
-    }
-
-    @Override
-    public void setBrightnessFromOffload(float brightness) {
-        Message msg = mHandler.obtainMessage(MSG_SET_BRIGHTNESS_FROM_OFFLOAD,
-                Float.floatToIntBits(brightness), 0 /*unused*/);
-        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-    }
-
-    @Override
-    public float[] getAutoBrightnessLevels(
-            @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
-        int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
-                Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL, UserHandle.USER_CURRENT);
-        return mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(mode, preset);
-    }
-
-    @Override
-    public float[] getAutoBrightnessLuxLevels(
-            @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
-        int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
-                Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL, UserHandle.USER_CURRENT);
-        return mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(mode, preset);
-    }
-
-    @Override
-    public BrightnessInfo getBrightnessInfo() {
-        synchronized (mCachedBrightnessInfo) {
-            return new BrightnessInfo(
-                    mCachedBrightnessInfo.brightness.value,
-                    mCachedBrightnessInfo.adjustedBrightness.value,
-                    mCachedBrightnessInfo.brightnessMin.value,
-                    mCachedBrightnessInfo.brightnessMax.value,
-                    mCachedBrightnessInfo.hbmMode.value,
-                    mCachedBrightnessInfo.hbmTransitionPoint.value,
-                    mCachedBrightnessInfo.brightnessMaxReason.value);
-        }
-    }
-
-    @Override
-    public void onBootCompleted() {
-        Message msg = mHandler.obtainMessage(MSG_BOOT_COMPLETED);
-        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-    }
-
-    private boolean saveBrightnessInfo(float brightness) {
-        return saveBrightnessInfo(brightness, /* state= */ null);
-    }
-
-    private boolean saveBrightnessInfo(float brightness, @Nullable DisplayBrightnessState state) {
-        return saveBrightnessInfo(brightness, brightness, state);
-    }
-
-    private boolean saveBrightnessInfo(float brightness, float adjustedBrightness,
-            @Nullable DisplayBrightnessState state) {
-        synchronized (mCachedBrightnessInfo) {
-            float stateMax = state != null ? state.getMaxBrightness() : PowerManager.BRIGHTNESS_MAX;
-            float stateMin = state != null ? state.getMinBrightness() : PowerManager.BRIGHTNESS_MAX;
-            final float minBrightness = Math.max(stateMin, Math.min(
-                    mBrightnessRangeController.getCurrentBrightnessMin(), stateMax));
-            final float maxBrightness = Math.min(
-                    mBrightnessRangeController.getCurrentBrightnessMax(), stateMax);
-            boolean changed = false;
-
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.brightness,
-                            brightness);
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.adjustedBrightness,
-                            adjustedBrightness);
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.brightnessMin,
-                            minBrightness);
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.brightnessMax,
-                            maxBrightness);
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.hbmMode,
-                            mBrightnessRangeController.getHighBrightnessMode());
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.hbmTransitionPoint,
-                            mBrightnessRangeController.getTransitionPoint());
-            changed |=
-                    mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason,
-                            mBrightnessClamperController.getBrightnessMaxReason());
-            return changed;
-        }
-    }
-
-    void postBrightnessChangeRunnable() {
-        if (!mHandler.hasCallbacks(mOnBrightnessChangeRunnable)) {
-            mHandler.post(mOnBrightnessChangeRunnable);
-        }
-    }
-
-    private HighBrightnessModeController createHbmControllerLocked(
-            HighBrightnessModeMetadata hbmMetadata, Runnable modeChangeCallback) {
-        final DisplayDeviceConfig ddConfig = mDisplayDevice.getDisplayDeviceConfig();
-        final IBinder displayToken = mDisplayDevice.getDisplayTokenLocked();
-        final String displayUniqueId = mDisplayDevice.getUniqueId();
-        final DisplayDeviceConfig.HighBrightnessModeData hbmData =
-                ddConfig != null ? ddConfig.getHighBrightnessModeData() : null;
-        final DisplayDeviceInfo info = mDisplayDevice.getDisplayDeviceInfoLocked();
-        return mInjector.getHighBrightnessModeController(mHandler, info.width, info.height,
-                displayToken, displayUniqueId, PowerManager.BRIGHTNESS_MIN,
-                PowerManager.BRIGHTNESS_MAX, hbmData, (sdrBrightness, maxDesiredHdrSdrRatio) ->
-                        mDisplayDeviceConfig.getHdrBrightnessFromSdr(sdrBrightness,
-                                maxDesiredHdrSdrRatio), modeChangeCallback, hbmMetadata, mContext);
-    }
-
-    private BrightnessThrottler createBrightnessThrottlerLocked() {
-        final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
-        final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig();
-        return new BrightnessThrottler(mHandler,
-                () -> {
-                    sendUpdatePowerState();
-                    postBrightnessChangeRunnable();
-                }, mUniqueDisplayId,
-                mLogicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId,
-                ddConfig.getThermalBrightnessThrottlingDataMapByThrottlingId());
-    }
-
-    private void blockScreenOn() {
-        if (mPendingScreenOnUnblocker == null) {
-            Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_TRACE_NAME, 0);
-            mPendingScreenOnUnblocker = new ScreenOnUnblocker();
-            mScreenOnBlockStartRealTime = SystemClock.elapsedRealtime();
-            Slog.i(mTag, "Blocking screen on until initial contents have been drawn.");
-        }
-    }
-
-    private void unblockScreenOn() {
-        if (mPendingScreenOnUnblocker != null) {
-            mPendingScreenOnUnblocker = null;
-            long delay = SystemClock.elapsedRealtime() - mScreenOnBlockStartRealTime;
-            Slog.i(mTag, "Unblocked screen on after " + delay + " ms");
-            Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_TRACE_NAME, 0);
-        }
-    }
-
-    private void blockScreenOff() {
-        if (mPendingScreenOffUnblocker == null) {
-            Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, SCREEN_OFF_BLOCKED_TRACE_NAME, 0);
-            mPendingScreenOffUnblocker = new ScreenOffUnblocker();
-            mScreenOffBlockStartRealTime = SystemClock.elapsedRealtime();
-            Slog.i(mTag, "Blocking screen off");
-        }
-    }
-
-    private void unblockScreenOff() {
-        if (mPendingScreenOffUnblocker != null) {
-            mPendingScreenOffUnblocker = null;
-            long delay = SystemClock.elapsedRealtime() - mScreenOffBlockStartRealTime;
-            Slog.i(mTag, "Unblocked screen off after " + delay + " ms");
-            Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, SCREEN_OFF_BLOCKED_TRACE_NAME, 0);
-        }
-    }
-
-    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 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()) {
-                if (mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_ON
-                        || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) {
-                    setReportedScreenState(REPORTED_TO_POLICY_SCREEN_TURNING_OFF);
-                    blockScreenOff();
-                    mWindowManagerPolicy.screenTurningOff(mDisplayId, mPendingScreenOffUnblocker);
-                    unblockScreenOff();
-                } else if (mPendingScreenOffUnblocker != null) {
-                    // Abort doing the state change until screen off is unblocked.
-                    return false;
-                }
-            }
-
-            if (!reportOnly && changed && readyToUpdateDisplayState()
-                    && mPendingScreenOffUnblocker == null
-                    && mPendingScreenOnUnblockerByDisplayOffload == null) {
-                Trace.traceCounter(Trace.TRACE_TAG_POWER, "ScreenState", state);
-
-                String propertyKey = "debug.tracing.screen_state";
-                String propertyValue = String.valueOf(state);
-                try {
-                    // TODO(b/153319140) remove when we can get this from the above trace invocation
-                    SystemProperties.set(propertyKey, propertyValue);
-                } catch (RuntimeException e) {
-                    Slog.e(mTag, "Failed to set a system property: key=" + propertyKey
-                            + " value=" + propertyValue + " " + e.getMessage());
-                }
-
-                mPowerState.setScreenState(state);
-                // Tell battery stats about the transition.
-                noteScreenState(state);
-            }
-        }
-
-        // Tell the window manager policy when the screen is turned off or on unless it's due
-        // to the proximity sensor.  We temporarily block turning the screen on until the
-        // window manager is ready by leaving a black surface covering the screen.
-        // This surface is essentially the final state of the color fade animation and
-        // it is only removed once the window manager tells us that the activity has
-        // finished drawing underneath.
-        if (isOff && mReportedScreenStateToPolicy != REPORTED_TO_POLICY_SCREEN_OFF
-                && !mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
-            setReportedScreenState(REPORTED_TO_POLICY_SCREEN_OFF);
-            unblockScreenOn();
-            mWindowManagerPolicy.screenTurnedOff(mDisplayId, mIsInTransition);
-        } else if (!isOff
-                && mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_TURNING_OFF) {
-
-            // We told policy already that screen was turning off, but now we changed our minds.
-            // Complete the full state transition on -> turningOff -> off.
-            unblockScreenOff();
-            mWindowManagerPolicy.screenTurnedOff(mDisplayId, mIsInTransition);
-            setReportedScreenState(REPORTED_TO_POLICY_SCREEN_OFF);
-        }
-        if (!isOff
-                && (mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_OFF
-                || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED)) {
-            setReportedScreenState(REPORTED_TO_POLICY_SCREEN_TURNING_ON);
-            if (mPowerState.getColorFadeLevel() == 0.0f) {
-                blockScreenOn();
-            } else {
-                unblockScreenOn();
-            }
-            mWindowManagerPolicy.screenTurningOn(mDisplayId, mPendingScreenOnUnblocker);
-        }
-
-        // Return true if the screen isn't blocked.
-        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() {
-        final int fallbackType = mDisplayId == Display.DEFAULT_DISPLAY
-                ? Sensor.TYPE_LIGHT : SensorUtils.NO_FALLBACK;
-        mLightSensor = SensorUtils.findSensor(mSensorManager,
-                mDisplayDeviceConfig.getAmbientLightSensor(), fallbackType);
-    }
-
-    private void loadScreenOffBrightnessSensor() {
-        mScreenOffBrightnessSensor = SensorUtils.findSensor(mSensorManager,
-                mDisplayDeviceConfig.getScreenOffBrightnessSensor(), SensorUtils.NO_FALLBACK);
-    }
-
-    private float clampScreenBrightness(float value) {
-        if (Float.isNaN(value)) {
-            value = PowerManager.BRIGHTNESS_MIN;
-        }
-        return MathUtils.constrain(value, mBrightnessRangeController.getCurrentBrightnessMin(),
-                mBrightnessRangeController.getCurrentBrightnessMax());
-    }
-
-    private void animateScreenBrightness(float target, float sdrTarget, float rate) {
-        animateScreenBrightness(target, sdrTarget, rate, /* ignoreAnimationLimits = */false);
-    }
-
-    private void animateScreenBrightness(float target, float sdrTarget, float rate,
-            boolean ignoreAnimationLimits) {
-        if (DEBUG) {
-            Slog.d(mTag, "Animating brightness: target=" + target + ", sdrTarget=" + sdrTarget
-                    + ", rate=" + rate);
-        }
-        if (mScreenBrightnessRampAnimator.animateTo(target, sdrTarget, rate,
-                ignoreAnimationLimits)) {
-            Trace.traceCounter(Trace.TRACE_TAG_POWER, "TargetScreenBrightness", (int) target);
-
-            String propertyKey = "debug.tracing.screen_brightness";
-            String propertyValue = String.valueOf(target);
-            try {
-                // TODO(b/153319140) remove when we can get this from the above trace invocation
-                SystemProperties.set(propertyKey, propertyValue);
-            } catch (RuntimeException e) {
-                Slog.e(mTag, "Failed to set a system property: key=" + propertyKey
-                        + " value=" + propertyValue + " " + e.getMessage());
-            }
-
-            noteScreenBrightness(target);
-        }
-    }
-
-    private void animateScreenStateChange(int target, boolean performScreenOffTransition) {
-        // If there is already an animation in progress, don't interfere with it.
-        if (mColorFadeEnabled
-                && (mColorFadeOnAnimator.isStarted() || mColorFadeOffAnimator.isStarted())) {
-            if (target != Display.STATE_ON) {
-                return;
-            }
-            // If display state changed to on, proceed and stop the color fade and turn screen on.
-            mPendingScreenOff = false;
-        }
-
-        if (mDisplayBlanksAfterDozeConfig
-                && Display.isDozeState(mPowerState.getScreenState())
-                && !Display.isDozeState(target)) {
-            // Skip the screen off animation and add a black surface to hide the
-            // contents of the screen.
-            mPowerState.prepareColorFade(mContext,
-                    mColorFadeFadesConfig ? ColorFade.MODE_FADE : ColorFade.MODE_WARM_UP);
-            if (mColorFadeOffAnimator != null) {
-                mColorFadeOffAnimator.end();
-            }
-            // Some display hardware will blank itself on the transition between doze and non-doze
-            // but still on display states. In this case we want to report to policy that the
-            // display has turned off so it can prepare the appropriate power on animation, but we
-            // don't want to actually transition to the fully off state since that takes
-            // significantly longer to transition from.
-            setScreenState(Display.STATE_OFF, target != Display.STATE_OFF /*reportOnly*/);
-        }
-
-        // If we were in the process of turning off the screen but didn't quite
-        // finish.  Then finish up now to prevent a jarring transition back
-        // to screen on if we skipped blocking screen on as usual.
-        if (mPendingScreenOff && target != Display.STATE_OFF) {
-            setScreenState(Display.STATE_OFF);
-            mPendingScreenOff = false;
-            mPowerState.dismissColorFadeResources();
-        }
-
-        if (target == Display.STATE_ON) {
-            // Want screen on.  The contents of the screen may not yet
-            // be visible if the color fade has not been dismissed because
-            // its last frame of animation is solid black.
-            if (!setScreenState(Display.STATE_ON)) {
-                return; // screen on blocked
-            }
-            if (USE_COLOR_FADE_ON_ANIMATION && mColorFadeEnabled && mPowerRequest.isBrightOrDim()) {
-                // Perform screen on animation.
-                if (mPowerState.getColorFadeLevel() == 1.0f) {
-                    mPowerState.dismissColorFade();
-                } else if (mPowerState.prepareColorFade(mContext,
-                        mColorFadeFadesConfig
-                                ? ColorFade.MODE_FADE : ColorFade.MODE_WARM_UP)) {
-                    mColorFadeOnAnimator.start();
-                } else {
-                    mColorFadeOnAnimator.end();
-                }
-            } else {
-                // Skip screen on animation.
-                mPowerState.setColorFadeLevel(1.0f);
-                mPowerState.dismissColorFade();
-            }
-        } else if (target == Display.STATE_DOZE) {
-            // Want screen dozing.
-            // Wait for brightness animation to complete beforehand when entering doze
-            // from screen on to prevent a perceptible jump because brightness may operate
-            // differently when the display is configured for dozing.
-            if (mScreenBrightnessRampAnimator.isAnimating()
-                    && mPowerState.getScreenState() == Display.STATE_ON) {
-                return;
-            }
-
-            // Set screen state.
-            if (!setScreenState(Display.STATE_DOZE)) {
-                return; // screen on blocked
-            }
-
-            // Dismiss the black surface without fanfare.
-            mPowerState.setColorFadeLevel(1.0f);
-            mPowerState.dismissColorFade();
-        } else if (target == Display.STATE_DOZE_SUSPEND) {
-            // Want screen dozing and suspended.
-            // Wait for brightness animation to complete beforehand unless already
-            // suspended because we may not be able to change it after suspension.
-            if (mScreenBrightnessRampAnimator.isAnimating()
-                    && mPowerState.getScreenState() != Display.STATE_DOZE_SUSPEND) {
-                return;
-            }
-
-            // If not already suspending, temporarily set the state to doze until the
-            // screen on is unblocked, then suspend.
-            if (mPowerState.getScreenState() != Display.STATE_DOZE_SUSPEND) {
-                if (!setScreenState(Display.STATE_DOZE)) {
-                    return; // screen on blocked
-                }
-                setScreenState(Display.STATE_DOZE_SUSPEND); // already on so can't block
-            }
-
-            // Dismiss the black surface without fanfare.
-            mPowerState.setColorFadeLevel(1.0f);
-            mPowerState.dismissColorFade();
-        } else if (target == Display.STATE_ON_SUSPEND) {
-            // Want screen full-power and suspended.
-            // Wait for brightness animation to complete beforehand unless already
-            // suspended because we may not be able to change it after suspension.
-            if (mScreenBrightnessRampAnimator.isAnimating()
-                    && mPowerState.getScreenState() != Display.STATE_ON_SUSPEND) {
-                return;
-            }
-
-            // If not already suspending, temporarily set the state to on until the
-            // screen on is unblocked, then suspend.
-            if (mPowerState.getScreenState() != Display.STATE_ON_SUSPEND) {
-                if (!setScreenState(Display.STATE_ON)) {
-                    return;
-                }
-                setScreenState(Display.STATE_ON_SUSPEND);
-            }
-
-            // Dismiss the black surface without fanfare.
-            mPowerState.setColorFadeLevel(1.0f);
-            mPowerState.dismissColorFade();
-        } else {
-            // Want screen off.
-            mPendingScreenOff = true;
-            if (!mColorFadeEnabled) {
-                mPowerState.setColorFadeLevel(0.0f);
-            }
-
-            if (mPowerState.getColorFadeLevel() == 0.0f) {
-                // Turn the screen off.
-                // A black surface is already hiding the contents of the screen.
-                setScreenState(Display.STATE_OFF);
-                mPendingScreenOff = false;
-                mPowerState.dismissColorFadeResources();
-            } else if (performScreenOffTransition
-                    && mPowerState.prepareColorFade(mContext,
-                    mColorFadeFadesConfig
-                            ? ColorFade.MODE_FADE : ColorFade.MODE_COOL_DOWN)
-                    && mPowerState.getScreenState() != Display.STATE_OFF) {
-                // Perform the screen off animation.
-                mColorFadeOffAnimator.start();
-            } else {
-                // Skip the screen off animation and add a black surface to hide the
-                // contents of the screen.
-                mColorFadeOffAnimator.end();
-            }
-        }
-    }
-
-    private final Runnable mCleanListener = this::sendUpdatePowerState;
-
-    private void sendOnStateChangedWithWakelock() {
-        boolean wakeLockAcquired = mWakelockController.acquireWakelock(
-                WakelockController.WAKE_LOCK_STATE_CHANGED);
-        if (wakeLockAcquired) {
-            mHandler.post(mWakelockController.getOnStateChangedRunnable());
-        }
-    }
-
-    private void logDisplayPolicyChanged(int newPolicy) {
-        LogMaker log = new LogMaker(MetricsEvent.DISPLAY_POLICY);
-        log.setType(MetricsEvent.TYPE_UPDATE);
-        log.setSubtype(newPolicy);
-        MetricsLogger.action(log);
-    }
-
-    private void handleSettingsChange(boolean userSwitch) {
-        mDisplayBrightnessController
-                .setPendingScreenBrightness(mDisplayBrightnessController
-                        .getScreenBrightnessSetting());
-        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(userSwitch);
-        if (userSwitch) {
-            // Don't treat user switches as user initiated change.
-            mDisplayBrightnessController
-                    .setAndNotifyCurrentScreenBrightness(mDisplayBrightnessController
-                            .getPendingScreenBrightness());
-            if (mAutomaticBrightnessController != null) {
-                mAutomaticBrightnessController.resetShortTermModel();
-            }
-        }
-        sendUpdatePowerState();
-    }
-
-    private void handleBrightnessModeChange() {
-        final int screenBrightnessModeSetting = Settings.System.getIntForUser(
-                mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT);
-        mHandler.postAtTime(() -> {
-            mAutomaticBrightnessStrategy.setUseAutoBrightness(screenBrightnessModeSetting
-                    == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-            updatePowerState();
-        }, mClock.uptimeMillis());
-    }
-
-
-    @Override
-    public float getScreenBrightnessSetting() {
-        return mDisplayBrightnessController.getScreenBrightnessSetting();
-    }
-
-    @Override
-    public void setBrightness(float brightnessValue, int userSerial) {
-        mDisplayBrightnessController.setBrightness(clampScreenBrightness(brightnessValue),
-                userSerial);
-    }
-
-    @Override
-    public int getDisplayId() {
-        return mDisplayId;
-    }
-
-    @Override
-    public int getLeadDisplayId() {
-        return mLeadDisplayId;
-    }
-
-    @Override
-    public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
-            boolean slowChange) {
-        mBrightnessRangeController.onAmbientLuxChange(ambientLux);
-        if (nits == BrightnessMappingStrategy.INVALID_NITS) {
-            mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness, slowChange);
-        } else {
-            float brightness = mDisplayBrightnessController.getBrightnessFromNits(nits);
-            if (BrightnessUtils.isValidBrightnessValue(brightness)) {
-                mDisplayBrightnessController.setBrightnessToFollow(brightness, slowChange);
-            } else {
-                // The device does not support nits
-                mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness,
-                        slowChange);
-            }
-        }
-        sendUpdatePowerState();
-    }
-
-    private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated,
-            boolean wasShortTermModelActive, boolean autobrightnessEnabled,
-            boolean brightnessIsTemporary, boolean shouldUseAutoBrightness) {
-
-        final float brightnessInNits =
-                mDisplayBrightnessController.convertToAdjustedNits(brightness);
-        // Don't report brightness to brightnessTracker:
-        // If brightness is temporary (ie the slider has not been released)
-        // or if we are in idle screen brightness mode.
-        // or display is not on
-        // or we shouldn't be using autobrightness
-        // or the nits is invalid.
-        if (brightnessIsTemporary
-                || mAutomaticBrightnessController == null
-                || mAutomaticBrightnessController.isInIdleMode()
-                || !autobrightnessEnabled
-                || mBrightnessTracker == null
-                || !shouldUseAutoBrightness
-                || brightnessInNits < 0.0f) {
-            return;
-        }
-
-        if (userInitiated && (mAutomaticBrightnessController == null
-                || !mAutomaticBrightnessController.hasValidAmbientLux())) {
-            // If we don't have a valid lux reading we can't report a valid
-            // slider event so notify as if the system changed the brightness.
-            userInitiated = false;
-        }
-
-        // We only want to track changes on devices that can actually map the display backlight
-        // values into a physical brightness unit since the value provided by the API is in
-        // nits and not using the arbitrary backlight units.
-        final float powerFactor = mPowerRequest.lowPowerMode
-                ? mPowerRequest.screenLowPowerBrightnessFactor
-                : 1.0f;
-        mBrightnessTracker.notifyBrightnessChanged(brightnessInNits, userInitiated,
-                powerFactor, wasShortTermModelActive,
-                mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId,
-                mAutomaticBrightnessController.getLastSensorValues(),
-                mAutomaticBrightnessController.getLastSensorTimestamps());
-    }
-
-    @Override
-    public void addDisplayBrightnessFollower(DisplayPowerControllerInterface follower) {
-        synchronized (mLock) {
-            mDisplayBrightnessFollowers.append(follower.getDisplayId(), follower);
-            sendUpdatePowerStateLocked();
-        }
-    }
-
-    @Override
-    public void removeDisplayBrightnessFollower(DisplayPowerControllerInterface follower) {
-        synchronized (mLock) {
-            mDisplayBrightnessFollowers.remove(follower.getDisplayId());
-            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
-                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
-                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void clearDisplayBrightnessFollowersLocked() {
-        for (int i = 0; i < mDisplayBrightnessFollowers.size(); i++) {
-            DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i);
-            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
-                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
-                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
-        }
-        mDisplayBrightnessFollowers.clear();
-    }
-
-    @Override
-    public void dump(final PrintWriter pw) {
-        synchronized (mLock) {
-            pw.println();
-            pw.println("Display Power Controller:");
-            pw.println("  mDisplayId=" + mDisplayId);
-            pw.println("  mLeadDisplayId=" + mLeadDisplayId);
-            pw.println("  mLightSensor=" + mLightSensor);
-            pw.println("  mDisplayBrightnessFollowers=" + mDisplayBrightnessFollowers);
-
-            pw.println();
-            pw.println("Display Power Controller Locked State:");
-            pw.println("  mDisplayReadyLocked=" + mDisplayReadyLocked);
-            pw.println("  mPendingRequestLocked=" + mPendingRequestLocked);
-            pw.println("  mPendingRequestChangedLocked=" + mPendingRequestChangedLocked);
-            pw.println("  mPendingUpdatePowerStateLocked=" + mPendingUpdatePowerStateLocked);
-        }
-
-        pw.println();
-        pw.println("Display Power Controller Configuration:");
-        pw.println("  mScreenBrightnessDozeConfig=" + mScreenBrightnessDozeConfig);
-        pw.println("  mUseSoftwareAutoBrightnessConfig=" + mUseSoftwareAutoBrightnessConfig);
-        pw.println("  mSkipScreenOnBrightnessRamp=" + mSkipScreenOnBrightnessRamp);
-        pw.println("  mColorFadeFadesConfig=" + mColorFadeFadesConfig);
-        pw.println("  mColorFadeEnabled=" + mColorFadeEnabled);
-        pw.println("  mIsDisplayInternal=" + mIsDisplayInternal);
-        synchronized (mCachedBrightnessInfo) {
-            pw.println("  mCachedBrightnessInfo.brightness="
-                    + mCachedBrightnessInfo.brightness.value);
-            pw.println("  mCachedBrightnessInfo.adjustedBrightness="
-                    + mCachedBrightnessInfo.adjustedBrightness.value);
-            pw.println("  mCachedBrightnessInfo.brightnessMin="
-                    + mCachedBrightnessInfo.brightnessMin.value);
-            pw.println("  mCachedBrightnessInfo.brightnessMax="
-                    + mCachedBrightnessInfo.brightnessMax.value);
-            pw.println("  mCachedBrightnessInfo.hbmMode=" + mCachedBrightnessInfo.hbmMode.value);
-            pw.println("  mCachedBrightnessInfo.hbmTransitionPoint="
-                    + mCachedBrightnessInfo.hbmTransitionPoint.value);
-            pw.println("  mCachedBrightnessInfo.brightnessMaxReason ="
-                    + mCachedBrightnessInfo.brightnessMaxReason.value);
-        }
-        pw.println("  mDisplayBlanksAfterDozeConfig=" + mDisplayBlanksAfterDozeConfig);
-        pw.println("  mBrightnessBucketsInDozeConfig=" + mBrightnessBucketsInDozeConfig);
-        mHandler.runWithScissors(() -> dumpLocal(pw), 1000);
-    }
-
-    private void dumpLocal(PrintWriter pw) {
-        pw.println();
-        pw.println("Display Power Controller Thread State:");
-        pw.println("  mPowerRequest=" + mPowerRequest);
-        pw.println("  mBrightnessReason=" + mBrightnessReason);
-        pw.println("  mAppliedDimming=" + mAppliedDimming);
-        pw.println("  mAppliedThrottling=" + mAppliedThrottling);
-        pw.println("  mDozing=" + mDozing);
-        pw.println("  mSkipRampState=" + skipRampStateToString(mSkipRampState));
-        pw.println("  mScreenOnBlockStartRealTime=" + mScreenOnBlockStartRealTime);
-        pw.println("  mScreenOffBlockStartRealTime=" + mScreenOffBlockStartRealTime);
-        pw.println("  mPendingScreenOnUnblocker=" + mPendingScreenOnUnblocker);
-        pw.println("  mPendingScreenOffUnblocker=" + mPendingScreenOffUnblocker);
-        pw.println("  mPendingScreenOff=" + mPendingScreenOff);
-        pw.println("  mReportedToPolicy="
-                + reportedToPolicyToString(mReportedScreenStateToPolicy));
-        pw.println("  mIsRbcActive=" + mIsRbcActive);
-        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "    ");
-        mAutomaticBrightnessStrategy.dump(ipw);
-
-        if (mScreenBrightnessRampAnimator != null) {
-            pw.println("  mScreenBrightnessRampAnimator.isAnimating()="
-                    + mScreenBrightnessRampAnimator.isAnimating());
-        }
-
-        if (mColorFadeOnAnimator != null) {
-            pw.println("  mColorFadeOnAnimator.isStarted()="
-                    + mColorFadeOnAnimator.isStarted());
-        }
-        if (mColorFadeOffAnimator != null) {
-            pw.println("  mColorFadeOffAnimator.isStarted()="
-                    + mColorFadeOffAnimator.isStarted());
-        }
-
-        if (mPowerState != null) {
-            mPowerState.dump(pw);
-        }
-
-        if (mAutomaticBrightnessController != null) {
-            mAutomaticBrightnessController.dump(pw);
-            dumpBrightnessEvents(pw);
-        }
-
-        dumpRbcEvents(pw);
-
-        if (mScreenOffBrightnessSensorController != null) {
-            mScreenOffBrightnessSensorController.dump(pw);
-        }
-
-        if (mBrightnessRangeController != null) {
-            mBrightnessRangeController.dump(pw);
-        }
-
-        if (mBrightnessThrottler != null) {
-            mBrightnessThrottler.dump(pw);
-        }
-
-        pw.println();
-        if (mDisplayWhiteBalanceController != null) {
-            mDisplayWhiteBalanceController.dump(pw);
-            mDisplayWhiteBalanceSettings.dump(pw);
-        }
-
-        pw.println();
-
-        if (mWakelockController != null) {
-            mWakelockController.dumpLocal(pw);
-        }
-
-        pw.println();
-        if (mDisplayBrightnessController != null) {
-            mDisplayBrightnessController.dump(pw);
-        }
-
-        pw.println();
-        if (mDisplayStateController != null) {
-            mDisplayStateController.dumpsys(pw);
-        }
-
-        pw.println();
-        if (mBrightnessClamperController != null) {
-            mBrightnessClamperController.dump(ipw);
-        }
-    }
-
-
-    private static String reportedToPolicyToString(int state) {
-        switch (state) {
-            case REPORTED_TO_POLICY_SCREEN_OFF:
-                return "REPORTED_TO_POLICY_SCREEN_OFF";
-            case REPORTED_TO_POLICY_SCREEN_TURNING_ON:
-                return "REPORTED_TO_POLICY_SCREEN_TURNING_ON";
-            case REPORTED_TO_POLICY_SCREEN_ON:
-                return "REPORTED_TO_POLICY_SCREEN_ON";
-            default:
-                return Integer.toString(state);
-        }
-    }
-
-    private static String skipRampStateToString(int state) {
-        switch (state) {
-            case RAMP_STATE_SKIP_NONE:
-                return "RAMP_STATE_SKIP_NONE";
-            case RAMP_STATE_SKIP_INITIAL:
-                return "RAMP_STATE_SKIP_INITIAL";
-            case RAMP_STATE_SKIP_AUTOBRIGHT:
-                return "RAMP_STATE_SKIP_AUTOBRIGHT";
-            default:
-                return Integer.toString(state);
-        }
-    }
-
-    private void dumpBrightnessEvents(PrintWriter pw) {
-        int size = mBrightnessEventRingBuffer.size();
-        if (size < 1) {
-            pw.println("No Automatic Brightness Adjustments");
-            return;
-        }
-
-        pw.println("Automatic Brightness Adjustments Last " + size + " Events: ");
-        BrightnessEvent[] eventArray = mBrightnessEventRingBuffer.toArray();
-        for (int i = 0; i < mBrightnessEventRingBuffer.size(); i++) {
-            pw.println("  " + eventArray[i].toString());
-        }
-    }
-
-    private void dumpRbcEvents(PrintWriter pw) {
-        int size = mRbcEventRingBuffer.size();
-        if (size < 1) {
-            pw.println("No Reduce Bright Colors Adjustments");
-            return;
-        }
-
-        pw.println("Reduce Bright Colors Adjustments Last " + size + " Events: ");
-        BrightnessEvent[] eventArray = mRbcEventRingBuffer.toArray();
-        for (int i = 0; i < mRbcEventRingBuffer.size(); i++) {
-            pw.println("  " + eventArray[i]);
-        }
-    }
-
-
-    private void noteScreenState(int screenState) {
-        // Log screen state change with display id
-        FrameworkStatsLog.write(FrameworkStatsLog.SCREEN_STATE_CHANGED_V2,
-                screenState, mDisplayStatsId);
-        if (mBatteryStats != null) {
-            try {
-                // TODO(multi-display): make this multi-display
-                mBatteryStats.noteScreenState(screenState);
-            } catch (RemoteException e) {
-                // same process
-            }
-        }
-    }
-
-    @SuppressLint("AndroidFrameworkRequiresPermission")
-    private void noteScreenBrightness(float brightness) {
-        if (mBatteryStats != null) {
-            try {
-                // TODO(brightnessfloat): change BatteryStats to use float
-                int brightnessInt = mFlags.isBrightnessIntRangeUserPerceptionEnabled()
-                        ? BrightnessSynchronizer.brightnessFloatToIntSetting(mContext, brightness)
-                        : BrightnessSynchronizer.brightnessFloatToInt(brightness);
-                mBatteryStats.noteScreenBrightness(brightnessInt);
-            } catch (RemoteException e) {
-                // same process
-            }
-        }
-    }
-
-    private void reportStats(float brightness) {
-        if (mLastStatsBrightness == brightness) {
-            return;
-        }
-
-        float hbmTransitionPoint = PowerManager.BRIGHTNESS_MAX;
-        synchronized (mCachedBrightnessInfo) {
-            if (mCachedBrightnessInfo.hbmTransitionPoint == null) {
-                return;
-            }
-            hbmTransitionPoint = mCachedBrightnessInfo.hbmTransitionPoint.value;
-        }
-
-        final boolean aboveTransition = brightness > hbmTransitionPoint;
-        final boolean oldAboveTransition = mLastStatsBrightness > hbmTransitionPoint;
-
-        if (aboveTransition || oldAboveTransition) {
-            mLastStatsBrightness = brightness;
-            mHandler.removeMessages(MSG_STATSD_HBM_BRIGHTNESS);
-            if (aboveTransition != oldAboveTransition) {
-                // report immediately
-                logHbmBrightnessStats(brightness, mDisplayStatsId);
-            } else {
-                // delay for rate limiting
-                Message msg = mHandler.obtainMessage();
-                msg.what = MSG_STATSD_HBM_BRIGHTNESS;
-                msg.arg1 = Float.floatToIntBits(brightness);
-                msg.arg2 = mDisplayStatsId;
-                mHandler.sendMessageAtTime(msg, mClock.uptimeMillis()
-                        + BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS);
-            }
-        }
-    }
-
-    private void logHbmBrightnessStats(float brightness, int displayStatsId) {
-        synchronized (mHandler) {
-            FrameworkStatsLog.write(
-                    FrameworkStatsLog.DISPLAY_HBM_BRIGHTNESS_CHANGED, displayStatsId, brightness);
-        }
-    }
-
-    // Return bucket index of range_[left]_[right] where
-    // left <= nits < right
-    private int nitsToRangeIndex(float nits) {
-        for (int i = 0; i < BRIGHTNESS_RANGE_BOUNDARIES.length; i++) {
-            if (nits < BRIGHTNESS_RANGE_BOUNDARIES[i]) {
-                return BRIGHTNESS_RANGE_INDEX[i];
-            }
-        }
-        return FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__BUCKET_INDEX__RANGE_3000_INF;
-    }
-
-    private int convertBrightnessReasonToStatsEnum(int brightnessReason) {
-        switch(brightnessReason) {
-            case BrightnessReason.REASON_UNKNOWN:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_UNKNOWN;
-            case BrightnessReason.REASON_MANUAL:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_MANUAL;
-            case BrightnessReason.REASON_DOZE:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_DOZE;
-            case BrightnessReason.REASON_DOZE_DEFAULT:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_DOZE_DEFAULT;
-            case BrightnessReason.REASON_AUTOMATIC:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_AUTOMATIC;
-            case BrightnessReason.REASON_SCREEN_OFF:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_SCREEN_OFF;
-            case BrightnessReason.REASON_OVERRIDE:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_OVERRIDE;
-            case BrightnessReason.REASON_TEMPORARY:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_TEMPORARY;
-            case BrightnessReason.REASON_BOOST:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_BOOST;
-            case BrightnessReason.REASON_SCREEN_OFF_BRIGHTNESS_SENSOR:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_SCREEN_OFF_BRIGHTNESS_SENSOR;
-            case BrightnessReason.REASON_FOLLOWER:
-                return FrameworkStatsLog
-                    .DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_FOLLOWER;
-        }
-        return FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_UNKNOWN;
-    }
-
-    private void logBrightnessEvent(BrightnessEvent event, float unmodifiedBrightness) {
-        int modifier = event.getReason().getModifier();
-        int flags = event.getFlags();
-        // It's easier to check if the brightness is at maximum level using the brightness
-        // value untouched by any modifiers
-        boolean brightnessIsMax = unmodifiedBrightness == event.getHbmMax();
-        float brightnessInNits =
-                mDisplayBrightnessController.convertToAdjustedNits(event.getBrightness());
-        float appliedLowPowerMode = event.isLowPowerModeSet() ? event.getPowerFactor() : -1f;
-        int appliedRbcStrength  = event.isRbcEnabled() ? event.getRbcStrength() : -1;
-        float appliedHbmMaxNits =
-                event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF
-                ? -1f : mDisplayBrightnessController.convertToAdjustedNits(event.getHbmMax());
-        // thermalCapNits set to -1 if not currently capping max brightness
-        float appliedThermalCapNits =
-                event.getThermalMax() == PowerManager.BRIGHTNESS_MAX
-                ? -1f : mDisplayBrightnessController.convertToAdjustedNits(event.getThermalMax());
-        if (mIsDisplayInternal) {
-            FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
-                    mDisplayBrightnessController
-                            .convertToAdjustedNits(event.getInitialBrightness()),
-                    brightnessInNits,
-                    event.getLux(),
-                    event.getPhysicalDisplayId(),
-                    event.wasShortTermModelActive(),
-                    appliedLowPowerMode,
-                    appliedRbcStrength,
-                    appliedHbmMaxNits,
-                    appliedThermalCapNits,
-                    event.isAutomaticBrightnessEnabled(),
-                    FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL,
-                    convertBrightnessReasonToStatsEnum(event.getReason().getReason()),
-                    nitsToRangeIndex(brightnessInNits),
-                    brightnessIsMax,
-                    event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT,
-                    event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR,
-                    (modifier & BrightnessReason.MODIFIER_LOW_POWER) > 0,
-                    mBrightnessClamperController.getBrightnessMaxReason(),
-                    // TODO: (flc) add brightnessMinReason here too.
-                    (modifier & BrightnessReason.MODIFIER_DIMMED) > 0,
-                    event.isRbcEnabled(),
-                    (flags & BrightnessEvent.FLAG_INVALID_LUX) > 0,
-                    (flags & BrightnessEvent.FLAG_DOZE_SCALE) > 0,
-                    (flags & BrightnessEvent.FLAG_USER_SET) > 0,
-                    (flags & BrightnessEvent.FLAG_IDLE_CURVE) > 0,
-                    (flags & BrightnessEvent.FLAG_LOW_POWER_MODE) > 0);
-        }
-    }
-
-    /**
-     * Indicates whether the display state is ready to update. If this is the default display, we
-     * want to update it right away so that we can draw the boot animation on it. If it is not
-     * the default display, drawing the boot animation on it would look incorrect, so we need
-     * to wait until boot is completed.
-     * @return True if the display state is ready to update
-     */
-    private boolean readyToUpdateDisplayState() {
-        return mDisplayId == Display.DEFAULT_DISPLAY || mBootCompleted;
-    }
-
-    private final class DisplayControllerHandler extends Handler {
-        DisplayControllerHandler(Looper looper) {
-            super(looper, null, true /*async*/);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_UPDATE_POWER_STATE:
-                    updatePowerState();
-                    break;
-
-                case MSG_SCREEN_ON_UNBLOCKED:
-                    if (mPendingScreenOnUnblocker == msg.obj) {
-                        unblockScreenOn();
-                        updatePowerState();
-                    }
-                    break;
-                case MSG_SCREEN_OFF_UNBLOCKED:
-                    if (mPendingScreenOffUnblocker == msg.obj) {
-                        unblockScreenOff();
-                        updatePowerState();
-                    }
-                    break;
-                case MSG_OFFLOADING_SCREEN_ON_UNBLOCKED:
-                    if (mDisplayOffloadSession == msg.obj) {
-                        unblockScreenOnByDisplayOffload();
-                        updatePowerState();
-                    }
-                    break;
-                case MSG_CONFIGURE_BRIGHTNESS:
-                    BrightnessConfiguration brightnessConfiguration =
-                            (BrightnessConfiguration) msg.obj;
-                    mAutomaticBrightnessStrategy.setBrightnessConfiguration(brightnessConfiguration,
-                            msg.arg1 == 1);
-                    if (mBrightnessTracker != null) {
-                        mBrightnessTracker
-                                .setShouldCollectColorSample(brightnessConfiguration != null
-                                        && brightnessConfiguration.shouldCollectColorSamples());
-                    }
-                    updatePowerState();
-                    break;
-
-                case MSG_SET_TEMPORARY_BRIGHTNESS:
-                    // TODO: Should we have a a timeout for the temporary brightness?
-                    mDisplayBrightnessController
-                            .setTemporaryBrightness(Float.intBitsToFloat(msg.arg1));
-                    updatePowerState();
-                    break;
-
-                case MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT:
-                    mAutomaticBrightnessStrategy
-                            .setTemporaryAutoBrightnessAdjustment(Float.intBitsToFloat(msg.arg1));
-                    updatePowerState();
-                    break;
-
-                case MSG_STOP:
-                    cleanupHandlerThreadAfterStop();
-                    break;
-
-                case MSG_UPDATE_BRIGHTNESS:
-                    if (mStopped) {
-                        return;
-                    }
-                    handleSettingsChange(false /*userSwitch*/);
-                    break;
-
-                case MSG_UPDATE_RBC:
-                    handleRbcChanged();
-                    break;
-
-                case MSG_BRIGHTNESS_RAMP_DONE:
-                    if (mPowerState != null) {
-                        final float brightness = mPowerState.getScreenBrightness();
-                        reportStats(brightness);
-                    }
-                    break;
-
-                case MSG_STATSD_HBM_BRIGHTNESS:
-                    logHbmBrightnessStats(Float.intBitsToFloat(msg.arg1), msg.arg2);
-                    break;
-
-                case MSG_SWITCH_USER:
-                    handleOnSwitchUser(msg.arg1);
-                    break;
-
-                case MSG_BOOT_COMPLETED:
-                    mBootCompleted = true;
-                    updatePowerState();
-                    break;
-
-                case MSG_SET_DWBC_STRONG_MODE:
-                    setDwbcStrongMode(msg.arg1);
-                    break;
-
-                case MSG_SET_DWBC_COLOR_OVERRIDE:
-                    final float cct = Float.intBitsToFloat(msg.arg1);
-                    setDwbcOverride(cct);
-                    break;
-
-                case MSG_SET_DWBC_LOGGING_ENABLED:
-                    setDwbcLoggingEnabled(msg.arg1);
-                    break;
-                case MSG_SET_BRIGHTNESS_FROM_OFFLOAD:
-                    mDisplayBrightnessController.setBrightnessFromOffload(
-                            Float.intBitsToFloat(msg.arg1));
-                    updatePowerState();
-                    break;
-            }
-        }
-    }
-
-
-    private final class SettingsObserver extends ContentObserver {
-        SettingsObserver(Handler handler) {
-            super(handler);
-        }
-
-        @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            if (uri.equals(Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE))) {
-                handleBrightnessModeChange();
-            } else if (uri.equals(Settings.System.getUriFor(
-                    Settings.System.SCREEN_BRIGHTNESS_FOR_ALS))) {
-                int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
-                        Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
-                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL,
-                        UserHandle.USER_CURRENT);
-                Slog.i(mTag, "Setting up auto-brightness for preset "
-                        + autoBrightnessPresetToString(preset));
-                setUpAutoBrightness(mContext, mHandler);
-                sendUpdatePowerState();
-            } else {
-                handleSettingsChange(false /* userSwitch */);
-            }
-        }
-    }
-
-    private final class ScreenOnUnblocker implements WindowManagerPolicy.ScreenOnListener {
-        @Override
-        public void onScreenOn() {
-            Message msg = mHandler.obtainMessage(MSG_SCREEN_ON_UNBLOCKED, this);
-            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-        }
-    }
-
-    private final class ScreenOffUnblocker implements WindowManagerPolicy.ScreenOffListener {
-        @Override
-        public void onScreenOff() {
-            Message msg = mHandler.obtainMessage(MSG_SCREEN_OFF_UNBLOCKED, this);
-            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
-        }
-    }
-
-    @Override
-    public void setAutoBrightnessLoggingEnabled(boolean enabled) {
-        if (mAutomaticBrightnessController != null) {
-            mAutomaticBrightnessController.setLoggingEnabled(enabled);
-        }
-    }
-
-    @Override // DisplayWhiteBalanceController.Callbacks
-    public void updateWhiteBalance() {
-        sendUpdatePowerState();
-    }
-
-    @Override
-    public void setDisplayWhiteBalanceLoggingEnabled(boolean enabled) {
-        Message msg = mHandler.obtainMessage();
-        msg.what = MSG_SET_DWBC_LOGGING_ENABLED;
-        msg.arg1 = enabled ? 1 : 0;
-        msg.sendToTarget();
-    }
-
-    @Override
-    public void setAmbientColorTemperatureOverride(float cct) {
-        Message msg = mHandler.obtainMessage();
-        msg.what = MSG_SET_DWBC_COLOR_OVERRIDE;
-        msg.arg1 = Float.floatToIntBits(cct);
-        msg.sendToTarget();
-    }
-
-    /** Functional interface for providing time. */
-    @VisibleForTesting
-    interface Clock {
-        /**
-         * Returns current time in milliseconds since boot, not counting time spent in deep sleep.
-         */
-        long uptimeMillis();
-    }
-
-    @VisibleForTesting
-    static class Injector {
-        Clock getClock() {
-            return SystemClock::uptimeMillis;
-        }
-
-        DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
-                int displayId, int displayState) {
-            return new DisplayPowerState(blanker, colorFade, displayId, displayState);
-        }
-
-        DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
-                FloatProperty<DisplayPowerState> firstProperty,
-                FloatProperty<DisplayPowerState> secondProperty) {
-            return new DualRampAnimator(dps, firstProperty, secondProperty);
-        }
-
-        WakelockController getWakelockController(int displayId,
-                DisplayPowerCallbacks displayPowerCallbacks) {
-            return new WakelockController(displayId, displayPowerCallbacks);
-        }
-
-        DisplayPowerProximityStateController getDisplayPowerProximityStateController(
-                WakelockController wakelockController, DisplayDeviceConfig displayDeviceConfig,
-                Looper looper, Runnable nudgeUpdatePowerState,
-                int displayId, SensorManager sensorManager) {
-            return new DisplayPowerProximityStateController(wakelockController, displayDeviceConfig,
-                    looper, nudgeUpdatePowerState,
-                    displayId, sensorManager, /* injector= */ null);
-        }
-
-        AutomaticBrightnessController getAutomaticBrightnessController(
-                AutomaticBrightnessController.Callbacks callbacks, Looper looper,
-                SensorManager sensorManager, Sensor lightSensor,
-                SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-                int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
-                float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
-                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
-                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
-                boolean resetAmbientLuxAfterWarmUpConfig,
-                HysteresisLevels ambientBrightnessThresholds,
-                HysteresisLevels screenBrightnessThresholds,
-                HysteresisLevels ambientBrightnessThresholdsIdle,
-                HysteresisLevels screenBrightnessThresholdsIdle, Context context,
-                BrightnessRangeController brightnessModeController,
-                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, float userLux, float userNits) {
-            return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
-                    brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin,
-                    brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
-                    brighteningLightDebounceConfig, darkeningLightDebounceConfig,
-                    brighteningLightDebounceConfigIdle, darkeningLightDebounceConfigIdle,
-                    resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
-                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
-                    screenBrightnessThresholdsIdle, context, brightnessModeController,
-                    brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
-                    userNits);
-        }
-
-        BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
-                DisplayDeviceConfig displayDeviceConfig,
-                DisplayWhiteBalanceController displayWhiteBalanceController) {
-            return BrightnessMappingStrategy.create(context, displayDeviceConfig,
-                    AUTO_BRIGHTNESS_MODE_DEFAULT, displayWhiteBalanceController);
-        }
-
-        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
-                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
-                float[] darkeningThresholdLevels, float minDarkeningThreshold,
-                float minBrighteningThreshold) {
-            return new HysteresisLevels(brighteningThresholdsPercentages,
-                    darkeningThresholdsPercentages, brighteningThresholdLevels,
-                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold);
-        }
-
-        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
-                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
-                float[] darkeningThresholdLevels, float minDarkeningThreshold,
-                float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
-            return new HysteresisLevels(brighteningThresholdsPercentages,
-                    darkeningThresholdsPercentages, brighteningThresholdLevels,
-                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold,
-                    potentialOldBrightnessRange);
-        }
-
-        ScreenOffBrightnessSensorController getScreenOffBrightnessSensorController(
-                SensorManager sensorManager,
-                Sensor lightSensor,
-                Handler handler,
-                ScreenOffBrightnessSensorController.Clock clock,
-                int[] sensorValueToLux,
-                BrightnessMappingStrategy brightnessMapper) {
-            return new ScreenOffBrightnessSensorController(
-                    sensorManager,
-                    lightSensor,
-                    handler,
-                    clock,
-                    sensorValueToLux,
-                    brightnessMapper
-            );
-        }
-
-        HighBrightnessModeController getHighBrightnessModeController(Handler handler, int width,
-                int height, IBinder displayToken, String displayUniqueId, float brightnessMin,
-                float brightnessMax, DisplayDeviceConfig.HighBrightnessModeData hbmData,
-                HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg,
-                Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata,
-                Context context) {
-            return new HighBrightnessModeController(handler, width, height, displayToken,
-                    displayUniqueId, brightnessMin, brightnessMax, hbmData, hdrBrightnessCfg,
-                    hbmChangeCallback, hbmMetadata, context);
-        }
-
-        BrightnessRangeController getBrightnessRangeController(
-                HighBrightnessModeController hbmController, Runnable modeChangeCallback,
-                DisplayDeviceConfig displayDeviceConfig, Handler handler,
-                DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) {
-            return new BrightnessRangeController(hbmController,
-                    modeChangeCallback, displayDeviceConfig, handler, flags, displayToken, info);
-        }
-
-        BrightnessClamperController getBrightnessClamperController(Handler handler,
-                BrightnessClamperController.ClamperChangeListener clamperChangeListener,
-                BrightnessClamperController.DisplayDeviceData data, Context context,
-                DisplayManagerFlags flags) {
-
-            return new BrightnessClamperController(handler, clamperChangeListener, data, context,
-                    flags);
-        }
-
-        DisplayWhiteBalanceController getDisplayWhiteBalanceController(Handler handler,
-                SensorManager sensorManager, Resources resources) {
-            return DisplayWhiteBalanceFactory.create(handler,
-                    sensorManager, resources);
-        }
-
-        boolean isColorFadeEnabled() {
-            return !ActivityManager.isLowRamDeviceStatic();
-        }
-    }
-
-    static class CachedBrightnessInfo {
-        public MutableFloat brightness = new MutableFloat(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        public MutableFloat adjustedBrightness =
-                new MutableFloat(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        public MutableFloat brightnessMin =
-                new MutableFloat(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        public MutableFloat brightnessMax =
-                new MutableFloat(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        public MutableInt hbmMode = new MutableInt(BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF);
-        public MutableFloat hbmTransitionPoint =
-                new MutableFloat(HighBrightnessModeController.HBM_TRANSITION_POINT_INVALID);
-        public MutableInt brightnessMaxReason =
-                new MutableInt(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE);
-
-        public boolean checkAndSetFloat(MutableFloat mf, float f) {
-            if (mf.value != f) {
-                mf.value = f;
-                return true;
-            }
-            return false;
-        }
-
-        public boolean checkAndSetInt(MutableInt mi, int i) {
-            if (mi.value != i) {
-                mi.value = i;
-                return true;
-            }
-            return false;
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java b/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java
index 465584c..403dfbe 100644
--- a/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java
+++ b/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java
@@ -41,13 +41,6 @@
         mDeviceConfig = deviceConfig;
     }
 
-    // feature: revamping_display_power_controller_feature
-    // parameter: use_newly_structured_display_power_controller
-    public boolean isNewPowerControllerFeatureEnabled() {
-        return mDeviceConfig.getBoolean(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                DisplayManager.DeviceConfig.KEY_NEW_POWER_CONTROLLER, true);
-    }
-
     // feature: hdr_output_control
     // parameter: enable_hdr_output_control
     public boolean isHdrOutputControlFeatureEnabled() {
diff --git a/services/core/java/com/android/server/inputmethod/OWNERS b/services/core/java/com/android/server/inputmethod/OWNERS
index aa638aa..e507c6b 100644
--- a/services/core/java/com/android/server/inputmethod/OWNERS
+++ b/services/core/java/com/android/server/inputmethod/OWNERS
@@ -6,5 +6,8 @@
 fstern@google.com
 cosminbaies@google.com
 
+# Automotive
+kanant@google.com
+
 ogunwale@google.com #{LAST_RESORT_SUGGESTION}
 jjaggi@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
index c772e08..5df0de8 100644
--- a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
+++ b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
@@ -22,12 +22,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.os.SystemClock;
 import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyManager;
 import android.util.Log;
 
 import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.flags.Flags;
 import com.android.server.FgThread;
 
 import java.util.Objects;
@@ -104,10 +106,26 @@
         boolean isInExtensionTime = mEmergencyCallEndRealtimeMs != Long.MIN_VALUE
                 && (SystemClock.elapsedRealtime() - mEmergencyCallEndRealtimeMs) < extensionTimeMs;
 
-        return mIsInEmergencyCall
-                || isInExtensionTime
-                || mTelephonyManager.getEmergencyCallbackMode()
-                || mTelephonyManager.isInEmergencySmsMode();
+        if (!Flags.enforceTelephonyFeatureMapping()) {
+            return mIsInEmergencyCall
+                    || isInExtensionTime
+                    || mTelephonyManager.getEmergencyCallbackMode()
+                    || mTelephonyManager.isInEmergencySmsMode();
+        } else {
+            boolean emergencyCallbackMode = false;
+            boolean emergencySmsMode = false;
+            PackageManager pm = mContext.getPackageManager();
+            if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
+                emergencyCallbackMode = mTelephonyManager.getEmergencyCallbackMode();
+            }
+            if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
+                emergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
+            }
+            return mIsInEmergencyCall
+                    || isInExtensionTime
+                    || emergencyCallbackMode
+                    || emergencySmsMode;
+        }
     }
 
     private class EmergencyCallTelephonyCallback extends TelephonyCallback implements
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index ad09082..542b3b0 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -52,6 +52,7 @@
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.IActivityManager;
@@ -74,6 +75,7 @@
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.database.sqlite.SQLiteDatabase;
@@ -303,7 +305,7 @@
     private boolean mThirdPartyAppsStarted;
 
     // Current password metrics for all secured users on the device. Updated when user unlocks the
-    // device or changes password. Removed when user is stopped.
+    // device or changes password. Removed if user is stopped with its CE key evicted.
     @GuardedBy("this")
     private final SparseArray<PasswordMetrics> mUserPasswordMetrics = new SparseArray<>();
     @VisibleForTesting
@@ -793,13 +795,33 @@
     }
 
     @VisibleForTesting
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.QUERY_USERS,
+            android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
     void onUserStopped(int userId) {
         hideEncryptionNotification(new UserHandle(userId));
-        // User is stopped with its CE key evicted. Restore strong auth requirement to the default
-        // flags after boot since stopping and restarting a user later is equivalent to rebooting
-        // the device.
+
+        // Normally, CE storage is locked when a user is stopped, and restarting the user requires
+        // strong auth.  Therefore, reset the user's strong auth flags.  The exception is users that
+        // allow delayed locking; under some circumstances, biometric authentication is allowed to
+        // restart such users.  Don't reset the strong auth flags for such users.
+        //
+        // TODO(b/319142556): It might make more sense to reset the strong auth flags when CE
+        // storage is locked, instead of when the user is stopped.  This would ensure the flags get
+        // reset if CE storage is locked later for a user that allows delayed locking.
+        if (android.os.Flags.allowPrivateProfile()
+                && android.multiuser.Flags.enableBiometricsToUnlockPrivateSpace()) {
+            UserProperties userProperties = mUserManager.getUserProperties(UserHandle.of(userId));
+            if (userProperties != null && userProperties.getAllowStoppingUserWithDelayedLocking()) {
+                return;
+            }
+        }
         int strongAuthRequired = LockPatternUtils.StrongAuthTracker.getDefaultFlags(mContext);
         requireStrongAuth(strongAuthRequired, userId);
+
+        // Don't keep the password metrics in memory for a stopped user that will require strong
+        // auth to start again, since strong auth will make the password metrics available again.
         synchronized (this) {
             mUserPasswordMetrics.remove(userId);
         }
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 6deda46..f6571d9 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -653,14 +653,14 @@
         }
 
         @Override // Binder call
-        public boolean hasProjectionPermission(int uid, String packageName) {
+        public boolean hasProjectionPermission(int processUid, String packageName) {
             final long token = Binder.clearCallingIdentity();
             boolean hasPermission = false;
             try {
                 hasPermission |= checkPermission(packageName,
                         android.Manifest.permission.CAPTURE_VIDEO_OUTPUT)
                         || mAppOps.noteOpNoThrow(
-                                AppOpsManager.OP_PROJECT_MEDIA, uid, packageName)
+                                AppOpsManager.OP_PROJECT_MEDIA, processUid, packageName)
                         == AppOpsManager.MODE_ALLOWED;
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -669,7 +669,7 @@
         }
 
         @Override // Binder call
-        public IMediaProjection createProjection(int uid, String packageName, int type,
+        public IMediaProjection createProjection(int processUid, String packageName, int type,
                 boolean isPermanentGrant) {
             if (mContext.checkCallingPermission(MANAGE_MEDIA_PROJECTION)
                         != PackageManager.PERMISSION_GRANTED) {
@@ -680,13 +680,13 @@
                 throw new IllegalArgumentException("package name must not be empty");
             }
             final UserHandle callingUser = Binder.getCallingUserHandle();
-            return createProjectionInternal(uid, packageName, type, isPermanentGrant,
+            return createProjectionInternal(processUid, packageName, type, isPermanentGrant,
                     callingUser);
         }
 
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-        public IMediaProjection getProjection(int uid, String packageName) {
+        public IMediaProjection getProjection(int processUid, String packageName) {
             getProjection_enforcePermission();
             if (packageName == null || packageName.isEmpty()) {
                 throw new IllegalArgumentException("package name must not be empty");
@@ -695,7 +695,7 @@
             MediaProjection projection;
             final long callingToken = Binder.clearCallingIdentity();
             try {
-                projection = getProjectionInternal(uid, packageName);
+                projection = getProjectionInternal(processUid, packageName);
             } finally {
                 Binder.restoreCallingIdentity(callingToken);
             }
@@ -869,12 +869,13 @@
 
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-        public void notifyPermissionRequestInitiated(int hostUid, int sessionCreationSource) {
+        public void notifyPermissionRequestInitiated(
+                int hostProcessUid, int sessionCreationSource) {
             notifyPermissionRequestInitiated_enforcePermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 MediaProjectionManagerService.this.notifyPermissionRequestInitiated(
-                        hostUid, sessionCreationSource);
+                        hostProcessUid, sessionCreationSource);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -882,11 +883,11 @@
 
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-        public void notifyPermissionRequestDisplayed(int hostUid) {
+        public void notifyPermissionRequestDisplayed(int hostProcessUid) {
             notifyPermissionRequestDisplayed_enforcePermission();
             final long token = Binder.clearCallingIdentity();
             try {
-                MediaProjectionManagerService.this.notifyPermissionRequestDisplayed(hostUid);
+                MediaProjectionManagerService.this.notifyPermissionRequestDisplayed(hostProcessUid);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -894,11 +895,11 @@
 
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-        public void notifyPermissionRequestCancelled(int hostUid) {
+        public void notifyPermissionRequestCancelled(int hostProcessUid) {
             notifyPermissionRequestCancelled_enforcePermission();
             final long token = Binder.clearCallingIdentity();
             try {
-                MediaProjectionManagerService.this.notifyPermissionRequestCancelled(hostUid);
+                MediaProjectionManagerService.this.notifyPermissionRequestCancelled(hostProcessUid);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -906,11 +907,11 @@
 
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-        public void notifyAppSelectorDisplayed(int hostUid) {
+        public void notifyAppSelectorDisplayed(int hostProcessUid) {
             notifyAppSelectorDisplayed_enforcePermission();
             final long token = Binder.clearCallingIdentity();
             try {
-                MediaProjectionManagerService.this.notifyAppSelectorDisplayed(hostUid);
+                MediaProjectionManagerService.this.notifyAppSelectorDisplayed(hostProcessUid);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -919,12 +920,12 @@
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
         public void notifyWindowingModeChanged(
-                int contentToRecord, int targetUid, int windowingMode) {
+                int contentToRecord, int targetProcessUid, int windowingMode) {
             notifyWindowingModeChanged_enforcePermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 MediaProjectionManagerService.this.notifyWindowingModeChanged(
-                        contentToRecord, targetUid, windowingMode);
+                        contentToRecord, targetProcessUid, windowingMode);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryManager.java b/services/core/java/com/android/server/notification/NotificationHistoryManager.java
index e3880c3..deb95d8 100644
--- a/services/core/java/com/android/server/notification/NotificationHistoryManager.java
+++ b/services/core/java/com/android/server/notification/NotificationHistoryManager.java
@@ -108,7 +108,7 @@
                 for (int i = 0; i < pendingPackageRemovals.size(); i++) {
                     userHistory.onPackageRemoved(pendingPackageRemovals.get(i));
                 }
-                mUserPendingPackageRemovals.put(userId, null);
+                mUserPendingPackageRemovals.remove(userId);
             }
 
             // delete history if it was disabled when the user was locked
@@ -133,7 +133,7 @@
         synchronized (mLock) {
             // Actual data deletion is handled by other parts of the system (the entire directory is
             // removed) - we just need clean up our internal state for GC
-            mUserPendingPackageRemovals.put(userId, null);
+            mUserPendingPackageRemovals.remove(userId);
             mHistoryEnabled.put(userId, false);
             mUserPendingHistoryDisables.put(userId, false);
             onUserStopped(userId);
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 3adeb4b..446c629 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -77,6 +77,7 @@
 import android.os.Environment;
 import android.os.FileUtils;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.LocaleList;
 import android.os.Looper;
@@ -113,7 +114,6 @@
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.infra.AndroidFuture;
 import com.android.internal.logging.MetricsLogger;
-import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
@@ -485,7 +485,14 @@
     }
 
     public ShortcutService(Context context) {
-        this(context, BackgroundThread.get().getLooper(), /*onyForPackgeManagerApis*/ false);
+        this(context, getBgLooper(), /*onyForPackgeManagerApis*/ false);
+    }
+
+    private static Looper getBgLooper() {
+        final HandlerThread handlerThread = new HandlerThread("shortcut",
+                android.os.Process.THREAD_PRIORITY_BACKGROUND);
+        handlerThread.start();
+        return handlerThread.getLooper();
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 49af4fe..c1b7489 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2337,8 +2337,18 @@
         final long identity = Binder.clearCallingIdentity();
         try {
             final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
-            if (telecomManager != null && telecomManager.isInCall()) {
-                flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
+            if (com.android.internal.telephony.flags
+                    .Flags.enforceTelephonyFeatureMappingForPublicApis()) {
+                if (mContext.getPackageManager().hasSystemFeature(
+                        PackageManager.FEATURE_TELECOM)) {
+                    if (telecomManager != null && telecomManager.isInCall()) {
+                        flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
+                    }
+                }
+            } else {
+                if (telecomManager != null && telecomManager.isInCall()) {
+                    flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
+                }
             }
         } finally {
             Binder.restoreCallingIdentity(identity);
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index 8d93408..13f1141 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -1279,8 +1279,8 @@
                 ipw.println();
             }
 
-            PackageWatchdog.getInstance(mContext).dump(ipw);
         });
+        PackageWatchdog.getInstance(mContext).dump(ipw);
     }
 
     @AnyThread
diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
index 58acbe0..c0e3308 100755
--- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java
+++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
@@ -1339,16 +1339,22 @@
                     String inputId = mHardwareInputIdMap.get(deviceId);
 
                     if (inputId != null) {
-                        if (connection.updateCableConnectionStatusLocked(cableConnectionStatus)) {
-                            if (previousCableConnectionStatus != connection.getInputStateLocked()) {
-                                mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
-                                    connection.getInputStateLocked(), 0, inputId).sendToTarget();
-                            }
-                        } else {
-                            if ((previousConfigsLength == 0)
-                                    != (connection.getConfigsLengthLocked() == 0)) {
-                                mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
-                                    connection.getInputStateLocked(), 0, inputId).sendToTarget();
+                        synchronized (mLock) {
+                            if (connection.updateCableConnectionStatusLocked(
+                                        cableConnectionStatus)) {
+                                if (previousCableConnectionStatus
+                                        != connection.getInputStateLocked()) {
+                                    mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
+                                                    connection.getInputStateLocked(), 0, inputId)
+                                            .sendToTarget();
+                                }
+                            } else {
+                                if ((previousConfigsLength == 0)
+                                        != (connection.getConfigsLengthLocked() == 0)) {
+                                    mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
+                                                    connection.getInputStateLocked(), 0, inputId)
+                                            .sendToTarget();
+                                }
                             }
                         }
                     }
diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java
index 9213d96..ed04e5f 100644
--- a/services/core/java/com/android/server/vcn/VcnContext.java
+++ b/services/core/java/com/android/server/vcn/VcnContext.java
@@ -34,6 +34,7 @@
     @NonNull private final Looper mLooper;
     @NonNull private final VcnNetworkProvider mVcnNetworkProvider;
     @NonNull private final FeatureFlags mFeatureFlags;
+    @NonNull private final com.android.net.flags.FeatureFlags mCoreNetFeatureFlags;
     private final boolean mIsInTestMode;
 
     public VcnContext(
@@ -48,6 +49,7 @@
 
         // Auto-generated class
         mFeatureFlags = new FeatureFlagsImpl();
+        mCoreNetFeatureFlags = new com.android.net.flags.FeatureFlagsImpl();
     }
 
     @NonNull
@@ -69,11 +71,23 @@
         return mIsInTestMode;
     }
 
+    public boolean isFlagNetworkMetricMonitorEnabled() {
+        return mFeatureFlags.networkMetricMonitor();
+    }
+
+    public boolean isFlagIpSecTransformStateEnabled() {
+        return mCoreNetFeatureFlags.ipsecTransformState();
+    }
+
     @NonNull
     public FeatureFlags getFeatureFlags() {
         return mFeatureFlags;
     }
 
+    public boolean isFlagSafeModeTimeoutConfigEnabled() {
+        return mFeatureFlags.safeModeTimeoutConfig();
+    }
+
     /**
      * Verifies that the caller is running on the VcnContext Thread.
      *
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 54c97dd..fcc0de1 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -915,9 +915,11 @@
             // TODO(b/180132994): explore safely removing this Thread check
             mVcnContext.ensureRunningOnLooperThread();
 
-            logInfo(
-                    "Selected underlying network changed: "
-                            + (underlying == null ? null : underlying.network));
+            if (!UnderlyingNetworkRecord.isSameNetwork(mUnderlying, underlying)) {
+                logInfo(
+                        "Selected underlying network changed: "
+                                + (underlying == null ? null : underlying.network));
+            }
 
             // TODO(b/179091925): Move the delayed-message handling to BaseState
 
@@ -1242,9 +1244,28 @@
                 createScheduledAlarm(
                         SAFEMODE_TIMEOUT_ALARM,
                         delayedMessage,
-                        mVcnContext.isInTestMode()
-                                ? TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS_TEST_MODE)
-                                : TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS));
+                        getSafeModeTimeoutMs(mVcnContext, mLastSnapshot, mSubscriptionGroup));
+    }
+
+    /** Gets the safe mode timeout */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static long getSafeModeTimeoutMs(
+            VcnContext vcnContext, TelephonySubscriptionSnapshot snapshot, ParcelUuid subGrp) {
+        final int defaultSeconds =
+                vcnContext.isInTestMode()
+                        ? SAFEMODE_TIMEOUT_SECONDS_TEST_MODE
+                        : SAFEMODE_TIMEOUT_SECONDS;
+
+        final PersistableBundleWrapper carrierConfig = snapshot.getCarrierConfigForSubGrp(subGrp);
+        int resultSeconds = defaultSeconds;
+
+        if (vcnContext.isFlagSafeModeTimeoutConfigEnabled() && carrierConfig != null) {
+            resultSeconds =
+                    carrierConfig.getInt(
+                            VcnManager.VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY, defaultSeconds);
+        }
+
+        return TimeUnit.SECONDS.toMillis(resultSeconds);
     }
 
     private void cancelSafeModeAlarm() {
diff --git a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
new file mode 100644
index 0000000..5f4852f
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
@@ -0,0 +1,387 @@
+/*
+ * 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.vcn.routeselection;
+
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.IpSecTransformState;
+import android.net.Network;
+import android.net.vcn.VcnManager;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.OutcomeReceiver;
+import android.os.PowerManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
+import com.android.server.vcn.VcnContext;
+
+import java.util.BitSet;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * IpSecPacketLossDetector is responsible for continuously monitoring IPsec packet loss
+ *
+ * <p>When the packet loss rate surpass the threshold, IpSecPacketLossDetector will report it to the
+ * caller
+ *
+ * <p>IpSecPacketLossDetector will start monitoring when the network being monitored is selected AND
+ * an inbound IpSecTransform has been applied to this network.
+ *
+ * <p>This class is flag gated by "network_metric_monitor" and "ipsec_tramsform_state"
+ */
+public class IpSecPacketLossDetector extends NetworkMetricMonitor {
+    private static final String TAG = IpSecPacketLossDetector.class.getSimpleName();
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int PACKET_LOSS_UNAVALAIBLE = -1;
+
+    // For VoIP, losses between 5% and 10% of the total packet stream will affect the quality
+    // significantly (as per "Computer Networking for LANS to WANS: Hardware, Software and
+    // Security"). For audio and video streaming, above 10-12% packet loss is unacceptable (as per
+    // "ICTP-SDU: About PingER"). Thus choose 12% as a conservative default threshold to declare a
+    // validation failure.
+    private static final int IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT = 12;
+
+    private static final int POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT = 20;
+
+    private long mPollIpSecStateIntervalMs;
+    private final int mPacketLossRatePercentThreshold;
+
+    @NonNull private final Handler mHandler;
+    @NonNull private final PowerManager mPowerManager;
+    @NonNull private final Object mCancellationToken = new Object();
+    @NonNull private final PacketLossCalculator mPacketLossCalculator;
+
+    @Nullable private IpSecTransformWrapper mInboundTransform;
+    @Nullable private IpSecTransformState mLastIpSecTransformState;
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public IpSecPacketLossDetector(
+            @NonNull VcnContext vcnContext,
+            @NonNull Network network,
+            @Nullable PersistableBundleWrapper carrierConfig,
+            @NonNull NetworkMetricMonitorCallback callback,
+            @NonNull Dependencies deps)
+            throws IllegalAccessException {
+        super(vcnContext, network, carrierConfig, callback);
+
+        Objects.requireNonNull(deps, "Missing deps");
+
+        if (!vcnContext.isFlagIpSecTransformStateEnabled()) {
+            // Caller error
+            logWtf("ipsecTransformState flag disabled");
+            throw new IllegalAccessException("ipsecTransformState flag disabled");
+        }
+
+        mHandler = new Handler(getVcnContext().getLooper());
+
+        mPowerManager = getVcnContext().getContext().getSystemService(PowerManager.class);
+
+        mPacketLossCalculator = deps.getPacketLossCalculator();
+
+        mPollIpSecStateIntervalMs = getPollIpSecStateIntervalMs(carrierConfig);
+        mPacketLossRatePercentThreshold = getPacketLossRatePercentThreshold(carrierConfig);
+
+        // Register for system broadcasts to monitor idle mode change
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
+        getVcnContext()
+                .getContext()
+                .registerReceiver(
+                        new BroadcastReceiver() {
+                            @Override
+                            public void onReceive(Context context, Intent intent) {
+                                if (PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(
+                                                intent.getAction())
+                                        && mPowerManager.isDeviceIdleMode()) {
+                                    mLastIpSecTransformState = null;
+                                }
+                            }
+                        },
+                        intentFilter,
+                        null /* broadcastPermission not required */,
+                        mHandler);
+    }
+
+    public IpSecPacketLossDetector(
+            @NonNull VcnContext vcnContext,
+            @NonNull Network network,
+            @Nullable PersistableBundleWrapper carrierConfig,
+            @NonNull NetworkMetricMonitorCallback callback)
+            throws IllegalAccessException {
+        this(vcnContext, network, carrierConfig, callback, new Dependencies());
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static class Dependencies {
+        public PacketLossCalculator getPacketLossCalculator() {
+            return new PacketLossCalculator();
+        }
+    }
+
+    private static long getPollIpSecStateIntervalMs(
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        final int seconds;
+
+        if (carrierConfig != null) {
+            seconds =
+                    carrierConfig.getInt(
+                            VcnManager.VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY,
+                            POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT);
+        } else {
+            seconds = POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT;
+        }
+
+        return TimeUnit.SECONDS.toMillis(seconds);
+    }
+
+    private static int getPacketLossRatePercentThreshold(
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        if (carrierConfig != null) {
+            return carrierConfig.getInt(
+                    VcnManager.VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY,
+                    IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT);
+        }
+        return IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT;
+    }
+
+    @Override
+    protected void onSelectedUnderlyingNetworkChanged() {
+        if (!isSelectedUnderlyingNetwork()) {
+            mInboundTransform = null;
+            stop();
+        }
+
+        // No action when the underlying network got selected. Wait for the inbound transform to
+        // start the monitor
+    }
+
+    @Override
+    public void setInboundTransformInternal(@NonNull IpSecTransformWrapper inboundTransform) {
+        Objects.requireNonNull(inboundTransform, "inboundTransform is null");
+
+        if (Objects.equals(inboundTransform, mInboundTransform)) {
+            return;
+        }
+
+        if (!isSelectedUnderlyingNetwork()) {
+            logWtf("setInboundTransform called but network not selected");
+            return;
+        }
+
+        // When multiple parallel inbound transforms are created, NetworkMetricMonitor will be
+        // enabled on the last one as a sample
+        mInboundTransform = inboundTransform;
+        start();
+    }
+
+    @Override
+    public void setCarrierConfig(@Nullable PersistableBundleWrapper carrierConfig) {
+        // The already scheduled event will not be affected. The followup events will be scheduled
+        // with the new interval
+        mPollIpSecStateIntervalMs = getPollIpSecStateIntervalMs(carrierConfig);
+    }
+
+    @Override
+    protected void start() {
+        super.start();
+        clearTransformStateAndPollingEvents();
+        mHandler.postDelayed(new PollIpSecStateRunnable(), mCancellationToken, 0L);
+    }
+
+    @Override
+    public void stop() {
+        super.stop();
+        clearTransformStateAndPollingEvents();
+    }
+
+    private void clearTransformStateAndPollingEvents() {
+        mHandler.removeCallbacksAndEqualMessages(mCancellationToken);
+        mLastIpSecTransformState = null;
+    }
+
+    @Override
+    public void close() {
+        super.close();
+
+        if (mInboundTransform != null) {
+            mInboundTransform.close();
+        }
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    @Nullable
+    public IpSecTransformState getLastTransformState() {
+        return mLastIpSecTransformState;
+    }
+
+    @VisibleForTesting(visibility = Visibility.PROTECTED)
+    @Nullable
+    public IpSecTransformWrapper getInboundTransformInternal() {
+        return mInboundTransform;
+    }
+
+    private class PollIpSecStateRunnable implements Runnable {
+        @Override
+        public void run() {
+            if (!isStarted()) {
+                logWtf("Monitor stopped but PollIpSecStateRunnable not removed from Handler");
+                return;
+            }
+
+            getInboundTransformInternal()
+                    .getIpSecTransformState(
+                            new HandlerExecutor(mHandler), new IpSecTransformStateReceiver());
+
+            // Schedule for next poll
+            mHandler.postDelayed(
+                    new PollIpSecStateRunnable(), mCancellationToken, mPollIpSecStateIntervalMs);
+        }
+    }
+
+    private class IpSecTransformStateReceiver
+            implements OutcomeReceiver<IpSecTransformState, RuntimeException> {
+        @Override
+        public void onResult(@NonNull IpSecTransformState state) {
+            getVcnContext().ensureRunningOnLooperThread();
+
+            if (!isStarted()) {
+                return;
+            }
+
+            onIpSecTransformStateReceived(state);
+        }
+
+        @Override
+        public void onError(@NonNull RuntimeException error) {
+            getVcnContext().ensureRunningOnLooperThread();
+
+            // Nothing we can do here
+            logW("TransformStateReceiver#onError " + error.toString());
+        }
+    }
+
+    private void onIpSecTransformStateReceived(@NonNull IpSecTransformState state) {
+        if (mLastIpSecTransformState == null) {
+            // This is first time to poll the state
+            mLastIpSecTransformState = state;
+            return;
+        }
+
+        final int packetLossRate =
+                mPacketLossCalculator.getPacketLossRatePercentage(
+                        mLastIpSecTransformState, state, getLogPrefix());
+
+        if (packetLossRate == PACKET_LOSS_UNAVALAIBLE) {
+            return;
+        }
+
+        final String logMsg =
+                "packetLossRate: "
+                        + packetLossRate
+                        + "% in the past "
+                        + (state.getTimestamp() - mLastIpSecTransformState.getTimestamp())
+                        + "ms";
+
+        mLastIpSecTransformState = state;
+        if (packetLossRate < mPacketLossRatePercentThreshold) {
+            logV(logMsg);
+            onValidationResultReceivedInternal(false /* isFailed */);
+        } else {
+            logInfo(logMsg);
+            onValidationResultReceivedInternal(true /* isFailed */);
+        }
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static class PacketLossCalculator {
+        /** Calculate the packet loss rate between two timestamps */
+        public int getPacketLossRatePercentage(
+                @NonNull IpSecTransformState oldState,
+                @NonNull IpSecTransformState newState,
+                String logPrefix) {
+            logVIpSecTransform("oldState", oldState, logPrefix);
+            logVIpSecTransform("newState", newState, logPrefix);
+
+            final int replayWindowSize = oldState.getReplayBitmap().length * 8;
+            final long oldSeqHi = oldState.getRxHighestSequenceNumber();
+            final long oldSeqLow = Math.max(0L, oldSeqHi - replayWindowSize + 1);
+            final long newSeqHi = newState.getRxHighestSequenceNumber();
+            final long newSeqLow = Math.max(0L, newSeqHi - replayWindowSize + 1);
+
+            if (oldSeqHi == newSeqHi || newSeqHi < replayWindowSize) {
+                // The replay window did not proceed and all packets might have been delivered out
+                // of order
+                return PACKET_LOSS_UNAVALAIBLE;
+            }
+
+            // Get the expected packet count by assuming there is no packet loss. In this case, SA
+            // should receive all packets whose sequence numbers are smaller than the lower bound of
+            // the replay window AND the packets received within the window.
+            // When the lower bound is 0, it's not possible to tell whether packet with seqNo 0 is
+            // received or not. For simplicity just assume that packet is received.
+            final long newExpectedPktCnt = newSeqLow + getPacketCntInReplayWindow(newState);
+            final long oldExpectedPktCnt = oldSeqLow + getPacketCntInReplayWindow(oldState);
+
+            final long expectedPktCntDiff = newExpectedPktCnt - oldExpectedPktCnt;
+            final long actualPktCntDiff = newState.getPacketCount() - oldState.getPacketCount();
+
+            logV(
+                    TAG,
+                    logPrefix
+                            + " expectedPktCntDiff: "
+                            + expectedPktCntDiff
+                            + " actualPktCntDiff: "
+                            + actualPktCntDiff);
+
+            if (expectedPktCntDiff < 0
+                    || expectedPktCntDiff == 0
+                    || actualPktCntDiff < 0
+                    || actualPktCntDiff > expectedPktCntDiff) {
+                logWtf(TAG, "Impossible values for expectedPktCntDiff or" + " actualPktCntDiff");
+                return PACKET_LOSS_UNAVALAIBLE;
+            }
+
+            return 100 - (int) (actualPktCntDiff * 100 / expectedPktCntDiff);
+        }
+    }
+
+    private static void logVIpSecTransform(
+            String transformTag, IpSecTransformState state, String logPrefix) {
+        final String stateString =
+                " seqNo: "
+                        + state.getRxHighestSequenceNumber()
+                        + " | pktCnt: "
+                        + state.getPacketCount()
+                        + " | pktCntInWindow: "
+                        + getPacketCntInReplayWindow(state);
+        logV(TAG, logPrefix + " " + transformTag + stateString);
+    }
+
+    /** Get the number of received packets within the replay window */
+    private static long getPacketCntInReplayWindow(@NonNull IpSecTransformState state) {
+        return BitSet.valueOf(state.getReplayBitmap()).cardinality();
+    }
+}
diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
new file mode 100644
index 0000000..a79f188
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
@@ -0,0 +1,269 @@
+/*
+ * 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.vcn.routeselection;
+
+import static com.android.server.VcnManagementService.LOCAL_LOG;
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.IpSecTransform;
+import android.net.IpSecTransformState;
+import android.net.Network;
+import android.os.OutcomeReceiver;
+import android.util.CloseGuard;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
+import com.android.server.vcn.VcnContext;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * NetworkMetricMonitor is responsible for managing metric monitoring and tracking validation
+ * results.
+ *
+ * <p>This class is flag gated by "network_metric_monitor"
+ */
+public abstract class NetworkMetricMonitor implements AutoCloseable {
+    private static final String TAG = NetworkMetricMonitor.class.getSimpleName();
+
+    private static final boolean VDBG = false; // STOPSHIP: if true
+
+    @NonNull private final CloseGuard mCloseGuard = new CloseGuard();
+
+    @NonNull private final VcnContext mVcnContext;
+    @NonNull private final Network mNetwork;
+    @NonNull private final NetworkMetricMonitorCallback mCallback;
+
+    private boolean mIsSelectedUnderlyingNetwork;
+    private boolean mIsStarted;
+    private boolean mIsValidationFailed;
+
+    protected NetworkMetricMonitor(
+            @NonNull VcnContext vcnContext,
+            @NonNull Network network,
+            @Nullable PersistableBundleWrapper carrierConfig,
+            @NonNull NetworkMetricMonitorCallback callback)
+            throws IllegalAccessException {
+        if (!vcnContext.isFlagNetworkMetricMonitorEnabled()) {
+            // Caller error
+            logWtf("networkMetricMonitor flag disabled");
+            throw new IllegalAccessException("networkMetricMonitor flag disabled");
+        }
+
+        mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
+        mNetwork = Objects.requireNonNull(network, "Missing network");
+        mCallback = Objects.requireNonNull(callback, "Missing callback");
+
+        mIsSelectedUnderlyingNetwork = false;
+        mIsStarted = false;
+        mIsValidationFailed = false;
+    }
+
+    /** Callback to notify caller of the validation result */
+    public interface NetworkMetricMonitorCallback {
+        /** Called when there is a validation result is ready */
+        void onValidationResultReceived();
+    }
+
+    /**
+     * Start monitoring
+     *
+     * <p>This method might be called on a an already started monitor for updating monitor
+     * properties (e.g. IpSecTransform, carrier config)
+     *
+     * <p>Subclasses MUST call super.start() when overriding this method
+     */
+    protected void start() {
+        mIsStarted = true;
+    }
+
+    /**
+     * Stop monitoring
+     *
+     * <p>Subclasses MUST call super.stop() when overriding this method
+     */
+    public void stop() {
+        mIsValidationFailed = false;
+        mIsStarted = false;
+    }
+
+    /** Called by the subclasses when the validation result is ready */
+    protected void onValidationResultReceivedInternal(boolean isFailed) {
+        mIsValidationFailed = isFailed;
+        mCallback.onValidationResultReceived();
+    }
+
+    /** Called when the underlying network changes to selected or unselected */
+    protected abstract void onSelectedUnderlyingNetworkChanged();
+
+    /**
+     * Mark the network being monitored selected or unselected
+     *
+     * <p>Subclasses MUST call super when overriding this method
+     */
+    public void setIsSelectedUnderlyingNetwork(boolean isSelectedUnderlyingNetwork) {
+        if (mIsSelectedUnderlyingNetwork == isSelectedUnderlyingNetwork) {
+            return;
+        }
+
+        mIsSelectedUnderlyingNetwork = isSelectedUnderlyingNetwork;
+        onSelectedUnderlyingNetworkChanged();
+    }
+
+    /** Wrapper that allows injection for testing purposes */
+    @VisibleForTesting(visibility = Visibility.PROTECTED)
+    public static class IpSecTransformWrapper {
+        @NonNull public final IpSecTransform ipSecTransform;
+
+        public IpSecTransformWrapper(@NonNull IpSecTransform ipSecTransform) {
+            this.ipSecTransform = ipSecTransform;
+        }
+
+        /** Poll an IpSecTransformState */
+        public void getIpSecTransformState(
+                @NonNull Executor executor,
+                @NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
+            ipSecTransform.getIpSecTransformState(executor, callback);
+        }
+
+        /** Close this instance and release the underlying resources */
+        public void close() {
+            ipSecTransform.close();
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(ipSecTransform);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof IpSecTransformWrapper)) {
+                return false;
+            }
+
+            final IpSecTransformWrapper other = (IpSecTransformWrapper) o;
+
+            return Objects.equals(ipSecTransform, other.ipSecTransform);
+        }
+    }
+
+    /** Set the IpSecTransform that applied to the Network being monitored */
+    public void setInboundTransform(@NonNull IpSecTransform inTransform) {
+        setInboundTransformInternal(new IpSecTransformWrapper(inTransform));
+    }
+
+    /**
+     * Set the IpSecTransform that applied to the Network being monitored *
+     *
+     * <p>Subclasses MUST call super when overriding this method
+     */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public void setInboundTransformInternal(@NonNull IpSecTransformWrapper inTransform) {
+        // Subclasses MUST override it if they care
+    }
+
+    /** Update the carrierconfig */
+    public void setCarrierConfig(@Nullable PersistableBundleWrapper carrierConfig) {
+        // Subclasses MUST override it if they care
+    }
+
+    public boolean isValidationFailed() {
+        return mIsValidationFailed;
+    }
+
+    public boolean isSelectedUnderlyingNetwork() {
+        return mIsSelectedUnderlyingNetwork;
+    }
+
+    public boolean isStarted() {
+        return mIsStarted;
+    }
+
+    @NonNull
+    public VcnContext getVcnContext() {
+        return mVcnContext;
+    }
+
+    // Override methods for AutoCloseable. Subclasses MUST call super when overriding this method
+    @Override
+    public void close() {
+        mCloseGuard.close();
+
+        stop();
+    }
+
+    // Override #finalize() to use closeGuard for flagging that #close() was not called
+    @SuppressWarnings("Finalize")
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+            close();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    private String getClassName() {
+        return this.getClass().getSimpleName();
+    }
+
+    protected String getLogPrefix() {
+        return " [Network " + mNetwork + "] ";
+    }
+
+    protected void logV(String msg) {
+        if (VDBG) {
+            Slog.v(getClassName(), getLogPrefix() + msg);
+            LOCAL_LOG.log("[VERBOSE ] " + getClassName() + getLogPrefix() + msg);
+        }
+    }
+
+    protected void logInfo(String msg) {
+        Slog.i(getClassName(), getLogPrefix() + msg);
+        LOCAL_LOG.log("[INFO ] " + getClassName() + getLogPrefix() + msg);
+    }
+
+    protected void logW(String msg) {
+        Slog.w(getClassName(), getLogPrefix() + msg);
+        LOCAL_LOG.log("[WARN ] " + getClassName() + getLogPrefix() + msg);
+    }
+
+    protected void logWtf(String msg) {
+        Slog.wtf(getClassName(), getLogPrefix() + msg);
+        LOCAL_LOG.log("[WTF ] " + getClassName() + getLogPrefix() + msg);
+    }
+
+    protected static void logV(String className, String msgWithPrefix) {
+        if (VDBG) {
+            Slog.wtf(className, msgWithPrefix);
+            LOCAL_LOG.log("[VERBOSE ] " + className + msgWithPrefix);
+        }
+    }
+
+    protected static void logWtf(String className, String msgWithPrefix) {
+        Slog.wtf(className, msgWithPrefix);
+        LOCAL_LOG.log("[WTF ] " + className + msgWithPrefix);
+    }
+}
diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
index 7f129ea..d32e5cc 100644
--- a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
+++ b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
@@ -47,7 +47,6 @@
 
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 
 /** @hide */
@@ -86,7 +85,6 @@
      * <p>VCN MUST never select a non-INTERNET network that are unvalidated or fail to match any
      * template as the underlying network.
      */
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
     static final int PRIORITY_INVALID = -1;
 
     /** Gives networks a priority class, based on configured VcnGatewayConnectionConfig */
@@ -96,7 +94,7 @@
             List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
-            UnderlyingNetworkRecord currentlySelected,
+            boolean isSelected,
             PersistableBundleWrapper carrierConfig) {
         // mRouteSelectionNetworkRequest requires a network be both VALIDATED and NOT_SUSPENDED
 
@@ -118,7 +116,7 @@
                     networkRecord,
                     subscriptionGroup,
                     snapshot,
-                    currentlySelected,
+                    isSelected,
                     carrierConfig)) {
                 return priorityIndex;
             }
@@ -140,12 +138,9 @@
             UnderlyingNetworkRecord networkRecord,
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
-            UnderlyingNetworkRecord currentlySelected,
+            boolean isSelected,
             PersistableBundleWrapper carrierConfig) {
         final NetworkCapabilities caps = networkRecord.networkCapabilities;
-        final boolean isSelectedUnderlyingNetwork =
-                currentlySelected != null
-                        && Objects.equals(currentlySelected.network, networkRecord.network);
 
         final int meteredMatch = networkPriority.getMetered();
         final boolean isMetered = !caps.hasCapability(NET_CAPABILITY_NOT_METERED);
@@ -159,7 +154,7 @@
         if (caps.getLinkUpstreamBandwidthKbps() < networkPriority.getMinExitUpstreamBandwidthKbps()
                 || (caps.getLinkUpstreamBandwidthKbps()
                                 < networkPriority.getMinEntryUpstreamBandwidthKbps()
-                        && !isSelectedUnderlyingNetwork)) {
+                        && !isSelected)) {
             return false;
         }
 
@@ -167,7 +162,7 @@
                         < networkPriority.getMinExitDownstreamBandwidthKbps()
                 || (caps.getLinkDownstreamBandwidthKbps()
                                 < networkPriority.getMinEntryDownstreamBandwidthKbps()
-                        && !isSelectedUnderlyingNetwork)) {
+                        && !isSelected)) {
             return false;
         }
 
@@ -191,7 +186,7 @@
             return checkMatchesWifiPriorityRule(
                     (VcnWifiUnderlyingNetworkTemplate) networkPriority,
                     networkRecord,
-                    currentlySelected,
+                    isSelected,
                     carrierConfig);
         }
 
@@ -214,7 +209,7 @@
     public static boolean checkMatchesWifiPriorityRule(
             VcnWifiUnderlyingNetworkTemplate networkPriority,
             UnderlyingNetworkRecord networkRecord,
-            UnderlyingNetworkRecord currentlySelected,
+            boolean isSelected,
             PersistableBundleWrapper carrierConfig) {
         final NetworkCapabilities caps = networkRecord.networkCapabilities;
 
@@ -223,7 +218,7 @@
         }
 
         // TODO: Move the Network Quality check to the network metric monitor framework.
-        if (!isWifiRssiAcceptable(networkRecord, currentlySelected, carrierConfig)) {
+        if (!isWifiRssiAcceptable(networkRecord, isSelected, carrierConfig)) {
             return false;
         }
 
@@ -237,15 +232,11 @@
 
     private static boolean isWifiRssiAcceptable(
             UnderlyingNetworkRecord networkRecord,
-            UnderlyingNetworkRecord currentlySelected,
+            boolean isSelected,
             PersistableBundleWrapper carrierConfig) {
         final NetworkCapabilities caps = networkRecord.networkCapabilities;
-        final boolean isSelectedNetwork =
-                currentlySelected != null
-                        && networkRecord.network.equals(currentlySelected.network);
 
-        if (isSelectedNetwork
-                && caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)) {
+        if (isSelected && caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)) {
             return true;
         }
 
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
index 6afa795..48df44b 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
@@ -48,6 +48,7 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.VcnContext;
@@ -83,6 +84,9 @@
     @NonNull private final TelephonyCallback mActiveDataSubIdListener =
             new VcnActiveDataSubscriptionIdListener();
 
+    private final Map<Network, UnderlyingNetworkEvaluator> mUnderlyingNetworkRecords =
+            new ArrayMap<>();
+
     @NonNull private final List<NetworkCallback> mCellBringupCallbacks = new ArrayList<>();
     @Nullable private NetworkCallback mWifiBringupCallback;
     @Nullable private NetworkCallback mWifiEntryRssiThresholdCallback;
@@ -105,7 +109,8 @@
         this(vcnContext, connectionConfig, subscriptionGroup, snapshot, cb, new Dependencies());
     }
 
-    private UnderlyingNetworkController(
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    UnderlyingNetworkController(
             @NonNull VcnContext vcnContext,
             @NonNull VcnGatewayConnectionConfig connectionConfig,
             @NonNull ParcelUuid subscriptionGroup,
@@ -196,6 +201,7 @@
         NetworkCallback oldWifiExitRssiThresholdCallback = mWifiExitRssiThresholdCallback;
         List<NetworkCallback> oldCellCallbacks = new ArrayList<>(mCellBringupCallbacks);
         mCellBringupCallbacks.clear();
+        mUnderlyingNetworkRecords.clear();
 
         // Register new callbacks. Make-before-break; always register new callbacks before removal
         // of old callbacks
@@ -395,6 +401,18 @@
         // Update carrier config
         mCarrierConfig = mLastSnapshot.getCarrierConfigForSubGrp(mSubscriptionGroup);
 
+        // Make sure all evaluators use the same updated TelephonySubscriptionSnapshot and carrier
+        // config to calculate their cached priority classes. For simplicity, the
+        // UnderlyingNetworkController does not listen for changes in VCN-related carrier config
+        // keys, and changes are applied at restart of the VcnGatewayConnection
+        for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) {
+            evaluator.reevaluate(
+                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                    mSubscriptionGroup,
+                    mLastSnapshot,
+                    mCarrierConfig);
+        }
+
         // Only trigger re-registration if subIds in this group have changed
         if (oldSnapshot
                 .getAllSubIdsInGroup(mSubscriptionGroup)
@@ -418,32 +436,62 @@
                 .unregisterTelephonyCallback(mActiveDataSubIdListener);
     }
 
+    private TreeSet<UnderlyingNetworkEvaluator> getSortedUnderlyingNetworks() {
+        TreeSet<UnderlyingNetworkEvaluator> sorted =
+                new TreeSet<>(UnderlyingNetworkEvaluator.getComparator());
+
+        for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) {
+            if (evaluator.getPriorityClass() != NetworkPriorityClassifier.PRIORITY_INVALID) {
+                sorted.add(evaluator);
+            }
+        }
+
+        return sorted;
+    }
+
     private void reevaluateNetworks() {
         if (mIsQuitting || mRouteSelectionCallback == null) {
             return; // UnderlyingNetworkController has quit.
         }
 
-        TreeSet<UnderlyingNetworkRecord> sorted =
-                mRouteSelectionCallback.getSortedUnderlyingNetworks();
-        UnderlyingNetworkRecord candidate = sorted.isEmpty() ? null : sorted.first();
+        TreeSet<UnderlyingNetworkEvaluator> sorted = getSortedUnderlyingNetworks();
+
+        UnderlyingNetworkEvaluator candidateEvaluator = sorted.isEmpty() ? null : sorted.first();
+        UnderlyingNetworkRecord candidate =
+                candidateEvaluator == null ? null : candidateEvaluator.getNetworkRecord();
         if (Objects.equals(mCurrentRecord, candidate)) {
             return;
         }
 
         String allNetworkPriorities = "";
-        for (UnderlyingNetworkRecord record : sorted) {
+        for (UnderlyingNetworkEvaluator recordEvaluator : sorted) {
             if (!allNetworkPriorities.isEmpty()) {
                 allNetworkPriorities += ", ";
             }
-            allNetworkPriorities += record.network + ": " + record.priorityClass;
+            allNetworkPriorities +=
+                    recordEvaluator.getNetwork() + ": " + recordEvaluator.getPriorityClass();
         }
-        logInfo(
-                "Selected network changed to "
-                        + (candidate == null ? null : candidate.network)
-                        + ", selected from list: "
-                        + allNetworkPriorities);
+
+        if (!UnderlyingNetworkRecord.isSameNetwork(mCurrentRecord, candidate)) {
+            logInfo(
+                    "Selected network changed to "
+                            + (candidate == null ? null : candidate.network)
+                            + ", selected from list: "
+                            + allNetworkPriorities);
+        }
+
         mCurrentRecord = candidate;
         mCb.onSelectedUnderlyingNetworkChanged(mCurrentRecord);
+
+        // Need to update all evaluators to ensure the previously selected one is unselected
+        for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) {
+            evaluator.setIsSelected(
+                    candidateEvaluator == evaluator,
+                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                    mSubscriptionGroup,
+                    mLastSnapshot,
+                    mCarrierConfig);
+        }
     }
 
     /**
@@ -463,46 +511,26 @@
      */
     @VisibleForTesting
     class UnderlyingNetworkListener extends NetworkCallback {
-        private final Map<Network, UnderlyingNetworkRecord.Builder>
-                mUnderlyingNetworkRecordBuilders = new ArrayMap<>();
-
         UnderlyingNetworkListener() {
             super(NetworkCallback.FLAG_INCLUDE_LOCATION_INFO);
         }
 
-        private TreeSet<UnderlyingNetworkRecord> getSortedUnderlyingNetworks() {
-            TreeSet<UnderlyingNetworkRecord> sorted =
-                    new TreeSet<>(UnderlyingNetworkRecord.getComparator());
-
-            for (UnderlyingNetworkRecord.Builder builder :
-                    mUnderlyingNetworkRecordBuilders.values()) {
-                if (builder.isValid()) {
-                    final UnderlyingNetworkRecord record =
-                            builder.build(
-                                    mVcnContext,
-                                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
-                                    mSubscriptionGroup,
-                                    mLastSnapshot,
-                                    mCurrentRecord,
-                                    mCarrierConfig);
-                    if (record.priorityClass != NetworkPriorityClassifier.PRIORITY_INVALID) {
-                        sorted.add(record);
-                    }
-                }
-            }
-
-            return sorted;
-        }
-
         @Override
         public void onAvailable(@NonNull Network network) {
-            mUnderlyingNetworkRecordBuilders.put(
-                    network, new UnderlyingNetworkRecord.Builder(network));
+            mUnderlyingNetworkRecords.put(
+                    network,
+                    mDeps.newUnderlyingNetworkEvaluator(
+                            mVcnContext,
+                            network,
+                            mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                            mSubscriptionGroup,
+                            mLastSnapshot,
+                            mCarrierConfig));
         }
 
         @Override
         public void onLost(@NonNull Network network) {
-            mUnderlyingNetworkRecordBuilders.remove(network);
+            mUnderlyingNetworkRecords.remove(network);
 
             reevaluateNetworks();
         }
@@ -510,15 +538,20 @@
         @Override
         public void onCapabilitiesChanged(
                 @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
-            final UnderlyingNetworkRecord.Builder builder =
-                    mUnderlyingNetworkRecordBuilders.get(network);
-            if (builder == null) {
+            final UnderlyingNetworkEvaluator evaluator = mUnderlyingNetworkRecords.get(network);
+            if (evaluator == null) {
                 logWtf("Got capabilities change for unknown key: " + network);
                 return;
             }
 
-            builder.setNetworkCapabilities(networkCapabilities);
-            if (builder.isValid()) {
+            evaluator.setNetworkCapabilities(
+                    networkCapabilities,
+                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                    mSubscriptionGroup,
+                    mLastSnapshot,
+                    mCarrierConfig);
+
+            if (evaluator.isValid()) {
                 reevaluateNetworks();
             }
         }
@@ -526,30 +559,40 @@
         @Override
         public void onLinkPropertiesChanged(
                 @NonNull Network network, @NonNull LinkProperties linkProperties) {
-            final UnderlyingNetworkRecord.Builder builder =
-                    mUnderlyingNetworkRecordBuilders.get(network);
-            if (builder == null) {
+            final UnderlyingNetworkEvaluator evaluator = mUnderlyingNetworkRecords.get(network);
+            if (evaluator == null) {
                 logWtf("Got link properties change for unknown key: " + network);
                 return;
             }
 
-            builder.setLinkProperties(linkProperties);
-            if (builder.isValid()) {
+            evaluator.setLinkProperties(
+                    linkProperties,
+                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                    mSubscriptionGroup,
+                    mLastSnapshot,
+                    mCarrierConfig);
+
+            if (evaluator.isValid()) {
                 reevaluateNetworks();
             }
         }
 
         @Override
         public void onBlockedStatusChanged(@NonNull Network network, boolean isBlocked) {
-            final UnderlyingNetworkRecord.Builder builder =
-                    mUnderlyingNetworkRecordBuilders.get(network);
-            if (builder == null) {
+            final UnderlyingNetworkEvaluator evaluator = mUnderlyingNetworkRecords.get(network);
+            if (evaluator == null) {
                 logWtf("Got blocked status change for unknown key: " + network);
                 return;
             }
 
-            builder.setIsBlocked(isBlocked);
-            if (builder.isValid()) {
+            evaluator.setIsBlocked(
+                    isBlocked,
+                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                    mSubscriptionGroup,
+                    mLastSnapshot,
+                    mCarrierConfig);
+
+            if (evaluator.isValid()) {
                 reevaluateNetworks();
             }
         }
@@ -614,16 +657,8 @@
         pw.println("Underlying networks:");
         pw.increaseIndent();
         if (mRouteSelectionCallback != null) {
-            for (UnderlyingNetworkRecord record :
-                    mRouteSelectionCallback.getSortedUnderlyingNetworks()) {
-                record.dump(
-                        mVcnContext,
-                        pw,
-                        mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
-                        mSubscriptionGroup,
-                        mLastSnapshot,
-                        mCurrentRecord,
-                        mCarrierConfig);
+            for (UnderlyingNetworkEvaluator recordEvaluator : getSortedUnderlyingNetworks()) {
+                recordEvaluator.dump(pw);
             }
         }
         pw.decreaseIndent();
@@ -653,5 +688,23 @@
                 @Nullable UnderlyingNetworkRecord underlyingNetworkRecord);
     }
 
-    private static class Dependencies {}
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static class Dependencies {
+        /** Construct a new UnderlyingNetworkEvaluator */
+        public UnderlyingNetworkEvaluator newUnderlyingNetworkEvaluator(
+                @NonNull VcnContext vcnContext,
+                @NonNull Network network,
+                @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+                @NonNull ParcelUuid subscriptionGroup,
+                @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+                @Nullable PersistableBundleWrapper carrierConfig) {
+            return new UnderlyingNetworkEvaluator(
+                    vcnContext,
+                    network,
+                    underlyingNetworkTemplates,
+                    subscriptionGroup,
+                    lastSnapshot,
+                    carrierConfig);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
new file mode 100644
index 0000000..c124a19
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.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.vcn.routeselection;
+
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.vcn.VcnUnderlyingNetworkTemplate;
+import android.os.ParcelUuid;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
+import com.android.server.vcn.VcnContext;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * UnderlyingNetworkEvaluator evaluates the quality and priority class of a network candidate for
+ * route selection.
+ *
+ * @hide
+ */
+public class UnderlyingNetworkEvaluator {
+    private static final String TAG = UnderlyingNetworkEvaluator.class.getSimpleName();
+
+    @NonNull private final VcnContext mVcnContext;
+    @NonNull private final UnderlyingNetworkRecord.Builder mNetworkRecordBuilder;
+
+    private boolean mIsSelected;
+    private int mPriorityClass = NetworkPriorityClassifier.PRIORITY_INVALID;
+
+    public UnderlyingNetworkEvaluator(
+            @NonNull VcnContext vcnContext,
+            @NonNull Network network,
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
+
+        Objects.requireNonNull(underlyingNetworkTemplates, "Missing underlyingNetworkTemplates");
+        Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup");
+        Objects.requireNonNull(lastSnapshot, "Missing lastSnapshot");
+
+        mNetworkRecordBuilder =
+                new UnderlyingNetworkRecord.Builder(
+                        Objects.requireNonNull(network, "Missing network"));
+        mIsSelected = false;
+
+        updatePriorityClass(
+                underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+    }
+
+    private void updatePriorityClass(
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        if (mNetworkRecordBuilder.isValid()) {
+            mPriorityClass =
+                    NetworkPriorityClassifier.calculatePriorityClass(
+                            mVcnContext,
+                            mNetworkRecordBuilder.build(),
+                            underlyingNetworkTemplates,
+                            subscriptionGroup,
+                            lastSnapshot,
+                            mIsSelected,
+                            carrierConfig);
+        } else {
+            mPriorityClass = NetworkPriorityClassifier.PRIORITY_INVALID;
+        }
+    }
+
+    public static Comparator<UnderlyingNetworkEvaluator> getComparator() {
+        return (left, right) -> {
+            final int leftIndex = left.mPriorityClass;
+            final int rightIndex = right.mPriorityClass;
+
+            // In the case of networks in the same priority class, prioritize based on other
+            // criteria (eg. actively selected network, link metrics, etc)
+            if (leftIndex == rightIndex) {
+                // TODO: Improve the strategy of network selection when both UnderlyingNetworkRecord
+                // fall into the same priority class.
+                if (left.mIsSelected) {
+                    return -1;
+                }
+                if (right.mIsSelected) {
+                    return 1;
+                }
+            }
+            return Integer.compare(leftIndex, rightIndex);
+        };
+    }
+
+    /** Set the NetworkCapabilities */
+    public void setNetworkCapabilities(
+            @NonNull NetworkCapabilities nc,
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        mNetworkRecordBuilder.setNetworkCapabilities(nc);
+
+        updatePriorityClass(
+                underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+    }
+
+    /** Set the LinkProperties */
+    public void setLinkProperties(
+            @NonNull LinkProperties lp,
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        mNetworkRecordBuilder.setLinkProperties(lp);
+
+        updatePriorityClass(
+                underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+    }
+
+    /** Set whether the network is blocked */
+    public void setIsBlocked(
+            boolean isBlocked,
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        mNetworkRecordBuilder.setIsBlocked(isBlocked);
+
+        updatePriorityClass(
+                underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+    }
+
+    /** Set whether the network is selected as VCN's underlying network */
+    public void setIsSelected(
+            boolean isSelected,
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        mIsSelected = isSelected;
+
+        updatePriorityClass(
+                underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+    }
+
+    /**
+     * Update the last TelephonySubscriptionSnapshot and carrier config to reevaluate the network
+     */
+    public void reevaluate(
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig) {
+        updatePriorityClass(
+                underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+    }
+
+    /** Return whether this network evaluator is valid */
+    public boolean isValid() {
+        return mNetworkRecordBuilder.isValid();
+    }
+
+    /** Return the network */
+    public Network getNetwork() {
+        return mNetworkRecordBuilder.getNetwork();
+    }
+
+    /** Return the network record */
+    public UnderlyingNetworkRecord getNetworkRecord() {
+        return mNetworkRecordBuilder.build();
+    }
+
+    /** Return the priority class for network selection */
+    public int getPriorityClass() {
+        return mPriorityClass;
+    }
+
+    /** Dump the information of this instance */
+    public void dump(IndentingPrintWriter pw) {
+        pw.println("UnderlyingNetworkEvaluator:");
+        pw.increaseIndent();
+
+        if (mNetworkRecordBuilder.isValid()) {
+            getNetworkRecord().dump(pw);
+        } else {
+            pw.println(
+                    "UnderlyingNetworkRecord incomplete: mNetwork: "
+                            + mNetworkRecordBuilder.getNetwork());
+        }
+
+        pw.println("mIsSelected: " + mIsSelected);
+        pw.println("mPriorityClass: " + mPriorityClass);
+
+        pw.decreaseIndent();
+    }
+}
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
index aea9f4d..7ab8e55 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
@@ -16,24 +16,17 @@
 
 package com.android.server.vcn.routeselection;
 
-import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
-import android.net.vcn.VcnUnderlyingNetworkTemplate;
-import android.os.ParcelUuid;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
-import com.android.server.vcn.VcnContext;
 
-import java.util.Comparator;
-import java.util.List;
 import java.util.Objects;
 
 /**
@@ -46,54 +39,17 @@
     @NonNull public final NetworkCapabilities networkCapabilities;
     @NonNull public final LinkProperties linkProperties;
     public final boolean isBlocked;
-    public final boolean isSelected;
-    public final int priorityClass;
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     public UnderlyingNetworkRecord(
             @NonNull Network network,
             @NonNull NetworkCapabilities networkCapabilities,
             @NonNull LinkProperties linkProperties,
-            boolean isBlocked,
-            VcnContext vcnContext,
-            List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
-            ParcelUuid subscriptionGroup,
-            TelephonySubscriptionSnapshot snapshot,
-            UnderlyingNetworkRecord currentlySelected,
-            PersistableBundleWrapper carrierConfig) {
+            boolean isBlocked) {
         this.network = network;
         this.networkCapabilities = networkCapabilities;
         this.linkProperties = linkProperties;
         this.isBlocked = isBlocked;
-
-        this.isSelected = isSelected(this.network, currentlySelected);
-
-        priorityClass =
-                NetworkPriorityClassifier.calculatePriorityClass(
-                        vcnContext,
-                        this,
-                        underlyingNetworkTemplates,
-                        subscriptionGroup,
-                        snapshot,
-                        currentlySelected,
-                        carrierConfig);
-    }
-
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
-    public UnderlyingNetworkRecord(
-            @NonNull Network network,
-            @NonNull NetworkCapabilities networkCapabilities,
-            @NonNull LinkProperties linkProperties,
-            boolean isBlocked,
-            boolean isSelected,
-            int priorityClass) {
-        this.network = network;
-        this.networkCapabilities = networkCapabilities;
-        this.linkProperties = linkProperties;
-        this.isBlocked = isBlocked;
-        this.isSelected = isSelected;
-
-        this.priorityClass = priorityClass;
     }
 
     @Override
@@ -113,64 +69,20 @@
         return Objects.hash(network, networkCapabilities, linkProperties, isBlocked);
     }
 
-    /** Returns if two records are equal including their priority classes. */
-    public static boolean isEqualIncludingPriorities(
-            UnderlyingNetworkRecord left, UnderlyingNetworkRecord right) {
-        if (left != null && right != null) {
-            return left.equals(right)
-                    && left.isSelected == right.isSelected
-                    && left.priorityClass == right.priorityClass;
-        }
-
-        return left == right;
-    }
-
-    static Comparator<UnderlyingNetworkRecord> getComparator() {
-        return (left, right) -> {
-            final int leftIndex = left.priorityClass;
-            final int rightIndex = right.priorityClass;
-
-            // In the case of networks in the same priority class, prioritize based on other
-            // criteria (eg. actively selected network, link metrics, etc)
-            if (leftIndex == rightIndex) {
-                // TODO: Improve the strategy of network selection when both UnderlyingNetworkRecord
-                // fall into the same priority class.
-                if (left.isSelected) {
-                    return -1;
-                }
-                if (right.isSelected) {
-                    return 1;
-                }
-            }
-            return Integer.compare(leftIndex, rightIndex);
-        };
-    }
-
-    private static boolean isSelected(
-            Network networkToCheck, UnderlyingNetworkRecord currentlySelected) {
-        if (currentlySelected == null) {
-            return false;
-        }
-        if (currentlySelected.network.equals(networkToCheck)) {
-            return true;
-        }
-        return false;
+    /** Return whether two records represent the same network */
+    public static boolean isSameNetwork(
+            @Nullable UnderlyingNetworkRecord leftRecord,
+            @Nullable UnderlyingNetworkRecord rightRecord) {
+        final Network left = leftRecord == null ? null : leftRecord.network;
+        final Network right = rightRecord == null ? null : rightRecord.network;
+        return Objects.equals(left, right);
     }
 
     /** Dumps the state of this record for logging and debugging purposes. */
-    void dump(
-            VcnContext vcnContext,
-            IndentingPrintWriter pw,
-            List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
-            ParcelUuid subscriptionGroup,
-            TelephonySubscriptionSnapshot snapshot,
-            UnderlyingNetworkRecord currentlySelected,
-            PersistableBundleWrapper carrierConfig) {
+    void dump(IndentingPrintWriter pw) {
         pw.println("UnderlyingNetworkRecord:");
         pw.increaseIndent();
 
-        pw.println("priorityClass: " + priorityClass);
-        pw.println("isSelected: " + isSelected);
         pw.println("mNetwork: " + network);
         pw.println("mNetworkCapabilities: " + networkCapabilities);
         pw.println("mLinkProperties: " + linkProperties);
@@ -218,29 +130,14 @@
             return mNetworkCapabilities != null && mLinkProperties != null && mWasIsBlockedSet;
         }
 
-        UnderlyingNetworkRecord build(
-                VcnContext vcnContext,
-                List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
-                ParcelUuid subscriptionGroup,
-                TelephonySubscriptionSnapshot snapshot,
-                UnderlyingNetworkRecord currentlySelected,
-                PersistableBundleWrapper carrierConfig) {
+        UnderlyingNetworkRecord build() {
             if (!isValid()) {
                 throw new IllegalArgumentException(
                         "Called build before UnderlyingNetworkRecord was valid");
             }
 
             return new UnderlyingNetworkRecord(
-                    mNetwork,
-                    mNetworkCapabilities,
-                    mLinkProperties,
-                    mIsBlocked,
-                    vcnContext,
-                    underlyingNetworkTemplates,
-                    subscriptionGroup,
-                    snapshot,
-                    currentlySelected,
-                    carrierConfig);
+                    mNetwork, mNetworkCapabilities, mLinkProperties, mIsBlocked);
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 4b55bec..676203b 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -1166,11 +1166,12 @@
             transition.abort();
             return;
         }
-        transition.collect(topFocusedRootTask);
-        executeMultiWindowFullscreenRequest(fullscreenRequest, topFocusedRootTask);
-        r.mTransitionController.requestStartTransition(transition, topFocusedRootTask,
+        final Task requestingTask = r.getTask();
+        transition.collect(requestingTask);
+        executeMultiWindowFullscreenRequest(fullscreenRequest, requestingTask);
+        r.mTransitionController.requestStartTransition(transition, requestingTask,
                 null /* remoteTransition */, null /* displayChange */);
-        transition.setReady(topFocusedRootTask, true);
+        transition.setReady(requestingTask, true);
     }
 
     private static void reportMultiwindowFullscreenRequestValidatingResult(IRemoteCallback callback,
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index b94206d..9e56d7a 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -1311,7 +1311,7 @@
                 Rect insets;
                 if (mainWindow != null) {
                     insets = mainWindow.getInsetsStateWithVisibilityOverride().calculateInsets(
-                            mBounds, WindowInsets.Type.systemBars(),
+                            mBounds, WindowInsets.Type.tappableElement(),
                             false /* ignoreVisibility */).toRect();
                     InsetUtils.addInsets(insets, mainWindow.mActivityRecord.getLetterboxInsets());
                 } else {
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index fc3a338..9c9cf04 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -120,38 +120,56 @@
 
     static final int BAL_BLOCK = 0;
 
-    static final int BAL_ALLOW_DEFAULT = 1;
+    static final int BAL_ALLOW_DEFAULT =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_DEFAULT;
 
     // Following codes are in order of precedence
 
     /** Important UIDs which should be always allowed to launch activities */
-    static final int BAL_ALLOW_ALLOWLISTED_UID = 2;
+    static final int BAL_ALLOW_ALLOWLISTED_UID =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_ALLOWLISTED_UID;
 
     /** Apps that fulfill a certain role that can can always launch new tasks */
-    static final int BAL_ALLOW_ALLOWLISTED_COMPONENT = 3;
+    static final int BAL_ALLOW_ALLOWLISTED_COMPONENT =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_ALLOWLISTED_COMPONENT;
 
-    /** Apps which currently have a visible window or are bound by a service with a visible
-     * window */
-    static final int BAL_ALLOW_VISIBLE_WINDOW = 4;
+    /**
+     * Apps which currently have a visible window or are bound by a service with a visible
+     * window
+     */
+    static final int BAL_ALLOW_VISIBLE_WINDOW =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_VISIBLE_WINDOW;
 
     /** Allowed due to the PendingIntent sender */
-    static final int BAL_ALLOW_PENDING_INTENT = 5;
+    static final int BAL_ALLOW_PENDING_INTENT =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_PENDING_INTENT;
 
-    /** App has START_ACTIVITIES_FROM_BACKGROUND permission or BAL instrumentation privileges
-     * granted to it */
-    static final int BAL_ALLOW_PERMISSION = 6;
+    /**
+     * App has START_ACTIVITIES_FROM_BACKGROUND permission or BAL instrumentation privileges
+     * granted to it
+     */
+    static final int BAL_ALLOW_PERMISSION =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_BAL_PERMISSION;
 
     /** Process has SYSTEM_ALERT_WINDOW permission granted to it */
-    static final int BAL_ALLOW_SAW_PERMISSION = 7;
+    static final int BAL_ALLOW_SAW_PERMISSION =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_SAW_PERMISSION;
 
     /** App is in grace period after an activity was started or finished */
-    static final int BAL_ALLOW_GRACE_PERIOD = 8;
+    static final int BAL_ALLOW_GRACE_PERIOD =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_GRACE_PERIOD;
 
     /** App is in a foreground task or bound to a foreground service (but not itself visible) */
-    static final int BAL_ALLOW_FOREGROUND = 9;
+    static final int BAL_ALLOW_FOREGROUND =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_FOREGROUND;
 
     /** Process belongs to a SDK sandbox */
-    static final int BAL_ALLOW_SDK_SANDBOX = 10;
+    static final int BAL_ALLOW_SDK_SANDBOX =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_SDK_SANDBOX;
+
+    /** Process belongs to a SDK sandbox */
+    static final int BAL_ALLOW_NON_APP_VISIBLE_WINDOW =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_NON_APP_VISIBLE_WINDOW;
 
     static String balCodeToString(@BalCode int balCode) {
         return switch (balCode) {
@@ -160,6 +178,7 @@
             case BAL_ALLOW_DEFAULT -> "BAL_ALLOW_DEFAULT";
             case BAL_ALLOW_FOREGROUND -> "BAL_ALLOW_FOREGROUND";
             case BAL_ALLOW_GRACE_PERIOD -> "BAL_ALLOW_GRACE_PERIOD";
+            case BAL_ALLOW_NON_APP_VISIBLE_WINDOW -> "BAL_ALLOW_NON_APP_VISIBLE_WINDOW";
             case BAL_ALLOW_PENDING_INTENT -> "BAL_ALLOW_PENDING_INTENT";
             case BAL_ALLOW_PERMISSION -> "BAL_ALLOW_PERMISSION";
             case BAL_ALLOW_SAW_PERMISSION -> "BAL_ALLOW_SAW_PERMISSION";
@@ -788,7 +807,7 @@
                     /*background*/ false, "callingUid has visible window");
         }
         if (mService.mActiveUids.hasNonAppVisibleWindow(callingUid)) {
-            return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
+            return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
                     /*background*/ false, "callingUid has non-app visible window");
         }
 
@@ -884,7 +903,7 @@
                         /*background*/ false, "realCallingUid has visible window");
             }
             if (mService.mActiveUids.hasNonAppVisibleWindow(state.mRealCallingUid)) {
-                return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
+                return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
                         /*background*/ false, "realCallingUid has non-app visible window");
             }
         } else {
@@ -989,7 +1008,8 @@
                     || balCode == BAL_ALLOW_PERMISSION
                     || balCode == BAL_ALLOW_PENDING_INTENT
                     || balCode == BAL_ALLOW_SAW_PERMISSION
-                    || balCode == BAL_ALLOW_VISIBLE_WINDOW) {
+                    || balCode == BAL_ALLOW_VISIBLE_WINDOW
+                    || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW) {
                 return true;
             }
         }
@@ -1501,7 +1521,8 @@
         Intent intent = state.mIntent;
 
         if (code == BAL_ALLOW_PENDING_INTENT
-                && (callingUid == Process.SYSTEM_UID || realCallingUid == Process.SYSTEM_UID)) {
+                && (callingUid < Process.FIRST_APPLICATION_UID
+                || realCallingUid < Process.FIRST_APPLICATION_UID)) {
             String activityName = intent != null
                     ? requireNonNull(intent.getComponent()).flattenToShortString() : "";
             writeBalAllowedLog(activityName, BAL_ALLOW_PENDING_INTENT,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
deleted file mode 100644
index 4cc68cf..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
+++ /dev/null
@@ -1,1971 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
-import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
-import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
-
-import static org.junit.Assert.assertNotNull;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyFloat;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isA;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.res.Resources;
-import android.hardware.Sensor;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.hardware.display.BrightnessInfo;
-import android.hardware.display.DisplayManagerInternal;
-import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
-import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.PowerManager;
-import android.os.SystemProperties;
-import android.os.UserHandle;
-import android.os.test.TestLooper;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
-import android.provider.Settings;
-import android.testing.TestableContext;
-import android.util.FloatProperty;
-import android.util.SparseArray;
-import android.view.Display;
-import android.view.DisplayInfo;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.modules.utils.testing.ExtendedMockitoRule;
-import com.android.server.LocalServices;
-import com.android.server.am.BatteryStatsService;
-import com.android.server.display.RampAnimator.DualRampAnimator;
-import com.android.server.display.brightness.BrightnessEvent;
-import com.android.server.display.brightness.clamper.BrightnessClamperController;
-import com.android.server.display.brightness.clamper.HdrClamper;
-import com.android.server.display.color.ColorDisplayService;
-import com.android.server.display.config.SensorData;
-import com.android.server.display.feature.DisplayManagerFlags;
-import com.android.server.display.feature.flags.Flags;
-import com.android.server.display.layout.Layout;
-import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
-import com.android.server.policy.WindowManagerPolicy;
-import com.android.server.testutils.OffsettableClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.quality.Strictness;
-import org.mockito.stubbing.Answer;
-
-import java.util.List;
-
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public final class DisplayPowerController2Test {
-    private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY;
-    private static final String UNIQUE_ID = "unique_id_test123";
-    private static final int FOLLOWER_DISPLAY_ID = DISPLAY_ID + 1;
-    private static final String FOLLOWER_UNIQUE_ID = "unique_id_456";
-    private static final int SECOND_FOLLOWER_DISPLAY_ID = FOLLOWER_DISPLAY_ID + 1;
-    private static final String SECOND_FOLLOWER_UNIQUE_DISPLAY_ID = "unique_id_789";
-    private static final float PROX_SENSOR_MAX_RANGE = 5;
-    private static final float BRIGHTNESS_RAMP_RATE_MINIMUM = 0.0f;
-    private static final float BRIGHTNESS_RAMP_RATE_FAST_DECREASE = 0.3f;
-    private static final float BRIGHTNESS_RAMP_RATE_FAST_INCREASE = 0.4f;
-    private static final float BRIGHTNESS_RAMP_RATE_SLOW_DECREASE = 0.1f;
-    private static final float BRIGHTNESS_RAMP_RATE_SLOW_INCREASE = 0.2f;
-    private static final float BRIGHTNESS_RAMP_RATE_SLOW_INCREASE_IDLE = 0.5f;
-    private static final float BRIGHTNESS_RAMP_RATE_SLOW_DECREASE_IDLE = 0.6f;
-
-    private static final long BRIGHTNESS_RAMP_INCREASE_MAX = 1000;
-    private static final long BRIGHTNESS_RAMP_DECREASE_MAX = 2000;
-    private static final long BRIGHTNESS_RAMP_INCREASE_MAX_IDLE = 3000;
-    private static final long BRIGHTNESS_RAMP_DECREASE_MAX_IDLE = 4000;
-
-    private OffsettableClock mClock;
-    private TestLooper mTestLooper;
-    private Handler mHandler;
-    private DisplayPowerControllerHolder mHolder;
-    private Sensor mProxSensor;
-
-    @Mock
-    private DisplayPowerCallbacks mDisplayPowerCallbacksMock;
-    @Mock
-    private SensorManager mSensorManagerMock;
-    @Mock
-    private DisplayBlanker mDisplayBlankerMock;
-    @Mock
-    private BrightnessTracker mBrightnessTrackerMock;
-    @Mock
-    private WindowManagerPolicy mWindowManagerPolicyMock;
-    @Mock
-    private PowerManager mPowerManagerMock;
-    @Mock
-    private ColorDisplayService.ColorDisplayServiceInternal mCdsiMock;
-    @Mock
-    private DisplayWhiteBalanceController mDisplayWhiteBalanceControllerMock;
-    @Mock
-    private DisplayManagerFlags mDisplayManagerFlagsMock;
-    @Mock
-    private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
-    @Captor
-    private ArgumentCaptor<SensorEventListener> mSensorEventListenerCaptor;
-
-    @Rule
-    public final TestableContext mContext = new TestableContext(
-            InstrumentationRegistry.getInstrumentation().getContext());
-
-    @Rule
-    public final ExtendedMockitoRule mExtendedMockitoRule =
-            new ExtendedMockitoRule.Builder(this)
-                    .setStrictness(Strictness.LENIENT)
-                    .spyStatic(SystemProperties.class)
-                    .spyStatic(BatteryStatsService.class)
-                    .spyStatic(ActivityManager.class)
-                    .build();
-
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
-
-    @Before
-    public void setUp() throws Exception {
-        mClock = new OffsettableClock.Stopped();
-        mTestLooper = new TestLooper(mClock::now);
-        mHandler = new Handler(mTestLooper.getLooper());
-
-        // Set some settings to minimize unexpected events and have a consistent starting state
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
-        Settings.System.putFloatForUser(mContext.getContentResolver(),
-                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0, UserHandle.USER_CURRENT);
-
-        addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
-        addLocalServiceMock(ColorDisplayService.ColorDisplayServiceInternal.class,
-                mCdsiMock);
-
-        mContext.addMockSystemService(PowerManager.class, mPowerManagerMock);
-
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_displayColorFadeDisabled, false);
-
-        doAnswer((Answer<Void>) invocationOnMock -> null).when(() ->
-                SystemProperties.set(anyString(), any()));
-        doAnswer((Answer<Void>) invocationOnMock -> null).when(BatteryStatsService::getService);
-        doAnswer((Answer<Boolean>) invocationOnMock -> false)
-                .when(ActivityManager::isLowRamDeviceStatic);
-
-        setUpSensors();
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-    }
-
-    @After
-    public void tearDown() {
-        LocalServices.removeServiceForTest(WindowManagerPolicy.class);
-        LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
-    }
-
-    @Test
-    public void testReleaseProxSuspendBlockersOnExit() throws Exception {
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState to start listener for the prox sensor
-        advanceTime(1);
-
-        SensorEventListener listener = getSensorEventListener(mProxSensor);
-        assertNotNull(listener);
-
-        listener.onSensorChanged(TestUtils.createSensorEvent(mProxSensor, /* value= */ 5));
-        advanceTime(1);
-
-        // two times, one for unfinished business and one for proximity
-        verify(mHolder.wakelockController, times(2)).acquireWakelock(
-                WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
-        verify(mHolder.wakelockController).acquireWakelock(
-                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
-
-        mHolder.dpc.stop();
-        advanceTime(1);
-        // two times, one for unfinished business and one for proximity
-        verify(mHolder.wakelockController, times(2)).acquireWakelock(
-                WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
-        verify(mHolder.wakelockController).acquireWakelock(
-                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
-    }
-
-    @Test
-    public void testScreenOffBecauseOfProximity() throws Exception {
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState to start listener for the prox sensor
-        advanceTime(1);
-
-        SensorEventListener listener = getSensorEventListener(mProxSensor);
-        assertNotNull(listener);
-
-        // Send a positive proximity event
-        listener.onSensorChanged(TestUtils.createSensorEvent(mProxSensor, /* value= */ 1));
-        advanceTime(1);
-
-        // The display should have been turned off
-        verify(mHolder.displayPowerState).setScreenState(Display.STATE_OFF);
-
-        clearInvocations(mHolder.displayPowerState);
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
-        // Send a negative proximity event
-        listener.onSensorChanged(TestUtils.createSensorEvent(mProxSensor,
-                (int) PROX_SENSOR_MAX_RANGE + 1));
-        // Advance time by less than PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY
-        advanceTime(1);
-
-        // The prox sensor is debounced so the display should not have been turned back on yet
-        verify(mHolder.displayPowerState, never()).setScreenState(Display.STATE_ON);
-
-        // Advance time by more than PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY
-        advanceTime(1000);
-
-        // The display should have been turned back on
-        verify(mHolder.displayPowerState).setScreenState(Display.STATE_ON);
-    }
-
-    @Test
-    public void testScreenOffBecauseOfProximity_ProxSensorGone() throws Exception {
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState to start listener for the prox sensor
-        advanceTime(1);
-
-        SensorEventListener listener = getSensorEventListener(mProxSensor);
-        assertNotNull(listener);
-
-        // Send a positive proximity event
-        listener.onSensorChanged(TestUtils.createSensorEvent(mProxSensor, /* value= */ 1));
-        advanceTime(1);
-
-        // The display should have been turned off
-        verify(mHolder.displayPowerState).setScreenState(Display.STATE_OFF);
-
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
-        // The display device changes and we no longer have a prox sensor
-        reset(mSensorManagerMock);
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mock(DisplayDeviceConfig.class), /* isEnabled= */ true);
-        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, Layout.NO_LEAD_DISPLAY);
-
-        advanceTime(1); // Run updatePowerState
-
-        // The display should have been turned back on and the listener should have been
-        // unregistered
-        verify(mHolder.displayPowerState).setScreenState(Display.STATE_ON);
-        verify(mSensorManagerMock).unregisterListener(listener);
-    }
-
-    @Test
-    public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() {
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        // send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        final DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        followerDpc.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState
-        advanceTime(1);
-
-        verify(mSensorManagerMock, never()).registerListener(any(SensorEventListener.class),
-                eq(mProxSensor), anyInt(), any(Handler.class));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowers_BothDpcsSupportNits() {
-        DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
-                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
-
-        // Test different float scale values
-        float leadBrightness = 0.3f;
-        float followerBrightness = 0.4f;
-        float nits = 300;
-        when(mHolder.automaticBrightnessController.convertToNits(leadBrightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(followerBrightness);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(leadBrightness);
-        listener.onBrightnessChanged(leadBrightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(), anyFloat(), eq(false));
-        verify(followerDpc.animator).animateTo(eq(followerBrightness), anyFloat(),
-                anyFloat(), eq(false));
-
-        clearInvocations(mHolder.animator, followerDpc.animator);
-
-        // Test the same float scale value
-        float brightness = 0.6f;
-        nits = 600;
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
-        DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
-                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
-
-        float brightness = 0.3f;
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(300f);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(anyFloat()))
-                .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
-        DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
-                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
-
-        float brightness = 0.3f;
-        when(mHolder.automaticBrightnessController.convertToNits(anyFloat())).thenReturn(-1f);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
-        DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
-                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
-
-        float brightness = 0.3f;
-        when(mHolder.automaticBrightnessController.convertToNits(anyFloat())).thenReturn(-1f);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(anyFloat()))
-                .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowers_AutomaticBrightness() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        float leadBrightness = 0.1f;
-        float rawLeadBrightness = 0.3f;
-        float followerBrightness = 0.4f;
-        float nits = 300;
-        float ambientLux = 3000;
-        when(mHolder.automaticBrightnessController.getRawAutomaticScreenBrightness())
-                .thenReturn(rawLeadBrightness);
-        when(mHolder.automaticBrightnessController
-                .getAutomaticScreenBrightness(any(BrightnessEvent.class)))
-                .thenReturn(leadBrightness);
-        when(mHolder.automaticBrightnessController.convertToNits(rawLeadBrightness))
-                .thenReturn(nits);
-        when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(followerBrightness);
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        // One triggered by handleBrightnessModeChange, another triggered by setBrightnessToFollow
-        verify(followerDpc.hbmController, times(2)).onAmbientLuxChange(ambientLux);
-        verify(followerDpc.animator, times(2)).animateTo(eq(followerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(leadBrightness);
-        when(followerDpc.displayPowerState.getScreenBrightness()).thenReturn(followerBrightness);
-        clearInvocations(mHolder.animator, followerDpc.animator);
-
-        leadBrightness = 0.05f;
-        rawLeadBrightness = 0.2f;
-        followerBrightness = 0.3f;
-        nits = 200;
-        ambientLux = 2000;
-        when(mHolder.automaticBrightnessController.getRawAutomaticScreenBrightness())
-                .thenReturn(rawLeadBrightness);
-        when(mHolder.automaticBrightnessController
-                .getAutomaticScreenBrightness(any(BrightnessEvent.class)))
-                .thenReturn(leadBrightness);
-        when(mHolder.automaticBrightnessController.convertToNits(rawLeadBrightness))
-                .thenReturn(nits);
-        when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(followerBrightness);
-
-        mHolder.dpc.updateBrightness();
-        advanceTime(1); // Run updatePowerState
-
-        // The second time, the animation rate should be slow
-        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE), eq(false));
-        verify(followerDpc.hbmController).onAmbientLuxChange(ambientLux);
-        verify(followerDpc.animator).animateTo(eq(followerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE), eq(false));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowersRemoval_RemoveSingleFollower() {
-        DisplayPowerControllerHolder followerDpc = createDisplayPowerController(FOLLOWER_DISPLAY_ID,
-                FOLLOWER_UNIQUE_ID);
-        DisplayPowerControllerHolder secondFollowerDpc = createDisplayPowerController(
-                SECOND_FOLLOWER_DISPLAY_ID, SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(secondFollowerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        secondFollowerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
-                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
-
-        // Set the initial brightness on the DPC we're going to remove so we have a fixed value for
-        // it to return to.
-        listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(followerDpc.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener followerListener = listenerCaptor.getValue();
-        final float initialFollowerBrightness = 0.3f;
-        when(followerDpc.brightnessSetting.getBrightness()).thenReturn(initialFollowerBrightness);
-        followerListener.onBrightnessChanged(initialFollowerBrightness);
-        advanceTime(1);
-        verify(followerDpc.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(followerDpc.displayPowerState.getScreenBrightness())
-                .thenReturn(initialFollowerBrightness);
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
-        mHolder.dpc.addDisplayBrightnessFollower(secondFollowerDpc.dpc);
-        clearInvocations(followerDpc.animator);
-
-        // Validate both followers are correctly registered and receiving brightness updates
-        float brightness = 0.6f;
-        float nits = 600;
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(secondFollowerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(secondFollowerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        when(followerDpc.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        when(secondFollowerDpc.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        clearInvocations(mHolder.animator, followerDpc.animator, secondFollowerDpc.animator);
-
-        // Remove the first follower and validate it goes back to its original brightness.
-        mHolder.dpc.removeDisplayBrightnessFollower(followerDpc.dpc);
-        advanceTime(1);
-        verify(followerDpc.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE), eq(false));
-
-        when(followerDpc.displayPowerState.getScreenBrightness())
-                .thenReturn(initialFollowerBrightness);
-        clearInvocations(followerDpc.animator);
-
-        // Change the brightness of the lead display and validate only the second follower responds
-        brightness = 0.7f;
-        nits = 700;
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(secondFollowerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerDpc.animator, never()).animateTo(anyFloat(), anyFloat(), anyFloat(),
-                anyBoolean());
-        verify(secondFollowerDpc.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testDisplayBrightnessFollowersRemoval_RemoveAllFollowers() {
-        DisplayPowerControllerHolder followerHolder =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        DisplayPowerControllerHolder secondFollowerHolder =
-                createDisplayPowerController(SECOND_FOLLOWER_DISPLAY_ID,
-                        SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(followerHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(secondFollowerHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        secondFollowerHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
-                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
-
-        // Set the initial brightness on the DPCs we're going to remove so we have a fixed value for
-        // it to return to.
-        listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(followerHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener followerListener = listenerCaptor.getValue();
-        listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(secondFollowerHolder.brightnessSetting).registerListener(listenerCaptor.capture());
-        BrightnessSetting.BrightnessSettingListener secondFollowerListener =
-                listenerCaptor.getValue();
-        final float initialFollowerBrightness = 0.3f;
-        when(followerHolder.brightnessSetting.getBrightness()).thenReturn(
-                initialFollowerBrightness);
-        when(secondFollowerHolder.brightnessSetting.getBrightness()).thenReturn(
-                initialFollowerBrightness);
-        followerListener.onBrightnessChanged(initialFollowerBrightness);
-        secondFollowerListener.onBrightnessChanged(initialFollowerBrightness);
-        advanceTime(1);
-        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(followerHolder.displayPowerState.getScreenBrightness())
-                .thenReturn(initialFollowerBrightness);
-        when(secondFollowerHolder.displayPowerState.getScreenBrightness())
-                .thenReturn(initialFollowerBrightness);
-
-        mHolder.dpc.addDisplayBrightnessFollower(followerHolder.dpc);
-        mHolder.dpc.addDisplayBrightnessFollower(secondFollowerHolder.dpc);
-        clearInvocations(followerHolder.animator, secondFollowerHolder.animator);
-
-        // Validate both followers are correctly registered and receiving brightness updates
-        float brightness = 0.6f;
-        float nits = 600;
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerHolder.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(secondFollowerHolder.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(brightness);
-        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
-        listener.onBrightnessChanged(brightness);
-        advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(followerHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-        verify(secondFollowerHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        when(followerHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        when(secondFollowerHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        clearInvocations(mHolder.animator, followerHolder.animator, secondFollowerHolder.animator);
-
-        // Stop the lead DPC and validate that the followers go back to their original brightness.
-        mHolder.dpc.stop();
-        advanceTime(1);
-        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE), eq(false));
-        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE), eq(false));
-        clearInvocations(followerHolder.animator, secondFollowerHolder.animator);
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FAST_HDR_TRANSITIONS)
-    public void testDisplayBrightnessHdr_SkipAnimationOnHdrAppearance() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        final float sdrBrightness = 0.1f;
-        final float hdrBrightness = 0.3f;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(sdrBrightness);
-        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(1.0f);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(sdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(sdrBrightness);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(sdrBrightness);
-
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
-        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
-        clearInvocations(mHolder.animator);
-
-        mHolder.dpc.updateBrightness();
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(hdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_MINIMUM), eq(false));
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FAST_HDR_TRANSITIONS)
-    public void testDisplayBrightnessHdr_SkipAnimationOnHdrRemoval() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        final float sdrBrightness = 0.1f;
-        final float hdrBrightness = 0.3f;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.automaticBrightnessController.isInIdleMode()).thenReturn(true);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(sdrBrightness);
-        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(1.0f);
-
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
-        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(hdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(hdrBrightness);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(sdrBrightness);
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF);
-
-        clearInvocations(mHolder.animator);
-
-        mHolder.dpc.updateBrightness();
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(sdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_MINIMUM), eq(false));
-    }
-
-    @Test
-    public void testDoesNotSetScreenStateForNonDefaultDisplayUntilBootCompleted() {
-        // We should still set screen state for the default display
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-        verify(mHolder.displayPowerState, times(2)).setScreenState(anyInt());
-
-        mHolder = createDisplayPowerController(42, UNIQUE_ID);
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
-
-        mHolder.dpc.onBootCompleted();
-        advanceTime(1); // Run updatePowerState
-        verify(mHolder.displayPowerState).setScreenState(anyInt());
-    }
-
-    @Test
-    public void testSetScreenOffBrightnessSensorEnabled_DisplayIsOff() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_OFF;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .setLightSensorEnabled(true);
-
-        // The display turns on and we use the brightness value recommended by
-        // ScreenOffBrightnessSensorController
-        clearInvocations(mHolder.screenOffBrightnessSensorController);
-        float brightness = 0.14f;
-        when(mHolder.screenOffBrightnessSensorController.getAutomaticScreenBrightness())
-                .thenReturn(brightness);
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .getAutomaticScreenBrightness();
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat(), eq(false));
-    }
-
-    @Test
-    public void testSetScreenOffBrightnessSensorEnabled_DisplayIsInDoze() {
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, false);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .setLightSensorEnabled(true);
-
-        // The display turns on and we use the brightness value recommended by
-        // ScreenOffBrightnessSensorController
-        clearInvocations(mHolder.screenOffBrightnessSensorController);
-        float brightness = 0.14f;
-        when(mHolder.screenOffBrightnessSensorController.getAutomaticScreenBrightness())
-                .thenReturn(brightness);
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .getAutomaticScreenBrightness();
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat(), eq(false));
-    }
-
-    @Test
-    public void testSetScreenOffBrightnessSensorDisabled_AutoBrightnessIsDisabled() {
-        // Tests are set up with manual brightness by default, so no need to set it here.
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_OFF;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .setLightSensorEnabled(false);
-    }
-
-    @Test
-    public void testSetScreenOffBrightnessSensorDisabled_DisplayIsDisabled() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ false);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .setLightSensorEnabled(false);
-    }
-
-    @Test
-    public void testSetScreenOffBrightnessSensorDisabled_DisplayIsOn() {
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .setLightSensorEnabled(false);
-    }
-
-    @Test
-    public void testSetScreenOffBrightnessSensorDisabled_DisplayIsAFollower() {
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_OFF;
-
-        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, /* leadDisplayId= */ 42);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController, atLeastOnce())
-                .setLightSensorEnabled(false);
-    }
-
-    @Test
-    public void testStopScreenOffBrightnessSensorControllerWhenDisplayDeviceChanges() {
-        // New display device
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mock(DisplayDeviceConfig.class), /* isEnabled= */ true);
-
-        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, Layout.NO_LEAD_DISPLAY);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.screenOffBrightnessSensorController).stop();
-    }
-
-    @Test
-    public void testAutoBrightnessEnabled_DisplayIsOn() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.automaticBrightnessController).configure(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
-                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
-                /* shouldResetShortTermModel= */ false
-        );
-        verify(mHolder.hbmController)
-                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
-    }
-
-    @Test
-    public void testAutoBrightnessEnabled_DisplayIsInDoze() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.automaticBrightnessController).configure(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
-                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_DOZE,
-                /* shouldResetShortTermModel= */ false
-        );
-        verify(mHolder.hbmController)
-                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
-    }
-
-    @Test
-    public void testAutoBrightnessDisabled_ManualBrightnessMode() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        // One triggered by the test, the other by handleBrightnessModeChange
-        verify(mHolder.automaticBrightnessController, times(2)).configure(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
-                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
-                /* shouldResetShortTermModel= */ false
-        );
-        verify(mHolder.hbmController, times(2))
-                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
-    }
-
-    @Test
-    public void testAutoBrightnessDisabled_DisplayIsOff() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_OFF;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.automaticBrightnessController).configure(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
-                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_OFF,
-                /* shouldResetShortTermModel= */ false
-        );
-        verify(mHolder.hbmController).setAutoBrightnessEnabled(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE);
-    }
-
-    @Test
-    public void testAutoBrightnessDisabled_DisplayIsInDoze() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, false);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.automaticBrightnessController).configure(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
-                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_DOZE,
-                /* shouldResetShortTermModel= */ false
-        );
-        verify(mHolder.hbmController).setAutoBrightnessEnabled(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE);
-    }
-
-    @Test
-    public void testAutoBrightnessDisabled_FollowerDisplay() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        mHolder.dpc.setBrightnessToFollow(0.3f, -1, 0, /* slowChange= */ false);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        // One triggered by the test, the other by handleBrightnessModeChange
-        verify(mHolder.automaticBrightnessController, times(2)).configure(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
-                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
-                /* shouldResetShortTermModel= */ false
-        );
-
-        // HBM should be allowed for the follower display
-        verify(mHolder.hbmController)
-                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
-    }
-
-    @Test
-    public void testBrightnessNitsPersistWhenDisplayDeviceChanges() {
-        float brightness = 0.3f;
-        float nits = 500;
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_persistBrightnessNitsForDefaultDisplay,
-                true);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-
-        mHolder.dpc.setBrightness(brightness);
-        verify(mHolder.brightnessSetting).setBrightnessNitsForDefaultDisplay(nits);
-
-        float newBrightness = 0.4f;
-        when(mHolder.brightnessSetting.getBrightnessNitsForDefaultDisplay()).thenReturn(nits);
-        when(mHolder.automaticBrightnessController.getBrightnessFromNits(nits))
-                .thenReturn(newBrightness);
-        // New display device
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mock(DisplayDeviceConfig.class), /* isEnabled= */ true);
-        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, Layout.NO_LEAD_DISPLAY);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-        // One triggered by handleBrightnessModeChange, another triggered by onDisplayChanged
-        verify(mHolder.animator, times(2)).animateTo(eq(newBrightness), anyFloat(), anyFloat(),
-                eq(false));
-    }
-
-    @Test
-    public void testShortTermModelPersistsWhenDisplayDeviceChanges() {
-        float lux = 2000;
-        float nits = 500;
-        when(mHolder.automaticBrightnessController.getUserLux()).thenReturn(lux);
-        when(mHolder.automaticBrightnessController.getUserNits()).thenReturn(nits);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1);
-        clearInvocations(mHolder.injector);
-
-        // New display device
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mock(DisplayDeviceConfig.class), /* isEnabled= */ true);
-        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, Layout.NO_LEAD_DISPLAY);
-        advanceTime(1);
-
-        verify(mHolder.injector).getAutomaticBrightnessController(
-                any(AutomaticBrightnessController.Callbacks.class),
-                any(Looper.class),
-                eq(mSensorManagerMock),
-                /* lightSensor= */ any(),
-                /* brightnessMappingStrategyMap= */ any(SparseArray.class),
-                /* lightSensorWarmUpTime= */ anyInt(),
-                /* brightnessMin= */ anyFloat(),
-                /* brightnessMax= */ anyFloat(),
-                /* dozeScaleFactor */ anyFloat(),
-                /* lightSensorRate= */ anyInt(),
-                /* initialLightSensorRate= */ anyInt(),
-                /* brighteningLightDebounceConfig */ anyLong(),
-                /* darkeningLightDebounceConfig */ anyLong(),
-                /* brighteningLightDebounceConfigIdle= */ anyLong(),
-                /* darkeningLightDebounceConfigIdle= */ anyLong(),
-                /* resetAmbientLuxAfterWarmUpConfig= */ anyBoolean(),
-                any(HysteresisLevels.class),
-                any(HysteresisLevels.class),
-                any(HysteresisLevels.class),
-                any(HysteresisLevels.class),
-                eq(mContext),
-                any(BrightnessRangeController.class),
-                any(BrightnessThrottler.class),
-                /* ambientLightHorizonShort= */ anyInt(),
-                /* ambientLightHorizonLong= */ anyInt(),
-                eq(lux),
-                eq(nits)
-        );
-    }
-
-    @Test
-    public void testUpdateBrightnessThrottlingDataId() {
-        mHolder.display.getDisplayInfoLocked().thermalBrightnessThrottlingDataId =
-                "throttling-data-id";
-        clearInvocations(mHolder.display.getPrimaryDisplayDeviceLocked().getDisplayDeviceConfig());
-
-        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, Layout.NO_LEAD_DISPLAY);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.display.getPrimaryDisplayDeviceLocked().getDisplayDeviceConfig())
-                .getThermalBrightnessThrottlingDataMapByThrottlingId();
-    }
-
-    @Test
-    public void testSetBrightness_BrightnessShouldBeClamped() {
-        float clampedBrightness = 0.6f;
-        when(mHolder.hbmController.getCurrentBrightnessMax()).thenReturn(clampedBrightness);
-
-        mHolder.dpc.setBrightness(PowerManager.BRIGHTNESS_MAX);
-
-        verify(mHolder.brightnessSetting).setBrightness(clampedBrightness);
-    }
-
-    @Test
-    public void testDwbcCallsHappenOnHandler() {
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-
-        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
-        verify(mDisplayWhiteBalanceControllerMock, never()).setStrongModeEnabled(true);
-
-        // dispatch handler looper
-        advanceTime(1);
-        verify(mDisplayWhiteBalanceControllerMock, times(1)).setStrongModeEnabled(true);
-    }
-
-    @Test
-    public void testRampRatesIdle() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        float brightness = 0.6f;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.automaticBrightnessController.isInIdleMode()).thenReturn(true);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(brightness);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
-        brightness = 0.05f;
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(brightness);
-
-        mHolder.dpc.updateBrightness();
-        advanceTime(1); // Run updatePowerState
-
-        // The second time, the animation rate should be slow
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE_IDLE), eq(false));
-
-        brightness = 0.9f;
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(brightness);
-
-        mHolder.dpc.updateBrightness();
-        advanceTime(1); // Run updatePowerState
-        // The third time, the animation rate should be slow
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_SLOW_INCREASE_IDLE), eq(false));
-    }
-
-    @Test
-    public void testRampRateForHdrContent_HdrClamperOff() {
-        float hdrBrightness = 0.8f;
-        float clampedBrightness = 0.6f;
-        float transitionRate = 1.5f;
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
-        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
-        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(clampedBrightness);
-        when(mHolder.hdrClamper.getTransitionRate()).thenReturn(transitionRate);
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator, atLeastOnce()).animateTo(eq(hdrBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testRampRateForHdrContent_HdrClamperOn() {
-        float clampedBrightness = 0.6f;
-        float transitionRate = 1.5f;
-        when(mDisplayManagerFlagsMock.isHdrClamperEnabled()).thenReturn(true);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
-        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(PowerManager.BRIGHTNESS_MAX);
-        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(clampedBrightness);
-        when(mHolder.hdrClamper.getTransitionRate()).thenReturn(transitionRate);
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator, atLeastOnce()).animateTo(eq(clampedBrightness), anyFloat(),
-                eq(transitionRate), eq(true));
-    }
-
-    @Test
-    public void testRampRateForClampersControllerApplied() {
-        float transitionRate = 1.5f;
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
-        when(mHolder.clamperController.clamp(any(), anyFloat(), anyBoolean())).thenAnswer(
-                invocation -> DisplayBrightnessState.builder()
-                        .setIsSlowChange(invocation.getArgument(2))
-                        .setBrightness(invocation.getArgument(1))
-                        .setMaxBrightness(PowerManager.BRIGHTNESS_MAX)
-                        .setCustomAnimationRate(transitionRate).build());
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator, atLeastOnce()).animateTo(anyFloat(), anyFloat(),
-                eq(transitionRate), anyBoolean());
-    }
-
-    @Test
-    public void testRampRateForClampersControllerNotApplied_ifDoze() {
-        float transitionRate = 1.5f;
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        dpr.dozeScreenState = Display.STATE_UNKNOWN;
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
-        when(mHolder.clamperController.clamp(any(), anyFloat(), anyBoolean())).thenAnswer(
-                invocation -> DisplayBrightnessState.builder()
-                        .setIsSlowChange(invocation.getArgument(2))
-                        .setBrightness(invocation.getArgument(1))
-                        .setMaxBrightness(PowerManager.BRIGHTNESS_MAX)
-                        .setCustomAnimationRate(transitionRate).build());
-
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator, atLeastOnce()).animateTo(anyFloat(), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE), anyBoolean());
-        verify(mHolder.animator, never()).animateTo(anyFloat(), anyFloat(),
-                eq(transitionRate), anyBoolean());
-    }
-
-    @Test
-    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1)
-    public void testRampMaxTimeInteractiveThenIdle() {
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState
-        advanceTime(1);
-
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mHolder.config, /* isEnabled= */ true);
-        verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
-                BRIGHTNESS_RAMP_DECREASE_MAX);
-
-        // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
-        advanceTime(1);
-
-        // A second time, when switching to idle mode.
-        verify(mHolder.animator, times(2)).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
-                BRIGHTNESS_RAMP_DECREASE_MAX);
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1)
-    public void testRampMaxTimeInteractiveThenIdle_DifferentValues() {
-        when(mDisplayManagerFlagsMock.isAdaptiveTone1Enabled()).thenReturn(true);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true);
-
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState
-        advanceTime(1);
-
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mHolder.config, /* isEnabled= */ true);
-        verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
-                BRIGHTNESS_RAMP_DECREASE_MAX);
-
-        // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
-        advanceTime(1);
-
-        // A second time, when switching to idle mode.
-        verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE,
-                BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
-    }
-
-    @Test
-    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1)
-    public void testRampMaxTimeIdle() {
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-        // Run updatePowerState
-        advanceTime(1);
-        // Once on setup
-        verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
-                BRIGHTNESS_RAMP_DECREASE_MAX);
-
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mHolder.config, /* isEnabled= */ true);
-
-        // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
-
-        // A second time when switching to idle mode.
-        verify(mHolder.animator, times(2)).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
-                BRIGHTNESS_RAMP_DECREASE_MAX);
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1)
-    public void testRampMaxTimeIdle_DifferentValues() {
-        when(mDisplayManagerFlagsMock.isAdaptiveTone1Enabled()).thenReturn(true);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true);
-
-        // Send a display power request
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        dpr.useProximitySensor = true;
-        mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
-        // Run updatePowerState
-        advanceTime(1);
-
-        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
-                mHolder.config, /* isEnabled= */ true);
-
-        // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
-
-        verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE,
-                BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
-    }
-
-    @Test
-    public void testDozeScreenStateOverride_toSupportedOffloadStateFromDoze_DisplayStateChanges() {
-        // set up.
-        int initState = Display.STATE_DOZE;
-        int supportedTargetState = Display.STATE_DOZE_SUSPEND;
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        doAnswer(invocation -> {
-            when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
-            return null;
-        }).when(mHolder.displayPowerState).setScreenState(anyInt());
-        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
-
-        // start with DOZE.
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        mHolder.dpc.overrideDozeScreenState(supportedTargetState);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.displayPowerState).setScreenState(supportedTargetState);
-    }
-
-    @Test
-    public void testDozeScreenStateOverride_toUnSupportedOffloadStateFromDoze_stateRemains() {
-        // set up.
-        int initState = Display.STATE_DOZE;
-        int unSupportedTargetState = Display.STATE_ON;
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        doAnswer(invocation -> {
-            when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
-            return null;
-        }).when(mHolder.displayPowerState).setScreenState(anyInt());
-        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
-
-        // start with DOZE.
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        mHolder.dpc.overrideDozeScreenState(unSupportedTargetState);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
-    }
-
-    @Test
-    public void testDozeScreenStateOverride_toSupportedOffloadStateFromOFF_stateRemains() {
-        // set up.
-        int initState = Display.STATE_OFF;
-        int supportedTargetState = Display.STATE_DOZE_SUSPEND;
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        doAnswer(invocation -> {
-            when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
-            return null;
-        }).when(mHolder.displayPowerState).setScreenState(anyInt());
-        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
-
-        // start with OFF.
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_OFF;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        mHolder.dpc.overrideDozeScreenState(supportedTargetState);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
-    }
-
-    @Test
-    public void testBrightnessFromOffload() {
-        when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true);
-        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        float brightness = 0.34f;
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
-
-        mHolder.dpc.setBrightnessFromOffload(brightness);
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        // One triggered by handleBrightnessModeChange, another triggered by
-        // setBrightnessFromOffload
-        verify(mHolder.animator, times(2)).animateTo(eq(brightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-    }
-
-    @Test
-    public void testSwitchToDozeAutoBrightnessMode() {
-        when(mDisplayManagerFlagsMock.areAutoBrightnessModesEnabled()).thenReturn(true);
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        // One triggered by handleBrightnessModeChange, another triggered by requestPowerState
-        verify(mHolder.automaticBrightnessController, times(2))
-                .switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
-
-        // Back to default mode
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.automaticBrightnessController).switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
-    }
-
-    @Test
-    public void testDoesNotSwitchFromIdleToDozeAutoBrightnessMode() {
-        when(mDisplayManagerFlagsMock.areAutoBrightnessModesEnabled()).thenReturn(true);
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
-        when(mHolder.automaticBrightnessController.isInIdleMode()).thenReturn(true);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.automaticBrightnessController, never())
-                .switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
-    }
-
-    @Test
-    public void testDoesNotSwitchDozeAutoBrightnessModeIfFeatureFlagOff() {
-        when(mDisplayManagerFlagsMock.areAutoBrightnessModesEnabled()).thenReturn(false);
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.automaticBrightnessController, never())
-                .switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
-    }
-
-    /**
-     * Creates a mock and registers it to {@link LocalServices}.
-     */
-    private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
-        LocalServices.removeServiceForTest(clazz);
-        LocalServices.addService(clazz, mock);
-    }
-
-    private void advanceTime(long timeMs) {
-        mClock.fastForward(timeMs);
-        mTestLooper.dispatchAll();
-    }
-
-    private void setUpSensors() throws Exception {
-        mProxSensor = TestUtils.createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY,
-                PROX_SENSOR_MAX_RANGE);
-        Sensor screenOffBrightnessSensor = TestUtils.createSensor(
-                Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
-        when(mSensorManagerMock.getSensorList(eq(Sensor.TYPE_ALL)))
-                .thenReturn(List.of(mProxSensor, screenOffBrightnessSensor));
-    }
-
-    private SensorEventListener getSensorEventListener(Sensor sensor) {
-        verify(mSensorManagerMock).registerListener(mSensorEventListenerCaptor.capture(),
-                eq(sensor), eq(SensorManager.SENSOR_DELAY_NORMAL), isA(Handler.class));
-        return mSensorEventListenerCaptor.getValue();
-    }
-
-    private void setUpDisplay(int displayId, String uniqueId, LogicalDisplay logicalDisplayMock,
-            DisplayDevice displayDeviceMock, DisplayDeviceConfig displayDeviceConfigMock,
-            boolean isEnabled) {
-        DisplayInfo info = new DisplayInfo();
-        DisplayDeviceInfo deviceInfo = new DisplayDeviceInfo();
-        deviceInfo.uniqueId = uniqueId;
-
-        when(logicalDisplayMock.getDisplayIdLocked()).thenReturn(displayId);
-        when(logicalDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(displayDeviceMock);
-        when(logicalDisplayMock.getDisplayInfoLocked()).thenReturn(info);
-        when(logicalDisplayMock.isEnabledLocked()).thenReturn(isEnabled);
-        when(logicalDisplayMock.isInTransitionLocked()).thenReturn(false);
-        when(displayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo);
-        when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId);
-        when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock);
-        when(displayDeviceConfigMock.getProximitySensor()).thenReturn(
-                new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
-        when(displayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
-        when(displayDeviceConfigMock.isAutoBrightnessAvailable()).thenReturn(true);
-        when(displayDeviceConfigMock.getAmbientLightSensor()).thenReturn(
-                new SensorData());
-        when(displayDeviceConfigMock.getScreenOffBrightnessSensor()).thenReturn(
-                new SensorData(Sensor.STRING_TYPE_LIGHT, null));
-        when(displayDeviceConfigMock.getScreenOffBrightnessSensorValueToLux())
-                .thenReturn(new int[0]);
-
-        when(displayDeviceConfigMock.getBrightnessRampFastDecrease())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_FAST_DECREASE);
-        when(displayDeviceConfigMock.getBrightnessRampFastIncrease())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_FAST_INCREASE);
-        when(displayDeviceConfigMock.getBrightnessRampSlowDecrease())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE);
-        when(displayDeviceConfigMock.getBrightnessRampSlowIncrease())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_INCREASE);
-        when(displayDeviceConfigMock.getBrightnessRampSlowIncreaseIdle())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_INCREASE_IDLE);
-        when(displayDeviceConfigMock.getBrightnessRampSlowDecreaseIdle())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE_IDLE);
-
-        when(displayDeviceConfigMock.getBrightnessRampIncreaseMaxMillis())
-                .thenReturn(BRIGHTNESS_RAMP_INCREASE_MAX);
-        when(displayDeviceConfigMock.getBrightnessRampDecreaseMaxMillis())
-                .thenReturn(BRIGHTNESS_RAMP_DECREASE_MAX);
-        when(displayDeviceConfigMock.getBrightnessRampIncreaseMaxIdleMillis())
-                .thenReturn(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE);
-        when(displayDeviceConfigMock.getBrightnessRampDecreaseMaxIdleMillis())
-                .thenReturn(BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
-    }
-
-    private DisplayPowerControllerHolder createDisplayPowerController(int displayId,
-            String uniqueId) {
-        return createDisplayPowerController(displayId, uniqueId, /* isEnabled= */ true);
-    }
-
-    private DisplayPowerControllerHolder createDisplayPowerController(int displayId,
-            String uniqueId, boolean isEnabled) {
-        final DisplayPowerState displayPowerState = mock(DisplayPowerState.class);
-        final DualRampAnimator<DisplayPowerState> animator = mock(DualRampAnimator.class);
-        final AutomaticBrightnessController automaticBrightnessController =
-                mock(AutomaticBrightnessController.class);
-        final WakelockController wakelockController = mock(WakelockController.class);
-        final BrightnessMappingStrategy brightnessMappingStrategy =
-                mock(BrightnessMappingStrategy.class);
-        final HysteresisLevels hysteresisLevels = mock(HysteresisLevels.class);
-        final ScreenOffBrightnessSensorController screenOffBrightnessSensorController =
-                mock(ScreenOffBrightnessSensorController.class);
-        final HighBrightnessModeController hbmController = mock(HighBrightnessModeController.class);
-        final HdrClamper hdrClamper = mock(HdrClamper.class);
-        BrightnessClamperController clamperController = mock(BrightnessClamperController.class);
-
-        when(hbmController.getCurrentBrightnessMax()).thenReturn(PowerManager.BRIGHTNESS_MAX);
-        when(clamperController.clamp(any(), anyFloat(), anyBoolean())).thenAnswer(
-                invocation -> DisplayBrightnessState.builder()
-                        .setIsSlowChange(invocation.getArgument(2))
-                        .setBrightness(invocation.getArgument(1))
-                        .setMaxBrightness(PowerManager.BRIGHTNESS_MAX)
-                        .setCustomAnimationRate(-1).build());
-
-        TestInjector injector = spy(new TestInjector(displayPowerState, animator,
-                automaticBrightnessController, wakelockController, brightnessMappingStrategy,
-                hysteresisLevels, screenOffBrightnessSensorController, hbmController, hdrClamper,
-                clamperController, mDisplayManagerFlagsMock));
-
-        final LogicalDisplay display = mock(LogicalDisplay.class);
-        final DisplayDevice device = mock(DisplayDevice.class);
-        final HighBrightnessModeMetadata hbmMetadata = mock(HighBrightnessModeMetadata.class);
-        final BrightnessSetting brightnessSetting = mock(BrightnessSetting.class);
-        final DisplayDeviceConfig config = mock(DisplayDeviceConfig.class);
-
-        setUpDisplay(displayId, uniqueId, display, device, config, isEnabled);
-
-        final DisplayPowerController2 dpc = new DisplayPowerController2(
-                mContext, injector, mDisplayPowerCallbacksMock, mHandler,
-                mSensorManagerMock, mDisplayBlankerMock, display,
-                mBrightnessTrackerMock, brightnessSetting, () -> {
-        },
-                hbmMetadata, /* bootCompleted= */ false, mDisplayManagerFlagsMock);
-
-        return new DisplayPowerControllerHolder(dpc, display, displayPowerState, brightnessSetting,
-                animator, automaticBrightnessController, wakelockController,
-                screenOffBrightnessSensorController, hbmController, hdrClamper, clamperController,
-                hbmMetadata, brightnessMappingStrategy, injector, config);
-    }
-
-    /**
-     * A class for holding a DisplayPowerController under test and all the mocks specifically
-     * related to it.
-     */
-    private static class DisplayPowerControllerHolder {
-        public final DisplayPowerController2 dpc;
-        public final LogicalDisplay display;
-        public final DisplayPowerState displayPowerState;
-        public final BrightnessSetting brightnessSetting;
-        public final DualRampAnimator<DisplayPowerState> animator;
-        public final AutomaticBrightnessController automaticBrightnessController;
-        public final WakelockController wakelockController;
-        public final ScreenOffBrightnessSensorController screenOffBrightnessSensorController;
-        public final HighBrightnessModeController hbmController;
-
-        public final HdrClamper hdrClamper;
-        public final BrightnessClamperController clamperController;
-        public final HighBrightnessModeMetadata hbmMetadata;
-        public final BrightnessMappingStrategy brightnessMappingStrategy;
-        public final DisplayPowerController2.Injector injector;
-        public final DisplayDeviceConfig config;
-
-        DisplayPowerControllerHolder(DisplayPowerController2 dpc, LogicalDisplay display,
-                DisplayPowerState displayPowerState, BrightnessSetting brightnessSetting,
-                DualRampAnimator<DisplayPowerState> animator,
-                AutomaticBrightnessController automaticBrightnessController,
-                WakelockController wakelockController,
-                ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
-                HighBrightnessModeController hbmController,
-                HdrClamper hdrClamper,
-                BrightnessClamperController clamperController,
-                HighBrightnessModeMetadata hbmMetadata,
-                BrightnessMappingStrategy brightnessMappingStrategy,
-                DisplayPowerController2.Injector injector,
-                DisplayDeviceConfig config) {
-            this.dpc = dpc;
-            this.display = display;
-            this.displayPowerState = displayPowerState;
-            this.brightnessSetting = brightnessSetting;
-            this.animator = animator;
-            this.automaticBrightnessController = automaticBrightnessController;
-            this.wakelockController = wakelockController;
-            this.screenOffBrightnessSensorController = screenOffBrightnessSensorController;
-            this.hbmController = hbmController;
-            this.hdrClamper = hdrClamper;
-            this.clamperController = clamperController;
-            this.hbmMetadata = hbmMetadata;
-            this.brightnessMappingStrategy = brightnessMappingStrategy;
-            this.injector = injector;
-            this.config = config;
-        }
-    }
-
-    private class TestInjector extends DisplayPowerController2.Injector {
-        private final DisplayPowerState mDisplayPowerState;
-        private final DualRampAnimator<DisplayPowerState> mAnimator;
-        private final AutomaticBrightnessController mAutomaticBrightnessController;
-        private final WakelockController mWakelockController;
-        private final BrightnessMappingStrategy mBrightnessMappingStrategy;
-        private final HysteresisLevels mHysteresisLevels;
-        private final ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController;
-        private final HighBrightnessModeController mHighBrightnessModeController;
-
-        private final HdrClamper mHdrClamper;
-
-        private final BrightnessClamperController mClamperController;
-
-        private final DisplayManagerFlags mFlags;
-
-        TestInjector(DisplayPowerState dps, DualRampAnimator<DisplayPowerState> animator,
-                AutomaticBrightnessController automaticBrightnessController,
-                WakelockController wakelockController,
-                BrightnessMappingStrategy brightnessMappingStrategy,
-                HysteresisLevels hysteresisLevels,
-                ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
-                HighBrightnessModeController highBrightnessModeController,
-                HdrClamper hdrClamper,
-                BrightnessClamperController clamperController,
-                DisplayManagerFlags flags) {
-            mDisplayPowerState = dps;
-            mAnimator = animator;
-            mAutomaticBrightnessController = automaticBrightnessController;
-            mWakelockController = wakelockController;
-            mBrightnessMappingStrategy = brightnessMappingStrategy;
-            mHysteresisLevels = hysteresisLevels;
-            mScreenOffBrightnessSensorController = screenOffBrightnessSensorController;
-            mHighBrightnessModeController = highBrightnessModeController;
-            mHdrClamper = hdrClamper;
-            mClamperController = clamperController;
-            mFlags = flags;
-        }
-
-        @Override
-        DisplayPowerController2.Clock getClock() {
-            return mClock::now;
-        }
-
-        @Override
-        DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
-                int displayId, int displayState) {
-            return mDisplayPowerState;
-        }
-
-        @Override
-        DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
-                FloatProperty<DisplayPowerState> firstProperty,
-                FloatProperty<DisplayPowerState> secondProperty) {
-            return mAnimator;
-        }
-
-        @Override
-        WakelockController getWakelockController(int displayId,
-                DisplayPowerCallbacks displayPowerCallbacks) {
-            return mWakelockController;
-        }
-
-        @Override
-        DisplayPowerProximityStateController getDisplayPowerProximityStateController(
-                WakelockController wakelockController, DisplayDeviceConfig displayDeviceConfig,
-                Looper looper, Runnable nudgeUpdatePowerState, int displayId,
-                SensorManager sensorManager) {
-            return new DisplayPowerProximityStateController(wakelockController,
-                    displayDeviceConfig, looper, nudgeUpdatePowerState, displayId,
-                    sensorManager,
-                    new DisplayPowerProximityStateController.Injector() {
-                        @Override
-                        DisplayPowerProximityStateController.Clock createClock() {
-                            return mClock::now;
-                        }
-                    });
-        }
-
-        @Override
-        AutomaticBrightnessController getAutomaticBrightnessController(
-                AutomaticBrightnessController.Callbacks callbacks, Looper looper,
-                SensorManager sensorManager, Sensor lightSensor,
-                SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-                int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
-                float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
-                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
-                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
-                boolean resetAmbientLuxAfterWarmUpConfig,
-                HysteresisLevels ambientBrightnessThresholds,
-                HysteresisLevels screenBrightnessThresholds,
-                HysteresisLevels ambientBrightnessThresholdsIdle,
-                HysteresisLevels screenBrightnessThresholdsIdle, Context context,
-                BrightnessRangeController brightnessRangeController,
-                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, float userLux, float userNits) {
-            return mAutomaticBrightnessController;
-        }
-
-        @Override
-        BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
-                DisplayDeviceConfig displayDeviceConfig,
-                DisplayWhiteBalanceController displayWhiteBalanceController) {
-            return mBrightnessMappingStrategy;
-        }
-
-        @Override
-        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
-                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
-                float[] darkeningThresholdLevels, float minDarkeningThreshold,
-                float minBrighteningThreshold) {
-            return mHysteresisLevels;
-        }
-
-        @Override
-        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
-                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
-                float[] darkeningThresholdLevels, float minDarkeningThreshold,
-                float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
-            return mHysteresisLevels;
-        }
-
-        @Override
-        ScreenOffBrightnessSensorController getScreenOffBrightnessSensorController(
-                SensorManager sensorManager, Sensor lightSensor, Handler handler,
-                ScreenOffBrightnessSensorController.Clock clock, int[] sensorValueToLux,
-                BrightnessMappingStrategy brightnessMapper) {
-            return mScreenOffBrightnessSensorController;
-        }
-
-        @Override
-        HighBrightnessModeController getHighBrightnessModeController(Handler handler, int width,
-                int height, IBinder displayToken, String displayUniqueId, float brightnessMin,
-                float brightnessMax, DisplayDeviceConfig.HighBrightnessModeData hbmData,
-                HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg,
-                Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata,
-                Context context) {
-            return mHighBrightnessModeController;
-        }
-
-        @Override
-        BrightnessRangeController getBrightnessRangeController(
-                HighBrightnessModeController hbmController, Runnable modeChangeCallback,
-                DisplayDeviceConfig displayDeviceConfig, Handler handler,
-                DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) {
-            return new BrightnessRangeController(hbmController, modeChangeCallback,
-                    displayDeviceConfig, mHdrClamper, mFlags, displayToken, info);
-        }
-
-        @Override
-        BrightnessClamperController getBrightnessClamperController(Handler handler,
-                BrightnessClamperController.ClamperChangeListener clamperChangeListener,
-                BrightnessClamperController.DisplayDeviceData data, Context context,
-                DisplayManagerFlags flags) {
-            return mClamperController;
-        }
-
-        @Override
-        DisplayWhiteBalanceController getDisplayWhiteBalanceController(Handler handler,
-                SensorManager sensorManager, Resources resources) {
-            return mDisplayWhiteBalanceControllerMock;
-        }
-    }
-}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 943862f..88a9758 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -18,6 +18,8 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
 
 import static org.junit.Assert.assertNotNull;
@@ -67,15 +69,16 @@
 import android.view.DisplayInfo;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.internal.util.test.LocalServiceKeeperRule;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.server.LocalServices;
 import com.android.server.am.BatteryStatsService;
 import com.android.server.display.RampAnimator.DualRampAnimator;
 import com.android.server.display.brightness.BrightnessEvent;
+import com.android.server.display.brightness.clamper.BrightnessClamperController;
+import com.android.server.display.brightness.clamper.HdrClamper;
 import com.android.server.display.color.ColorDisplayService;
 import com.android.server.display.config.SensorData;
 import com.android.server.display.feature.DisplayManagerFlags;
@@ -85,6 +88,7 @@
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.testutils.OffsettableClock;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -147,7 +151,6 @@
     private DisplayManagerFlags mDisplayManagerFlagsMock;
     @Mock
     private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
-
     @Captor
     private ArgumentCaptor<SensorEventListener> mSensorEventListenerCaptor;
 
@@ -165,9 +168,6 @@
                     .build();
 
     @Rule
-    public LocalServiceKeeperRule mLocalServiceKeeperRule = new LocalServiceKeeperRule();
-
-    @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     @Before
@@ -183,10 +183,9 @@
         Settings.System.putFloatForUser(mContext.getContentResolver(),
                 Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0, UserHandle.USER_CURRENT);
 
-        mLocalServiceKeeperRule.overrideLocalService(
-                WindowManagerPolicy.class, mWindowManagerPolicyMock);
-        mLocalServiceKeeperRule.overrideLocalService(
-                ColorDisplayService.ColorDisplayServiceInternal.class, mCdsiMock);
+        addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
+        addLocalServiceMock(ColorDisplayService.ColorDisplayServiceInternal.class,
+                mCdsiMock);
 
         mContext.addMockSystemService(PowerManager.class, mPowerManagerMock);
 
@@ -203,6 +202,12 @@
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
     }
 
+    @After
+    public void tearDown() {
+        LocalServices.removeServiceForTest(WindowManagerPolicy.class);
+        LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
+    }
+
     @Test
     public void testReleaseProxSuspendBlockersOnExit() throws Exception {
         when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
@@ -222,19 +227,18 @@
         advanceTime(1);
 
         // two times, one for unfinished business and one for proximity
-        verify(mDisplayPowerCallbacksMock).acquireSuspendBlocker(
-                mHolder.dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
-        verify(mDisplayPowerCallbacksMock).acquireSuspendBlocker(
-                mHolder.dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
+        verify(mHolder.wakelockController, times(2)).acquireWakelock(
+                WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
+        verify(mHolder.wakelockController).acquireWakelock(
+                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
 
         mHolder.dpc.stop();
         advanceTime(1);
-
         // two times, one for unfinished business and one for proximity
-        verify(mDisplayPowerCallbacksMock).releaseSuspendBlocker(
-                mHolder.dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
-        verify(mDisplayPowerCallbacksMock).releaseSuspendBlocker(
-                mHolder.dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
+        verify(mHolder.wakelockController, times(2)).acquireWakelock(
+                WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
+        verify(mHolder.wakelockController).acquireWakelock(
+                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
     }
 
     @Test
@@ -316,14 +320,13 @@
 
     @Test
     public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() {
-        DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-
         when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
         // send a display power request
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
         dpr.useProximitySensor = true;
+        final DisplayPowerControllerHolder followerDpc =
+                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
         followerDpc.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
 
         // Run updatePowerState
@@ -334,7 +337,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowers_BothDpcsSupportNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
@@ -363,13 +365,10 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(leadBrightness);
         listener.onBrightnessChanged(leadBrightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(), anyFloat(), eq(false));
         verify(followerDpc.animator).animateTo(eq(followerBrightness), anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+                anyFloat(), eq(false));
 
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(leadBrightness);
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(followerBrightness);
         clearInvocations(mHolder.animator, followerDpc.animator);
 
         // Test the same float scale value
@@ -388,7 +387,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
@@ -421,7 +419,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
@@ -452,7 +449,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
@@ -485,7 +481,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowers_AutomaticBrightness() {
         Settings.System.putInt(mContext.getContentResolver(),
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
@@ -557,7 +552,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowersRemoval_RemoveSingleFollower() {
         DisplayPowerControllerHolder followerDpc = createDisplayPowerController(FOLLOWER_DISPLAY_ID,
                 FOLLOWER_UNIQUE_ID);
@@ -650,7 +644,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 294107062)
     public void testDisplayBrightnessFollowersRemoval_RemoveAllFollowers() {
         DisplayPowerControllerHolder followerHolder =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
@@ -737,6 +730,82 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_FAST_HDR_TRANSITIONS)
+    public void testDisplayBrightnessHdr_SkipAnimationOnHdrAppearance() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        final float sdrBrightness = 0.1f;
+        final float hdrBrightness = 0.3f;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
+                any(BrightnessEvent.class))).thenReturn(sdrBrightness);
+        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(1.0f);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(sdrBrightness), eq(sdrBrightness),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(sdrBrightness);
+        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(sdrBrightness);
+
+        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
+                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
+        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
+        clearInvocations(mHolder.animator);
+
+        mHolder.dpc.updateBrightness();
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(hdrBrightness), eq(sdrBrightness),
+                eq(BRIGHTNESS_RAMP_RATE_MINIMUM), eq(false));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_FAST_HDR_TRANSITIONS)
+    public void testDisplayBrightnessHdr_SkipAnimationOnHdrRemoval() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        final float sdrBrightness = 0.1f;
+        final float hdrBrightness = 0.3f;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.automaticBrightnessController.isInIdleMode()).thenReturn(true);
+        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
+                any(BrightnessEvent.class))).thenReturn(sdrBrightness);
+        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(1.0f);
+
+        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
+                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
+        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(hdrBrightness), eq(sdrBrightness),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(hdrBrightness);
+        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(sdrBrightness);
+        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
+                BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF);
+
+        clearInvocations(mHolder.animator);
+
+        mHolder.dpc.updateBrightness();
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(sdrBrightness), eq(sdrBrightness),
+                eq(BRIGHTNESS_RAMP_RATE_MINIMUM), eq(false));
+    }
+
+    @Test
     public void testDoesNotSetScreenStateForNonDefaultDisplayUntilBootCompleted() {
         // We should still set screen state for the default display
         DisplayPowerRequest dpr = new DisplayPowerRequest();
@@ -761,6 +830,8 @@
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
 
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
+
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_OFF;
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -798,6 +869,7 @@
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
 
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_DOZE;
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -842,7 +914,6 @@
         Settings.System.putInt(mContext.getContentResolver(),
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ false);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
@@ -1048,7 +1119,6 @@
         mContext.getOrCreateTestableResources().addOverride(
                 com.android.internal.R.bool.config_persistBrightnessNitsForDefaultDisplay,
                 true);
-
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
 
@@ -1199,76 +1269,98 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FAST_HDR_TRANSITIONS)
-    public void testDisplayBrightnessHdr_SkipAnimationOnHdrAppearance() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        final float sdrBrightness = 0.1f;
-        final float hdrBrightness = 0.3f;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(sdrBrightness);
+    public void testRampRateForHdrContent_HdrClamperOff() {
+        float hdrBrightness = 0.8f;
+        float clampedBrightness = 0.6f;
+        float transitionRate = 1.5f;
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(sdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
-
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(sdrBrightness);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(sdrBrightness);
-
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
+        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
         when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
                 BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
         when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
-        clearInvocations(mHolder.animator);
+        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(clampedBrightness);
+        when(mHolder.hdrClamper.getTransitionRate()).thenReturn(transitionRate);
 
-        mHolder.dpc.updateBrightness();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.animator).animateTo(eq(hdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_MINIMUM), eq(false));
+        verify(mHolder.animator, atLeastOnce()).animateTo(eq(hdrBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FAST_HDR_TRANSITIONS)
-    public void testDisplayBrightnessHdr_SkipAnimationOnHdrRemoval() {
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        final float sdrBrightness = 0.1f;
-        final float hdrBrightness = 0.3f;
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
-                any(BrightnessEvent.class))).thenReturn(sdrBrightness);
-
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
-        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(hdrBrightness);
+    public void testRampRateForHdrContent_HdrClamperOn() {
+        float clampedBrightness = 0.6f;
+        float transitionRate = 1.5f;
+        when(mDisplayManagerFlagsMock.isHdrClamperEnabled()).thenReturn(true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
+        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
+        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
+                BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
+        when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(PowerManager.BRIGHTNESS_MAX);
+        when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(clampedBrightness);
+        when(mHolder.hdrClamper.getTransitionRate()).thenReturn(transitionRate);
+
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.animator).animateTo(eq(hdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+        verify(mHolder.animator, atLeastOnce()).animateTo(eq(clampedBrightness), anyFloat(),
+                eq(transitionRate), eq(true));
+    }
 
-        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(hdrBrightness);
-        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(sdrBrightness);
-        when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
-                BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF);
+    @Test
+    public void testRampRateForClampersControllerApplied() {
+        float transitionRate = 1.5f;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
+        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
+        when(mHolder.clamperController.clamp(any(), anyFloat(), anyBoolean())).thenAnswer(
+                invocation -> DisplayBrightnessState.builder()
+                        .setIsSlowChange(invocation.getArgument(2))
+                        .setBrightness(invocation.getArgument(1))
+                        .setMaxBrightness(PowerManager.BRIGHTNESS_MAX)
+                        .setCustomAnimationRate(transitionRate).build());
 
-        clearInvocations(mHolder.animator);
-
-        mHolder.dpc.updateBrightness();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.animator).animateTo(eq(sdrBrightness), eq(sdrBrightness),
-                eq(BRIGHTNESS_RAMP_RATE_MINIMUM), eq(false));
+        verify(mHolder.animator, atLeastOnce()).animateTo(anyFloat(), anyFloat(),
+                eq(transitionRate), anyBoolean());
+    }
+
+    @Test
+    public void testRampRateForClampersControllerNotApplied_ifDoze() {
+        float transitionRate = 1.5f;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        dpr.dozeScreenState = Display.STATE_UNKNOWN;
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f);
+        when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f);
+        when(mHolder.clamperController.clamp(any(), anyFloat(), anyBoolean())).thenAnswer(
+                invocation -> DisplayBrightnessState.builder()
+                        .setIsSlowChange(invocation.getArgument(2))
+                        .setBrightness(invocation.getArgument(1))
+                        .setMaxBrightness(PowerManager.BRIGHTNESS_MAX)
+                        .setCustomAnimationRate(transitionRate).build());
+
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator, atLeastOnce()).animateTo(anyFloat(), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE), anyBoolean());
+        verify(mHolder.animator, never()).animateTo(anyFloat(), anyFloat(),
+                eq(transitionRate), anyBoolean());
     }
 
     @Test
@@ -1285,14 +1377,14 @@
 
         setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
                 mHolder.config, /* isEnabled= */ true);
-
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
                 BRIGHTNESS_RAMP_DECREASE_MAX);
 
-        // switch to idle
+        // switch to idle mode
         mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
         advanceTime(1);
 
+        // A second time, when switching to idle mode.
         verify(mHolder.animator, times(2)).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
                 BRIGHTNESS_RAMP_DECREASE_MAX);
     }
@@ -1301,6 +1393,8 @@
     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1)
     public void testRampMaxTimeInteractiveThenIdle_DifferentValues() {
         when(mDisplayManagerFlagsMock.isAdaptiveTone1Enabled()).thenReturn(true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true);
+
         // Send a display power request
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
@@ -1312,14 +1406,14 @@
 
         setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
                 mHolder.config, /* isEnabled= */ true);
-
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
                 BRIGHTNESS_RAMP_DECREASE_MAX);
 
-        // switch to idle
+        // switch to idle mode
         mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
         advanceTime(1);
 
+        // A second time, when switching to idle mode.
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE,
                 BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
     }
@@ -1332,11 +1426,9 @@
         dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
         dpr.useProximitySensor = true;
         mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
-
         // Run updatePowerState
         advanceTime(1);
-
-        // once on setup
+        // Once on setup
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
                 BRIGHTNESS_RAMP_DECREASE_MAX);
 
@@ -1346,7 +1438,7 @@
         // switch to idle mode
         mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
 
-        // second time when switching to idle screen brightness mode
+        // A second time when switching to idle mode.
         verify(mHolder.animator, times(2)).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
                 BRIGHTNESS_RAMP_DECREASE_MAX);
     }
@@ -1355,6 +1447,7 @@
     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1)
     public void testRampMaxTimeIdle_DifferentValues() {
         when(mDisplayManagerFlagsMock.isAdaptiveTone1Enabled()).thenReturn(true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true);
 
         // Send a display power request
         DisplayPowerRequest dpr = new DisplayPowerRequest();
@@ -1374,6 +1467,7 @@
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE,
                 BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
     }
+
     @Test
     public void testDozeScreenStateOverride_toSupportedOffloadStateFromDoze_DisplayStateChanges() {
         // set up.
@@ -1451,6 +1545,89 @@
         verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
     }
 
+    @Test
+    public void testBrightnessFromOffload() {
+        when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        float brightness = 0.34f;
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
+                any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+
+        mHolder.dpc.setBrightnessFromOffload(brightness);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // One triggered by handleBrightnessModeChange, another triggered by
+        // setBrightnessFromOffload
+        verify(mHolder.animator, times(2)).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+    }
+
+    @Test
+    public void testSwitchToDozeAutoBrightnessMode() {
+        when(mDisplayManagerFlagsMock.areAutoBrightnessModesEnabled()).thenReturn(true);
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // One triggered by handleBrightnessModeChange, another triggered by requestPowerState
+        verify(mHolder.automaticBrightnessController, times(2))
+                .switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
+
+        // Back to default mode
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController).switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
+    }
+
+    @Test
+    public void testDoesNotSwitchFromIdleToDozeAutoBrightnessMode() {
+        when(mDisplayManagerFlagsMock.areAutoBrightnessModesEnabled()).thenReturn(true);
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+        when(mHolder.automaticBrightnessController.isInIdleMode()).thenReturn(true);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController, never())
+                .switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
+    }
+
+    @Test
+    public void testDoesNotSwitchDozeAutoBrightnessModeIfFeatureFlagOff() {
+        when(mDisplayManagerFlagsMock.areAutoBrightnessModesEnabled()).thenReturn(false);
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController, never())
+                .switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
+    }
+
+    /**
+     * Creates a mock and registers it to {@link LocalServices}.
+     */
+    private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
+        LocalServices.removeServiceForTest(clazz);
+        LocalServices.addService(clazz, mock);
+    }
+
     private void advanceTime(long timeMs) {
         mClock.fastForward(timeMs);
         mTestLooper.dispatchAll();
@@ -1505,10 +1682,10 @@
                 .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE);
         when(displayDeviceConfigMock.getBrightnessRampSlowIncrease())
                 .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_INCREASE);
-        when(displayDeviceConfigMock.getBrightnessRampSlowDecreaseIdle())
-                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE_IDLE);
         when(displayDeviceConfigMock.getBrightnessRampSlowIncreaseIdle())
                 .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_INCREASE_IDLE);
+        when(displayDeviceConfigMock.getBrightnessRampSlowDecreaseIdle())
+                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE_IDLE);
 
         when(displayDeviceConfigMock.getBrightnessRampIncreaseMaxMillis())
                 .thenReturn(BRIGHTNESS_RAMP_INCREASE_MAX);
@@ -1531,18 +1708,28 @@
         final DualRampAnimator<DisplayPowerState> animator = mock(DualRampAnimator.class);
         final AutomaticBrightnessController automaticBrightnessController =
                 mock(AutomaticBrightnessController.class);
+        final WakelockController wakelockController = mock(WakelockController.class);
         final BrightnessMappingStrategy brightnessMappingStrategy =
                 mock(BrightnessMappingStrategy.class);
         final HysteresisLevels hysteresisLevels = mock(HysteresisLevels.class);
         final ScreenOffBrightnessSensorController screenOffBrightnessSensorController =
                 mock(ScreenOffBrightnessSensorController.class);
         final HighBrightnessModeController hbmController = mock(HighBrightnessModeController.class);
+        final HdrClamper hdrClamper = mock(HdrClamper.class);
+        BrightnessClamperController clamperController = mock(BrightnessClamperController.class);
 
         when(hbmController.getCurrentBrightnessMax()).thenReturn(PowerManager.BRIGHTNESS_MAX);
+        when(clamperController.clamp(any(), anyFloat(), anyBoolean())).thenAnswer(
+                invocation -> DisplayBrightnessState.builder()
+                        .setIsSlowChange(invocation.getArgument(2))
+                        .setBrightness(invocation.getArgument(1))
+                        .setMaxBrightness(PowerManager.BRIGHTNESS_MAX)
+                        .setCustomAnimationRate(-1).build());
 
-        DisplayPowerController.Injector injector = spy(new TestInjector(displayPowerState, animator,
-                automaticBrightnessController, brightnessMappingStrategy, hysteresisLevels,
-                screenOffBrightnessSensorController, hbmController));
+        TestInjector injector = spy(new TestInjector(displayPowerState, animator,
+                automaticBrightnessController, wakelockController, brightnessMappingStrategy,
+                hysteresisLevels, screenOffBrightnessSensorController, hbmController, hdrClamper,
+                clamperController, mDisplayManagerFlagsMock));
 
         final LogicalDisplay display = mock(LogicalDisplay.class);
         final DisplayDevice device = mock(DisplayDevice.class);
@@ -1555,12 +1742,14 @@
         final DisplayPowerController dpc = new DisplayPowerController(
                 mContext, injector, mDisplayPowerCallbacksMock, mHandler,
                 mSensorManagerMock, mDisplayBlankerMock, display,
-                mBrightnessTrackerMock, brightnessSetting, () -> {},
+                mBrightnessTrackerMock, brightnessSetting, () -> {
+        },
                 hbmMetadata, /* bootCompleted= */ false, mDisplayManagerFlagsMock);
 
         return new DisplayPowerControllerHolder(dpc, display, displayPowerState, brightnessSetting,
-                animator, automaticBrightnessController, screenOffBrightnessSensorController,
-                hbmController, hbmMetadata, brightnessMappingStrategy, injector, config);
+                animator, automaticBrightnessController, wakelockController,
+                screenOffBrightnessSensorController, hbmController, hdrClamper, clamperController,
+                hbmMetadata, brightnessMappingStrategy, injector, config);
     }
 
     /**
@@ -1574,8 +1763,12 @@
         public final BrightnessSetting brightnessSetting;
         public final DualRampAnimator<DisplayPowerState> animator;
         public final AutomaticBrightnessController automaticBrightnessController;
+        public final WakelockController wakelockController;
         public final ScreenOffBrightnessSensorController screenOffBrightnessSensorController;
         public final HighBrightnessModeController hbmController;
+
+        public final HdrClamper hdrClamper;
+        public final BrightnessClamperController clamperController;
         public final HighBrightnessModeMetadata hbmMetadata;
         public final BrightnessMappingStrategy brightnessMappingStrategy;
         public final DisplayPowerController.Injector injector;
@@ -1585,8 +1778,11 @@
                 DisplayPowerState displayPowerState, BrightnessSetting brightnessSetting,
                 DualRampAnimator<DisplayPowerState> animator,
                 AutomaticBrightnessController automaticBrightnessController,
+                WakelockController wakelockController,
                 ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
                 HighBrightnessModeController hbmController,
+                HdrClamper hdrClamper,
+                BrightnessClamperController clamperController,
                 HighBrightnessModeMetadata hbmMetadata,
                 BrightnessMappingStrategy brightnessMappingStrategy,
                 DisplayPowerController.Injector injector,
@@ -1597,8 +1793,11 @@
             this.brightnessSetting = brightnessSetting;
             this.animator = animator;
             this.automaticBrightnessController = automaticBrightnessController;
+            this.wakelockController = wakelockController;
             this.screenOffBrightnessSensorController = screenOffBrightnessSensorController;
             this.hbmController = hbmController;
+            this.hdrClamper = hdrClamper;
+            this.clamperController = clamperController;
             this.hbmMetadata = hbmMetadata;
             this.brightnessMappingStrategy = brightnessMappingStrategy;
             this.injector = injector;
@@ -1610,24 +1809,39 @@
         private final DisplayPowerState mDisplayPowerState;
         private final DualRampAnimator<DisplayPowerState> mAnimator;
         private final AutomaticBrightnessController mAutomaticBrightnessController;
+        private final WakelockController mWakelockController;
         private final BrightnessMappingStrategy mBrightnessMappingStrategy;
         private final HysteresisLevels mHysteresisLevels;
         private final ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController;
         private final HighBrightnessModeController mHighBrightnessModeController;
 
+        private final HdrClamper mHdrClamper;
+
+        private final BrightnessClamperController mClamperController;
+
+        private final DisplayManagerFlags mFlags;
+
         TestInjector(DisplayPowerState dps, DualRampAnimator<DisplayPowerState> animator,
                 AutomaticBrightnessController automaticBrightnessController,
+                WakelockController wakelockController,
                 BrightnessMappingStrategy brightnessMappingStrategy,
                 HysteresisLevels hysteresisLevels,
                 ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
-                HighBrightnessModeController highBrightnessModeController) {
+                HighBrightnessModeController highBrightnessModeController,
+                HdrClamper hdrClamper,
+                BrightnessClamperController clamperController,
+                DisplayManagerFlags flags) {
             mDisplayPowerState = dps;
             mAnimator = animator;
             mAutomaticBrightnessController = automaticBrightnessController;
+            mWakelockController = wakelockController;
             mBrightnessMappingStrategy = brightnessMappingStrategy;
             mHysteresisLevels = hysteresisLevels;
             mScreenOffBrightnessSensorController = screenOffBrightnessSensorController;
             mHighBrightnessModeController = highBrightnessModeController;
+            mHdrClamper = hdrClamper;
+            mClamperController = clamperController;
+            mFlags = flags;
         }
 
         @Override
@@ -1649,6 +1863,28 @@
         }
 
         @Override
+        WakelockController getWakelockController(int displayId,
+                DisplayPowerCallbacks displayPowerCallbacks) {
+            return mWakelockController;
+        }
+
+        @Override
+        DisplayPowerProximityStateController getDisplayPowerProximityStateController(
+                WakelockController wakelockController, DisplayDeviceConfig displayDeviceConfig,
+                Looper looper, Runnable nudgeUpdatePowerState, int displayId,
+                SensorManager sensorManager) {
+            return new DisplayPowerProximityStateController(wakelockController,
+                    displayDeviceConfig, looper, nudgeUpdatePowerState, displayId,
+                    sensorManager,
+                    new DisplayPowerProximityStateController.Injector() {
+                        @Override
+                        DisplayPowerProximityStateController.Clock createClock() {
+                            return mClock::now;
+                        }
+                    });
+        }
+
+        @Override
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
                 SensorManager sensorManager, Sensor lightSensor,
@@ -1710,6 +1946,23 @@
         }
 
         @Override
+        BrightnessRangeController getBrightnessRangeController(
+                HighBrightnessModeController hbmController, Runnable modeChangeCallback,
+                DisplayDeviceConfig displayDeviceConfig, Handler handler,
+                DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) {
+            return new BrightnessRangeController(hbmController, modeChangeCallback,
+                    displayDeviceConfig, mHdrClamper, mFlags, displayToken, info);
+        }
+
+        @Override
+        BrightnessClamperController getBrightnessClamperController(Handler handler,
+                BrightnessClamperController.ClamperChangeListener clamperChangeListener,
+                BrightnessClamperController.DisplayDeviceData data, Context context,
+                DisplayManagerFlags flags) {
+            return mClamperController;
+        }
+
+        @Override
         DisplayWhiteBalanceController getDisplayWhiteBalanceController(Handler handler,
                 SensorManager sensorManager, Resources resources) {
             return mDisplayWhiteBalanceControllerMock;
diff --git a/services/tests/servicestests/src/com/android/server/am/AnrHelperTest.java b/services/tests/servicestests/src/com/android/server/am/AnrHelperTest.java
index c0051c6..eee3752 100644
--- a/services/tests/servicestests/src/com/android/server/am/AnrHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/AnrHelperTest.java
@@ -133,7 +133,8 @@
         verify(mAnrApp.mErrorState, timeout(TIMEOUT_MS)).appNotResponding(
                 eq(activityShortComponentName), eq(appInfo), eq(parentShortComponentName),
                 eq(parentProcess), eq(aboveSystem), eq(timeoutRecord), eq(mAuxExecutorService),
-                eq(false) /* onlyDumpSelf */, eq(false) /*isContinuousAnr*/, eq(mEarlyDumpFuture));
+                anyBoolean() /* onlyDumpSelf */, eq(false) /*isContinuousAnr*/,
+                eq(mEarlyDumpFuture));
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java
index c10c3c2..9b25f58 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java
@@ -183,7 +183,6 @@
         assertThat(mHistoryManager.doesHistoryExistForUser(mProfileId)).isFalse();
         verify(mDb, times(2)).disableHistory();
     }
-
     @Test
     public void testAddProfile_historyEnabledInPrimary() {
         // create a history
@@ -610,4 +609,14 @@
 
         assertThat(mHistoryManager.isHistoryEnabled(USER_SYSTEM)).isFalse();
     }
+    @Test
+    public void testDelayedPackageRemoval_userLocked() {
+        String pkg = "pkg";
+        mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+        mHistoryManager.onUserUnlocked(USER_SYSTEM);
+        mHistoryManager.onUserStopped(USER_SYSTEM);
+        mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+
+        // no exception, yay
+    }
 }
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java
index 8773cab..1df7012 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java
@@ -24,6 +24,7 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.telephony.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -117,12 +118,28 @@
     private boolean checkCallStatus() {
         List<SubscriptionInfo> infoList = mSubscriptionManager.getActiveSubscriptionInfoList();
         if (infoList == null) return false;
-        return infoList.stream()
-                .filter(s -> (s.getSubscriptionId() != SubscriptionManager.INVALID_SUBSCRIPTION_ID))
-                .anyMatch(s -> isCallOngoingFromState(
-                                        mTelephonyManager
-                                                .createForSubscriptionId(s.getSubscriptionId())
-                                                .getCallStateForSubscription()));
+        if (!Flags.enforceTelephonyFeatureMapping()) {
+            return infoList.stream()
+                    .filter(s -> (s.getSubscriptionId()
+                            != SubscriptionManager.INVALID_SUBSCRIPTION_ID))
+                    .anyMatch(s -> isCallOngoingFromState(
+                            mTelephonyManager
+                                    .createForSubscriptionId(s.getSubscriptionId())
+                                    .getCallStateForSubscription()));
+        } else {
+            return infoList.stream()
+                    .filter(s -> (s.getSubscriptionId()
+                            != SubscriptionManager.INVALID_SUBSCRIPTION_ID))
+                    .anyMatch(s -> {
+                        try {
+                            return isCallOngoingFromState(mTelephonyManager
+                                    .createForSubscriptionId(s.getSubscriptionId())
+                                    .getCallStateForSubscription());
+                        } catch (UnsupportedOperationException e) {
+                            return false;
+                        }
+                    });
+        }
     }
 
     private void updateTelephonyListeners() {
diff --git a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
index 250c3a5..2a6ac98 100644
--- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
+++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
@@ -196,7 +196,7 @@
         // We have READ_PHONE_STATE permission, so return true as long as the AppOps bit hasn't been
         // revoked.
         AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
-        return appOps.noteOp(AppOpsManager.OPSTR_READ_PHONE_STATE, uid, callingPackage,
+        return appOps.noteOpNoThrow(AppOpsManager.OPSTR_READ_PHONE_STATE, uid, callingPackage,
                 callingFeatureId, null) == AppOpsManager.MODE_ALLOWED;
     }
 
@@ -249,7 +249,7 @@
         // We have READ_PHONE_STATE permission, so return true as long as the AppOps bit hasn't been
         // revoked.
         AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
-        return appOps.noteOp(AppOpsManager.OPSTR_READ_PHONE_STATE, uid, callingPackage,
+        return appOps.noteOpNoThrow(AppOpsManager.OPSTR_READ_PHONE_STATE, uid, callingPackage,
                 callingFeatureId, null) == AppOpsManager.MODE_ALLOWED;
     }
 
@@ -521,7 +521,7 @@
         // We have READ_CALL_LOG permission, so return true as long as the AppOps bit hasn't been
         // revoked.
         AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
-        return appOps.noteOp(AppOpsManager.OPSTR_READ_CALL_LOG, uid, callingPackage,
+        return appOps.noteOpNoThrow(AppOpsManager.OPSTR_READ_CALL_LOG, uid, callingPackage,
                 callingPackageName, null) == AppOpsManager.MODE_ALLOWED;
     }
 
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index c7b84a3..79e4ad0 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -10955,6 +10955,9 @@
      * @return A {@link PersistableBundle} containing the config for the given subId, or default
      *         values for an invalid subId.
      *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
+     *
      * @deprecated Use {@link #getConfigForSubId(int, String...)} instead.
      */
     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@@ -11002,6 +11005,9 @@
      * @return A {@link PersistableBundle} with key/value mapping for the specified configuration
      * on success, or an empty (but never null) bundle on failure (for example, when the calling app
      * has no permission).
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      */
     @RequiresPermission(anyOf = {
             Manifest.permission.READ_PHONE_STATE,
@@ -11047,6 +11053,9 @@
      * @param overrideValues Key-value pairs of the values that are to be overridden. If set to
      *                       {@code null}, this will remove all previous overrides and set the
      *                       carrier configuration back to production values.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      * @hide
      */
     @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
@@ -11104,6 +11113,10 @@
      *
      * @see #getConfigForSubId
      * @see #getConfig(String...)
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
+     *
      * @deprecated use {@link #getConfig(String...)} instead.
      */
     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@@ -11138,6 +11151,9 @@
      * configs on success, or an empty (but never null) bundle on failure.
      * @see #getConfigForSubId(int, String...)
      * @see SubscriptionManager#getDefaultSubscriptionId()
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      */
     @RequiresPermission(anyOf = {
             Manifest.permission.READ_PHONE_STATE,
@@ -11189,6 +11205,9 @@
      *
      * <p>This method returns before the reload has completed, and {@link
      * android.service.carrier.CarrierService#onLoadConfig} will be called from an arbitrary thread.
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      */
     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
     @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
@@ -11212,6 +11231,8 @@
      * <p>Depending on simState, the config may be cleared or loaded from config app. This is only
      * used by SubscriptionInfoUpdater.
      *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      * @hide
      */
     @SystemApi
@@ -11234,6 +11255,8 @@
      * Gets the package name for a default carrier service.
      * @return the package name for a default carrier service; empty string if not available.
      *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      * @hide
      */
     @NonNull
@@ -11287,6 +11310,9 @@
      * @param subId the subscription ID, normally obtained from {@link SubscriptionManager}.
      *
      * @see #getConfigForSubId
+     *
+     * @throws UnsupportedOperationException If the device does not have
+     *          {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
      */
     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
     @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 1b47dfe..0dce084 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -18923,6 +18923,62 @@
     }
 
     /**
+     * Enables or disables notifications sent when cellular null cipher or integrity algorithms
+     * are in use by the cellular modem.
+     *
+     * @throws IllegalStateException if the Telephony process is not currently available
+     * @throws SecurityException if the caller does not have the required privileges
+     * @throws UnsupportedOperationException if the modem does not support reporting on ciphering
+     * and integrity algorithms in use
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY)
+    @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
+    @SystemApi
+    public void setEnableNullCipherNotifications(boolean enable) {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                telephony.setEnableNullCipherNotifications(enable);
+            } else {
+                throw new IllegalStateException("telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            Rlog.e(TAG, "setEnableNullCipherNotifications RemoteException", ex);
+            ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get whether notifications are enabled for null cipher or integrity algorithms in use by the
+     * cellular modem.
+     *
+     * @throws IllegalStateException if the Telephony process is not currently available
+     * @throws SecurityException if the caller does not have the required privileges
+     * @throws UnsupportedOperationException if the modem does not support reporting on ciphering
+     * and integrity algorithms in use
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY)
+    @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+    @SystemApi
+    public boolean isNullCipherNotificationsEnabled() {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                return telephony.isNullCipherNotificationsEnabled();
+            } else {
+                throw new IllegalStateException("telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            Rlog.e(TAG, "isNullCipherNotificationsEnabled RemoteException", ex);
+            ex.rethrowFromSystemServer();
+        }
+        return false;
+    }
+
+
+    /**
      * Get current cell broadcast message identifier ranges.
      *
      * @throws SecurityException if the caller does not have the required permission
diff --git a/telephony/java/android/telephony/data/DataCallResponse.java b/telephony/java/android/telephony/data/DataCallResponse.java
index 9dd83d1..b6f9e1f 100644
--- a/telephony/java/android/telephony/data/DataCallResponse.java
+++ b/telephony/java/android/telephony/data/DataCallResponse.java
@@ -451,7 +451,7 @@
 
     /**
      * Return the network validation status that was initiated by {@link
-     * DataService.DataServiceProvider#requestValidation}
+     * DataService.DataServiceProvider#requestNetworkValidation}
      *
      * @return The network validation status of data connection.
      */
@@ -931,7 +931,7 @@
 
         /**
          * Set the network validation status that corresponds to the state of the network validation
-         * request started by {@link DataService.DataServiceProvider#requestValidation}
+         * request started by {@link DataService.DataServiceProvider#requestNetworkValidation}
          *
          * @param status The network validation status.
          * @return The same instance of the builder.
diff --git a/telephony/java/android/telephony/data/DataService.java b/telephony/java/android/telephony/data/DataService.java
index 80e91a3..f04e1c9 100644
--- a/telephony/java/android/telephony/data/DataService.java
+++ b/telephony/java/android/telephony/data/DataService.java
@@ -415,13 +415,13 @@
          *     request validation to the DataService and checks if the request has been submitted.
          */
         @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
-        public void requestValidation(int cid,
+        public void requestNetworkValidation(int cid,
                 @NonNull @CallbackExecutor Executor executor,
                 @NonNull @DataServiceCallback.ResultCode Consumer<Integer> resultCodeCallback) {
             Objects.requireNonNull(executor, "executor cannot be null");
             Objects.requireNonNull(resultCodeCallback, "resultCodeCallback cannot be null");
 
-            Log.d(TAG, "requestValidation: " + cid);
+            Log.d(TAG, "requestNetworkValidation: " + cid);
 
             // The default implementation is to return unsupported.
             executor.execute(() -> resultCodeCallback
@@ -741,7 +741,7 @@
                 case DATA_SERVICE_REQUEST_VALIDATION:
                     if (serviceProvider == null) break;
                     ValidationRequest validationRequest = (ValidationRequest) message.obj;
-                    serviceProvider.requestValidation(
+                    serviceProvider.requestNetworkValidation(
                             validationRequest.cid,
                             validationRequest.executor,
                             FunctionalUtils
@@ -924,9 +924,10 @@
         }
 
         @Override
-        public void requestValidation(int slotIndex, int cid, IIntegerConsumer resultCodeCallback) {
+        public void requestNetworkValidation(int slotIndex, int cid,
+                IIntegerConsumer resultCodeCallback) {
             if (resultCodeCallback == null) {
-                loge("requestValidation: resultCodeCallback is null");
+                loge("requestNetworkValidation: resultCodeCallback is null");
                 return;
             }
             ValidationRequest validationRequest =
diff --git a/telephony/java/android/telephony/data/IDataService.aidl b/telephony/java/android/telephony/data/IDataService.aidl
index 15f8881..c36c302 100644
--- a/telephony/java/android/telephony/data/IDataService.aidl
+++ b/telephony/java/android/telephony/data/IDataService.aidl
@@ -48,5 +48,5 @@
     void cancelHandover(int slotId, int cid, IDataServiceCallback callback);
     void registerForUnthrottleApn(int slotIndex, IDataServiceCallback callback);
     void unregisterForUnthrottleApn(int slotIndex, IDataServiceCallback callback);
-    void requestValidation(int slotId, int cid, IIntegerConsumer callback);
+    void requestNetworkValidation(int slotId, int cid, IIntegerConsumer callback);
 }
diff --git a/telephony/java/android/telephony/ims/RegistrationManager.java b/telephony/java/android/telephony/ims/RegistrationManager.java
index 54ceaed..9f83da9 100644
--- a/telephony/java/android/telephony/ims/RegistrationManager.java
+++ b/telephony/java/android/telephony/ims/RegistrationManager.java
@@ -84,7 +84,7 @@
                 SUGGESTED_ACTION_TRIGGER_PLMN_BLOCK,
                 SUGGESTED_ACTION_TRIGGER_PLMN_BLOCK_WITH_TIMEOUT,
                 SUGGESTED_ACTION_TRIGGER_RAT_BLOCK,
-                SUGGESTED_ACTION_TRIGGER_CLEAR_RAT_BLOCK
+                SUGGESTED_ACTION_TRIGGER_CLEAR_RAT_BLOCKS
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SuggestedAction {}
@@ -116,9 +116,10 @@
 
     /**
      * Indicates that the IMS registration on current RAT failed multiple times.
-     * The radio shall block the current RAT and search for other available RATs in the
-     * background. If no other RAT is available that meets the carrier requirements, the
-     * radio may remain on the current RAT for internet service. The radio clears all
+     * The radio shall block the {@link ImsRegistrationImplBase.ImsRegistrationTech}
+     * included with this and search for other available RATs in the background.
+     * If no other RAT is available that meets the carrier requirements, the
+     * radio may remain on the blocked RAT for internet service. The radio clears all
      * RATs marked as unavailable if the IMS service is registered to the carrier network.
      * @hide
      */
@@ -133,7 +134,7 @@
      */
     @SystemApi
     @FlaggedApi(Flags.FLAG_ADD_RAT_RELATED_SUGGESTED_ACTION_TO_IMS_REGISTRATION)
-    int SUGGESTED_ACTION_TRIGGER_CLEAR_RAT_BLOCK = 4;
+    int SUGGESTED_ACTION_TRIGGER_CLEAR_RAT_BLOCKS = 4;
 
     /**@hide*/
     // Translate ImsRegistrationImplBase API to new AccessNetworkConstant because WLAN
diff --git a/telephony/java/android/telephony/satellite/AntennaPosition.java b/telephony/java/android/telephony/satellite/AntennaPosition.java
index 8842886..d6440fc 100644
--- a/telephony/java/android/telephony/satellite/AntennaPosition.java
+++ b/telephony/java/android/telephony/satellite/AntennaPosition.java
@@ -35,10 +35,10 @@
 @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
 public final class AntennaPosition implements Parcelable {
     /** Antenna direction used for satellite communication. */
-    @NonNull AntennaDirection mAntennaDirection;
+    @NonNull private AntennaDirection mAntennaDirection;
 
     /** Enum corresponding to device hold position to be used by the end user. */
-    @SatelliteManager.DeviceHoldPosition int mSuggestedHoldPosition;
+    @SatelliteManager.DeviceHoldPosition private int mSuggestedHoldPosition;
 
     /**
      * @hide
diff --git a/telephony/java/android/telephony/satellite/ISatelliteStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
similarity index 94%
rename from telephony/java/android/telephony/satellite/ISatelliteStateCallback.aidl
rename to telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
index cd9d81e..9ff73e2 100644
--- a/telephony/java/android/telephony/satellite/ISatelliteStateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
@@ -20,7 +20,7 @@
  * Interface for satellite state change callback.
  * @hide
  */
-oneway interface ISatelliteStateCallback {
+oneway interface ISatelliteModemStateCallback {
     /**
      * Indicates that the satellite modem state has changed.
      *
diff --git a/telephony/java/android/telephony/satellite/PointingInfo.java b/telephony/java/android/telephony/satellite/PointingInfo.java
index 022a856..9440b65 100644
--- a/telephony/java/android/telephony/satellite/PointingInfo.java
+++ b/telephony/java/android/telephony/satellite/PointingInfo.java
@@ -17,6 +17,7 @@
 package android.telephony.satellite;
 
 import android.annotation.FlaggedApi;
+import android.annotation.FloatRange;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.os.Parcel;
@@ -108,11 +109,19 @@
         return sb.toString();
     }
 
+    /**
+     * Returns the azimuth of the satellite, in degrees.
+     */
+    @FloatRange(from = -180, to = 180)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public float getSatelliteAzimuthDegrees() {
         return mSatelliteAzimuthDegrees;
     }
 
+    /**
+     * Returns the elevation of the satellite, in degrees.
+     */
+    @FloatRange(from = -90, to = 90)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public float getSatelliteElevationDegrees() {
         return mSatelliteElevationDegrees;
diff --git a/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java b/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java
index b5763c3..8e79ca5 100644
--- a/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java
@@ -22,10 +22,15 @@
 
 import com.android.internal.telephony.flags.Flags;
 
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
  * A callback class for listening to satellite datagrams.
+ * {@link SatelliteDatagramCallback} is registered to telephony when an app which invokes
+ * {@link SatelliteManager#registerForSatelliteDatagram(Executor, SatelliteDatagramCallback)},
+ * and {@link #onSatelliteDatagramReceived(long, SatelliteDatagram, int, Consumer)} will be invoked
+ * when a new datagram is received from satellite.
  *
  * @hide
  */
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index e09bd20..2a69703 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -75,8 +75,9 @@
     private static final ConcurrentHashMap<SatelliteProvisionStateCallback,
             ISatelliteProvisionStateCallback> sSatelliteProvisionStateCallbackMap =
             new ConcurrentHashMap<>();
-    private static final ConcurrentHashMap<SatelliteStateCallback, ISatelliteStateCallback>
-            sSatelliteStateCallbackMap = new ConcurrentHashMap<>();
+    private static final ConcurrentHashMap<SatelliteModemStateCallback,
+            ISatelliteModemStateCallback>
+            sSatelliteModemStateCallbackMap = new ConcurrentHashMap<>();
     private static final ConcurrentHashMap<SatelliteTransmissionUpdateCallback,
             ISatelliteTransmissionUpdateCallback> sSatelliteTransmissionUpdateCallbackMap =
             new ConcurrentHashMap<>();
@@ -624,6 +625,11 @@
     /**
      * Request to get whether the satellite service is supported on the device.
      *
+     * <p>
+     * Note: This API only checks whether the device supports the satellite feature. The result will
+     * not be affected by whether the device is provisioned.
+     * </p>
+     *
      * @param executor The executor on which the callback will be called.
      * @param callback The callback object to which the result will be delivered.
      *                 If the request is successful, {@link OutcomeReceiver#onResult(Object)}
@@ -1301,21 +1307,22 @@
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     @SatelliteResult public int registerForSatelliteModemStateChanged(
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull SatelliteStateCallback callback) {
+            @NonNull SatelliteModemStateCallback callback) {
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
 
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
-                ISatelliteStateCallback internalCallback = new ISatelliteStateCallback.Stub() {
+                ISatelliteModemStateCallback internalCallback =
+                        new ISatelliteModemStateCallback.Stub() {
                     @Override
                     public void onSatelliteModemStateChanged(int state) {
                         executor.execute(() -> Binder.withCleanCallingIdentity(() ->
                                 callback.onSatelliteModemStateChanged(state)));
                     }
                 };
-                sSatelliteStateCallbackMap.put(callback, internalCallback);
+                sSatelliteModemStateCallbackMap.put(callback, internalCallback);
                 return telephony.registerForSatelliteModemStateChanged(mSubId, internalCallback);
             } else {
                 throw new IllegalStateException("telephony service is null.");
@@ -1332,16 +1339,18 @@
      * If callback was not registered before, the request will be ignored.
      *
      * @param callback The callback that was passed to
-     * {@link #registerForSatelliteModemStateChanged(Executor, SatelliteStateCallback)}.
+     * {@link #registerForSatelliteModemStateChanged(Executor, SatelliteModemStateCallback)}.
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
      */
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
-    public void unregisterForSatelliteModemStateChanged(@NonNull SatelliteStateCallback callback) {
+    public void unregisterForSatelliteModemStateChanged(
+            @NonNull SatelliteModemStateCallback callback) {
         Objects.requireNonNull(callback);
-        ISatelliteStateCallback internalCallback = sSatelliteStateCallbackMap.remove(callback);
+        ISatelliteModemStateCallback internalCallback = sSatelliteModemStateCallbackMap.remove(
+                callback);
 
         try {
             ITelephony telephony = getITelephony();
diff --git a/telephony/java/android/telephony/satellite/SatelliteStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
similarity index 96%
rename from telephony/java/android/telephony/satellite/SatelliteStateCallback.java
rename to telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
index bfe6e10..8d33c88 100644
--- a/telephony/java/android/telephony/satellite/SatelliteStateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
@@ -28,7 +28,7 @@
  */
 @SystemApi
 @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
-public interface SatelliteStateCallback {
+public interface SatelliteModemStateCallback {
     /**
      * Called when satellite modem state changes.
      * @param state The new satellite modem state.
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 9b5ee0c..3ea86c7 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -72,7 +72,7 @@
 import android.telephony.satellite.ISatelliteDatagramCallback;
 import android.telephony.satellite.ISatelliteTransmissionUpdateCallback;
 import android.telephony.satellite.ISatelliteProvisionStateCallback;
-import android.telephony.satellite.ISatelliteStateCallback;
+import android.telephony.satellite.ISatelliteModemStateCallback;
 import android.telephony.satellite.NtnSignalStrength;
 import android.telephony.satellite.SatelliteCapabilities;
 import android.telephony.satellite.SatelliteDatagram;
@@ -2896,7 +2896,7 @@
      */
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
             + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
-    int registerForSatelliteModemStateChanged(int subId, ISatelliteStateCallback callback);
+    int registerForSatelliteModemStateChanged(int subId, ISatelliteModemStateCallback callback);
 
     /**
      * Unregisters for modem state changed from satellite modem.
@@ -2907,7 +2907,7 @@
      */
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
             + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
-    void unregisterForSatelliteModemStateChanged(int subId, ISatelliteStateCallback callback);
+    void unregisterForSatelliteModemStateChanged(int subId, ISatelliteModemStateCallback callback);
 
    /**
      * Register to receive incoming datagrams over satellite.
@@ -3230,4 +3230,32 @@
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
         + "android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)")
     boolean isCellularIdentifierDisclosureNotificationsEnabled();
+
+    /**
+     * Enables or disables notifications sent when cellular null cipher or integrity algorithms
+     * are in use by the cellular modem.
+     *
+     * @throws IllegalStateException if the Telephony process is not currently available
+     * @throws SecurityException if the caller does not have the required privileges
+     * @throws UnsupportedOperationException if the modem does not support reporting on ciphering
+     * and integrity algorithms in use
+     * @hide
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+        + "android.Manifest.permission.MODIFY_PHONE_STATE)")
+    void setEnableNullCipherNotifications(boolean enable);
+
+    /**
+     * Get whether notifications are enabled for null cipher or integrity algorithms in use by the
+     * cellular modem.
+     *
+     * @throws IllegalStateException if the Telephony process is not currently available
+     * @throws SecurityException if the caller does not have the required privileges
+     * @throws UnsupportedOperationException if the modem does not support reporting on ciphering
+     * and integrity algorithms in use
+     * @hide
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+        + "android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)")
+    boolean isNullCipherNotificationsEnabled();
 }
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
index 80c1e5be..217659e 100644
--- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
@@ -43,9 +43,7 @@
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
-import java.util.Optional;
 import java.util.stream.Collectors;
 
 /**
@@ -178,8 +176,14 @@
             this.deviceFrameRate = deviceFrameRate;
         }
 
+        FrameRateTimeoutException(List<Float> expectedFrameRates, float deviceFrameRate) {
+            this.expectedFrameRates = expectedFrameRates;
+            this.deviceFrameRate = deviceFrameRate;
+        }
+
         public float expectedFrameRate;
         public float deviceFrameRate;
+        public List<Float> expectedFrameRates;
     }
 
     public enum Api {
@@ -502,31 +506,37 @@
     }
 
     private void waitForStableFrameRate(TestSurface... surfaces) throws InterruptedException {
-        verifyCompatibleAndStableFrameRate(0, surfaces);
+        verifyFrameRates(List.of(), surfaces);
     }
 
     private void verifyExactAndStableFrameRate(
             float expectedFrameRate,
             TestSurface... surfaces) throws InterruptedException {
-        verifyFrameRate(expectedFrameRate, false, surfaces);
+        verifyFrameRate(List.of(expectedFrameRate), false, surfaces);
     }
 
     private void verifyCompatibleAndStableFrameRate(
             float expectedFrameRate,
             TestSurface... surfaces) throws InterruptedException {
-        verifyFrameRate(expectedFrameRate, true, surfaces);
+        verifyFrameRate(List.of(expectedFrameRate), true, surfaces);
     }
 
-    // Set expectedFrameRate to 0.0 to verify only stable frame rate.
-    private void verifyFrameRate(
-            float expectedFrameRate, boolean multiplesAllowed,
+    /** Verify stable frame rate at one of the expectedFrameRates. */
+    private void verifyFrameRates(List<Float> expectedFrameRates, TestSurface... surfaces)
+            throws InterruptedException {
+        verifyFrameRate(expectedFrameRates, true, surfaces);
+    }
+
+    // Set expectedFrameRates to empty to verify only stable frame rate.
+    private void verifyFrameRate(List<Float> expectedFrameRates, boolean multiplesAllowed,
             TestSurface... surfaces) throws InterruptedException {
         Log.i(TAG, "Verifying compatible and stable frame rate");
         long nowNanos = System.nanoTime();
         long gracePeriodEndTimeNanos =
                 nowNanos + FRAME_RATE_SWITCH_GRACE_PERIOD_SECONDS * 1_000_000_000L;
         while (true) {
-            if (expectedFrameRate > FRAME_RATE_TOLERANCE) { // expectedFrameRate > 0
+            if (expectedFrameRates.size() == 1) {
+                float expectedFrameRate = expectedFrameRates.get(0);
                 // Wait until we switch to a compatible frame rate.
                 Log.i(TAG,
                         String.format(
@@ -557,11 +567,25 @@
             while (endTimeNanos > nowNanos) {
                 int numModeChangedEvents = mModeChangedEvents.size();
                 if (waitForEvents(endTimeNanos, surfaces)) {
-                    Log.i(TAG,
-                            String.format("Stable frame rate %.2f verified",
-                                    multiplesAllowed ? mDisplayModeRefreshRate
-                                                     : mDisplayRefreshRate));
-                    return;
+                    // Verify any expected frame rate since there are multiple that will suffice.
+                    // Mainly to account for running tests on real devices, where other non-test
+                    // layers may affect the outcome.
+                    if (expectedFrameRates.size() > 1) {
+                        for (float expectedFrameRate : expectedFrameRates) {
+                            if (isFrameRateMultiple(mDisplayModeRefreshRate, expectedFrameRate)) {
+                                return;
+                            }
+                        }
+                        // The frame rate is stable but it is not one of the expected frame rates.
+                        throw new FrameRateTimeoutException(
+                                expectedFrameRates, mDisplayModeRefreshRate);
+                    } else {
+                        Log.i(TAG,
+                                String.format("Stable frame rate %.2f verified",
+                                        multiplesAllowed ? mDisplayModeRefreshRate
+                                                         : mDisplayRefreshRate));
+                        return;
+                    }
                 }
                 nowNanos = System.nanoTime();
                 if (mModeChangedEvents.size() > numModeChangedEvents) {
@@ -623,12 +647,28 @@
                             // caused the timeout failure. Wait for a bit to see if we get notified
                             // of a precondition violation, and if so, retry the test. Otherwise
                             // fail.
-                            assertTrue(String.format(
-                                               "Timed out waiting for a stable and compatible frame"
-                                                       + " rate. expected=%.2f received=%.2f."
-                                                       + " Stack trace: " + stackTrace,
-                                               exc.expectedFrameRate, exc.deviceFrameRate),
-                                    waitForPreconditionViolation());
+                            if (exc.expectedFrameRates.isEmpty()) {
+                                assertTrue(
+                                        String.format(
+                                                "Timed out waiting for a stable and compatible"
+                                                        + " frame rate."
+                                                        + " expected=%.2f received=%.2f."
+                                                        + " Stack trace: " + stackTrace,
+                                                exc.expectedFrameRate, exc.deviceFrameRate),
+                                        waitForPreconditionViolation());
+                            } else {
+                                assertTrue(
+                                        String.format(
+                                                "Timed out waiting for a stable and compatible"
+                                                        + " frame rate."
+                                                        + " expected={%.2f} received=%.2f."
+                                                        + " Stack trace: " + stackTrace,
+                                                exc.expectedFrameRates.stream()
+                                                        .map(Object::toString)
+                                                        .collect(Collectors.joining(", ")),
+                                                exc.deviceFrameRate),
+                                        waitForPreconditionViolation());
+                            }
                         }
 
                         if (!testPassed) {
@@ -679,10 +719,15 @@
                     "**** Running testSurfaceControlFrameRateCompatibility with compatibility "
                             + compatibility);
 
-            float expectedFrameRate = getExpectedFrameRateForCompatibility(compatibility);
+            List<Float> expectedFrameRates = getExpectedFrameRateForCompatibility(compatibility);
+            Log.i(TAG,
+                    "Expected frame rates: "
+                            + expectedFrameRates.stream()
+                                      .map(Object::toString)
+                                      .collect(Collectors.joining(", ")));
             int initialNumEvents = mModeChangedEvents.size();
             surface.setFrameRate(30.f, compatibility);
-            verifyExactAndStableFrameRate(expectedFrameRate, surface);
+            verifyFrameRates(expectedFrameRates, surface);
             verifyModeSwitchesDontChangeResolution(initialNumEvents, mModeChangedEvents.size());
         });
     }
@@ -699,10 +744,10 @@
         runOneSurfaceTest((TestSurface surface) -> {
             Log.i(TAG, "**** Running testSurfaceControlFrameRateCategory for category " + category);
 
-            float expectedFrameRate = getExpectedFrameRateForCategory(category);
+            List<Float> expectedFrameRates = getExpectedFrameRateForCategory(category);
             int initialNumEvents = mModeChangedEvents.size();
             surface.setFrameRateCategory(category);
-            verifyCompatibleAndStableFrameRate(expectedFrameRate, surface);
+            verifyFrameRates(expectedFrameRates, surface);
             verifyModeSwitchesDontChangeResolution(initialNumEvents, mModeChangedEvents.size());
         });
     }
@@ -771,41 +816,41 @@
                 "frame rate strategy=" + parentStrategy);
     }
 
-    private float getExpectedFrameRateForCompatibility(int compatibility) {
+    private List<Float> getExpectedFrameRateForCompatibility(int compatibility) {
         assumeTrue("**** testSurfaceControlFrameRateCompatibility SKIPPED for compatibility "
                         + compatibility,
                 compatibility == Surface.FRAME_RATE_COMPATIBILITY_GTE);
 
         Display display = getDisplay();
-        Optional<Float> expectedFrameRate = getRefreshRates(display.getMode(), display)
-                                                    .stream()
-                                                    .filter(rate -> rate >= 30.f)
-                                                    .min(Comparator.naturalOrder());
+        List<Float> expectedFrameRates = getRefreshRates(display.getMode(), display)
+                                                 .stream()
+                                                 .filter(rate -> rate >= 30.f)
+                                                 .collect(Collectors.toList());
 
         assumeTrue("**** testSurfaceControlFrameRateCompatibility SKIPPED because no refresh rate "
                         + "is >= 30",
-                expectedFrameRate.isPresent());
-        return expectedFrameRate.get();
+                !expectedFrameRates.isEmpty());
+        return expectedFrameRates;
     }
 
-    private float getExpectedFrameRateForCategory(int category) {
+    private List<Float> getExpectedFrameRateForCategory(int category) {
         Display display = getDisplay();
         List<Float> frameRates = getRefreshRates(display.getMode(), display);
 
         if (category == Surface.FRAME_RATE_CATEGORY_DEFAULT) {
             // Max due to default vote and no other frame rate specifications.
-            return Collections.max(frameRates);
+            return List.of(Collections.max(frameRates));
         } else if (category == Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE) {
-            return Collections.min(frameRates);
+            return frameRates;
         }
 
         FpsRange categoryRange = convertCategory(category);
-        Optional<Float> expectedFrameRate = frameRates.stream()
-                                                    .filter(fps -> categoryRange.includes(fps))
-                                                    .min(Comparator.naturalOrder());
+        List<Float> expectedFrameRates = frameRates.stream()
+                                                 .filter(fps -> categoryRange.includes(fps))
+                                                 .collect(Collectors.toList());
         assumeTrue("**** testSurfaceControlFrameRateCategory SKIPPED for category " + category,
-                expectedFrameRate.isPresent());
-        return expectedFrameRate.get();
+                !expectedFrameRates.isEmpty());
+        return expectedFrameRates;
     }
 
     private FpsRange convertCategory(int category) {
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
index 29cbf01..e3988cd 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
@@ -24,8 +24,8 @@
 import android.view.View;
 import android.widget.ToggleButton;
 
+import androidx.window.embedding.ActivityEmbeddingController;
 import androidx.window.embedding.SplitAttributes;
-import androidx.window.embedding.SplitAttributesCalculatorParams;
 import androidx.window.embedding.SplitController;
 
 /**
@@ -66,7 +66,9 @@
                     @Override
                     public void onClick(View v) {
                         // This triggers a recalcuation of splitatributes.
-                        mSplitController.invalidateTopVisibleSplitAttributes();
+                        ActivityEmbeddingController
+                                .getInstance(ActivityEmbeddingSecondaryActivity.this)
+                                .invalidateTopVisibleActivityStacks();
                     }
         });
         findViewById(R.id.secondary_enter_pip_button).setOnClickListener(
diff --git a/tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java b/tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java
index be479f2..1b02792 100644
--- a/tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java
+++ b/tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java
@@ -25,6 +25,7 @@
 import android.security.Flags;
 
 import com.android.blockdevicewriter.BlockDeviceWriter;
+import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
@@ -52,7 +53,6 @@
     private static final String TARGET_PACKAGE = "com.android.fsverity";
 
     private static final String BASENAME = "test.file";
-    private static final String TARGET_PATH = "/data/data/" + TARGET_PACKAGE + "/files/" + BASENAME;
 
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
@@ -63,11 +63,11 @@
         prepareTest(10000);
 
         ITestDevice device = getDevice();
-        BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 0);
-        BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 8192);
+        BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 0);
+        BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 8192);
         BlockDeviceWriter.dropCaches(device);
 
-        verifyRead(TARGET_PATH, "0,2");
+        verifyRead(getTargetFilePath(), "0,2");
     }
 
     @Test
@@ -75,12 +75,17 @@
         prepareTest(128 * 4096 + 1);
 
         ITestDevice device = getDevice();
-        BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 4096);
-        BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 100 * 4096);
-        BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 128 * 4096 + 1);
+        BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 4096);
+        BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 100 * 4096);
+        BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 128 * 4096 + 1);
         BlockDeviceWriter.dropCaches(device);
 
-        verifyRead(TARGET_PATH, "1,100,128");
+        verifyRead(getTargetFilePath(), "1,100,128");
+    }
+
+    private String getTargetFilePath() throws DeviceNotAvailableException {
+        return "/data/user/" + getDevice().getCurrentUser() + "/" + TARGET_PACKAGE + "/files/"
+                + BASENAME;
     }
 
     private void prepareTest(int fileSize) throws Exception {
@@ -97,7 +102,7 @@
         options.setTestClassName(TARGET_PACKAGE + ".Helper");
         options.setTestMethodName("verifyFileRead");
         options.addInstrumentationArg("brokenBlockIndicesCsv", indicesCsv);
-        options.addInstrumentationArg("filePath", TARGET_PATH);
+        options.addInstrumentationArg("filePath", getTargetFilePath());
         assertThat(runDeviceTests(options)).isTrue();
     }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
index 692c8a8..49665f7 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
@@ -27,15 +27,18 @@
 import static android.net.vcn.VcnGatewayConnectionConfig.VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY;
 
 import static com.android.server.vcn.VcnGatewayConnection.DUMMY_ADDR;
+import static com.android.server.vcn.VcnGatewayConnection.SAFEMODE_TIMEOUT_SECONDS;
 import static com.android.server.vcn.VcnGatewayConnection.VcnChildSessionConfiguration;
 import static com.android.server.vcn.VcnGatewayConnection.VcnIkeSession;
 import static com.android.server.vcn.VcnGatewayConnection.VcnNetworkAgent;
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 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.CALLS_REAL_METHODS;
@@ -55,6 +58,7 @@
 import android.net.TelephonyNetworkSpecifier;
 import android.net.vcn.VcnGatewayConnectionConfig;
 import android.net.vcn.VcnGatewayConnectionConfigTest;
+import android.net.vcn.VcnManager;
 import android.net.vcn.VcnTransportInfo;
 import android.net.wifi.WifiInfo;
 import android.os.ParcelUuid;
@@ -81,6 +85,7 @@
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
 
 /** Tests for TelephonySubscriptionTracker */
 @RunWith(AndroidJUnit4.class)
@@ -352,4 +357,71 @@
                         any(Executor.class),
                         any(ConnectivityDiagnosticsCallback.class));
     }
+
+    private void verifyGetSafeModeTimeoutMs(
+            boolean isInTestMode,
+            boolean isConfigTimeoutSupported,
+            PersistableBundleWrapper carrierConfig,
+            long expectedTimeoutMs)
+            throws Exception {
+        doReturn(isInTestMode).when(mVcnContext).isInTestMode();
+        doReturn(isConfigTimeoutSupported).when(mVcnContext).isFlagSafeModeTimeoutConfigEnabled();
+
+        final TelephonySubscriptionSnapshot snapshot = mock(TelephonySubscriptionSnapshot.class);
+        doReturn(carrierConfig).when(snapshot).getCarrierConfigForSubGrp(TEST_SUB_GRP);
+
+        final long result =
+                VcnGatewayConnection.getSafeModeTimeoutMs(mVcnContext, snapshot, TEST_SUB_GRP);
+
+        assertEquals(expectedTimeoutMs, result);
+    }
+
+    @Test
+    public void testGetSafeModeTimeoutMs_configTimeoutUnsupported() throws Exception {
+        verifyGetSafeModeTimeoutMs(
+                false /* isInTestMode */,
+                false /* isConfigTimeoutSupported */,
+                null /* carrierConfig */,
+                TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS));
+    }
+
+    @Test
+    public void testGetSafeModeTimeoutMs_configTimeoutSupported() throws Exception {
+        final int carrierConfigTimeoutSeconds = 20;
+        final PersistableBundleWrapper carrierConfig = mock(PersistableBundleWrapper.class);
+        doReturn(carrierConfigTimeoutSeconds)
+                .when(carrierConfig)
+                .getInt(eq(VcnManager.VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY), anyInt());
+
+        verifyGetSafeModeTimeoutMs(
+                false /* isInTestMode */,
+                true /* isConfigTimeoutSupported */,
+                carrierConfig,
+                TimeUnit.SECONDS.toMillis(carrierConfigTimeoutSeconds));
+    }
+
+    @Test
+    public void testGetSafeModeTimeoutMs_configTimeoutSupported_carrierConfigNull()
+            throws Exception {
+        verifyGetSafeModeTimeoutMs(
+                false /* isInTestMode */,
+                true /* isConfigTimeoutSupported */,
+                null /* carrierConfig */,
+                TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS));
+    }
+
+    @Test
+    public void testGetSafeModeTimeoutMs_configTimeoutOverrideTestModeDefault() throws Exception {
+        final int carrierConfigTimeoutSeconds = 20;
+        final PersistableBundleWrapper carrierConfig = mock(PersistableBundleWrapper.class);
+        doReturn(carrierConfigTimeoutSeconds)
+                .when(carrierConfig)
+                .getInt(eq(VcnManager.VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY), anyInt());
+
+        verifyGetSafeModeTimeoutMs(
+                true /* isInTestMode */,
+                true /* isConfigTimeoutSupported */,
+                carrierConfig,
+                TimeUnit.SECONDS.toMillis(carrierConfigTimeoutSeconds));
+    }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index edced87..4c7b25a 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -67,6 +67,8 @@
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.Vcn.VcnGatewayStatusCallback;
 import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionCallback;
+import com.android.server.vcn.VcnGatewayConnection.VcnIkeSession;
+import com.android.server.vcn.VcnGatewayConnection.VcnNetworkAgent;
 import com.android.server.vcn.VcnGatewayConnection.VcnWakeLock;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController;
 import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
@@ -118,13 +120,7 @@
             NetworkCapabilities networkCapabilities,
             LinkProperties linkProperties,
             boolean isBlocked) {
-        return new UnderlyingNetworkRecord(
-                network,
-                networkCapabilities,
-                linkProperties,
-                isBlocked,
-                false /* isSelected */,
-                0 /* priorityClass */);
+        return new UnderlyingNetworkRecord(network, networkCapabilities, linkProperties, isBlocked);
     }
 
     protected static final String TEST_TCP_BUFFER_SIZES_1 = "1,2,3,4";
@@ -226,6 +222,7 @@
         doReturn(mTestLooper.getLooper()).when(mVcnContext).getLooper();
         doReturn(mVcnNetworkProvider).when(mVcnContext).getVcnNetworkProvider();
         doReturn(mFeatureFlags).when(mVcnContext).getFeatureFlags();
+        doReturn(true).when(mVcnContext).isFlagSafeModeTimeoutConfigEnabled();
 
         doReturn(mUnderlyingNetworkController)
                 .when(mDeps)
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
new file mode 100644
index 0000000..9daba6a
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
@@ -0,0 +1,419 @@
+/*
+ * 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.vcn.routeselection;
+
+import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY;
+import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY;
+
+import static com.android.server.vcn.routeselection.IpSecPacketLossDetector.PACKET_LOSS_UNAVALAIBLE;
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.net.IpSecTransformState;
+import android.os.OutcomeReceiver;
+import android.os.PowerManager;
+
+import com.android.server.vcn.routeselection.IpSecPacketLossDetector.PacketLossCalculator;
+import com.android.server.vcn.routeselection.NetworkMetricMonitor.IpSecTransformWrapper;
+import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.concurrent.TimeUnit;
+
+public class IpSecPacketLossDetectorTest extends NetworkEvaluationTestBase {
+    private static final String TAG = IpSecPacketLossDetectorTest.class.getSimpleName();
+
+    private static final int REPLAY_BITMAP_LEN_BYTE = 512;
+    private static final int REPLAY_BITMAP_LEN_BIT = REPLAY_BITMAP_LEN_BYTE * 8;
+    private static final int IPSEC_PACKET_LOSS_PERCENT_THRESHOLD = 5;
+    private static final long POLL_IPSEC_STATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(30L);
+
+    @Mock private IpSecTransformWrapper mIpSecTransform;
+    @Mock private NetworkMetricMonitorCallback mMetricMonitorCallback;
+    @Mock private PersistableBundleWrapper mCarrierConfig;
+    @Mock private IpSecPacketLossDetector.Dependencies mDependencies;
+    @Spy private PacketLossCalculator mPacketLossCalculator = new PacketLossCalculator();
+
+    @Captor private ArgumentCaptor<OutcomeReceiver> mTransformStateReceiverCaptor;
+    @Captor private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor;
+
+    private IpSecPacketLossDetector mIpSecPacketLossDetector;
+    private IpSecTransformState mTransformStateInitial;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mTransformStateInitial = newTransformState(0, 0, newReplayBitmap(0));
+
+        when(mCarrierConfig.getInt(
+                        eq(VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY), anyInt()))
+                .thenReturn((int) TimeUnit.MILLISECONDS.toSeconds(POLL_IPSEC_STATE_INTERVAL_MS));
+        when(mCarrierConfig.getInt(
+                        eq(VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY),
+                        anyInt()))
+                .thenReturn(IPSEC_PACKET_LOSS_PERCENT_THRESHOLD);
+
+        when(mDependencies.getPacketLossCalculator()).thenReturn(mPacketLossCalculator);
+
+        mIpSecPacketLossDetector =
+                new IpSecPacketLossDetector(
+                        mVcnContext,
+                        mNetwork,
+                        mCarrierConfig,
+                        mMetricMonitorCallback,
+                        mDependencies);
+    }
+
+    private static IpSecTransformState newTransformState(
+            long rxSeqNo, long packtCount, byte[] replayBitmap) {
+        return new IpSecTransformState.Builder()
+                .setRxHighestSequenceNumber(rxSeqNo)
+                .setPacketCount(packtCount)
+                .setReplayBitmap(replayBitmap)
+                .build();
+    }
+
+    private static byte[] newReplayBitmap(int receivedPktCnt) {
+        final BitSet bitSet = new BitSet(REPLAY_BITMAP_LEN_BIT);
+        for (int i = 0; i < receivedPktCnt; i++) {
+            bitSet.set(i);
+        }
+        return Arrays.copyOf(bitSet.toByteArray(), REPLAY_BITMAP_LEN_BYTE);
+    }
+
+    private void verifyStopped() {
+        assertFalse(mIpSecPacketLossDetector.isStarted());
+        assertFalse(mIpSecPacketLossDetector.isValidationFailed());
+        assertNull(mIpSecPacketLossDetector.getLastTransformState());
+
+        // No event scheduled
+        mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
+        assertNull(mTestLooper.nextMessage());
+    }
+
+    @Test
+    public void testInitialization() throws Exception {
+        assertFalse(mIpSecPacketLossDetector.isSelectedUnderlyingNetwork());
+        verifyStopped();
+    }
+
+    private OutcomeReceiver<IpSecTransformState, RuntimeException>
+            startMonitorAndCaptureStateReceiver() {
+        mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(true /* setIsSelected */);
+        mIpSecPacketLossDetector.setInboundTransformInternal(mIpSecTransform);
+
+        // Trigger the runnable
+        mTestLooper.dispatchAll();
+
+        verify(mIpSecTransform)
+                .getIpSecTransformState(any(), mTransformStateReceiverCaptor.capture());
+        return mTransformStateReceiverCaptor.getValue();
+    }
+
+    @Test
+    public void testStartMonitor() throws Exception {
+        final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
+                startMonitorAndCaptureStateReceiver();
+
+        assertTrue(mIpSecPacketLossDetector.isStarted());
+        assertFalse(mIpSecPacketLossDetector.isValidationFailed());
+        assertTrue(mIpSecPacketLossDetector.isSelectedUnderlyingNetwork());
+        assertEquals(mIpSecTransform, mIpSecPacketLossDetector.getInboundTransformInternal());
+
+        // Mock receiving a state
+        xfrmStateReceiver.onResult(mTransformStateInitial);
+
+        // Verify the first polled state is stored
+        assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
+        verify(mPacketLossCalculator, never())
+                .getPacketLossRatePercentage(any(), any(), anyString());
+
+        // Verify next poll is scheduled
+        assertNull(mTestLooper.nextMessage());
+        mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
+        assertNotNull(mTestLooper.nextMessage());
+    }
+
+    @Test
+    public void testStartedMonitor_enterDozeMoze() throws Exception {
+        final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
+                startMonitorAndCaptureStateReceiver();
+
+        // Mock receiving a state
+        xfrmStateReceiver.onResult(mTransformStateInitial);
+        assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
+
+        // Mock entering doze mode
+        final Intent intent = mock(Intent.class);
+        when(intent.getAction()).thenReturn(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
+        when(mPowerManagerService.isDeviceIdleMode()).thenReturn(true);
+
+        verify(mContext).registerReceiver(mBroadcastReceiverCaptor.capture(), any(), any(), any());
+        final BroadcastReceiver broadcastReceiver = mBroadcastReceiverCaptor.getValue();
+        broadcastReceiver.onReceive(mContext, intent);
+
+        assertNull(mIpSecPacketLossDetector.getLastTransformState());
+    }
+
+    @Test
+    public void testStartedMonitor_updateInboundTransform() throws Exception {
+        final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
+                startMonitorAndCaptureStateReceiver();
+
+        // Mock receiving a state
+        xfrmStateReceiver.onResult(mTransformStateInitial);
+        assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
+
+        // Update the inbound transform
+        final IpSecTransformWrapper newTransform = mock(IpSecTransformWrapper.class);
+        mIpSecPacketLossDetector.setInboundTransformInternal(newTransform);
+
+        // Verifications
+        assertNull(mIpSecPacketLossDetector.getLastTransformState());
+        mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
+        mTestLooper.dispatchAll();
+        verify(newTransform).getIpSecTransformState(any(), any());
+    }
+
+    @Test
+    public void testStartedMonitor_updateCarrierConfig() throws Exception {
+        startMonitorAndCaptureStateReceiver();
+
+        final int additionalPollIntervalMs = (int) TimeUnit.SECONDS.toMillis(10L);
+        when(mCarrierConfig.getInt(
+                        eq(VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY), anyInt()))
+                .thenReturn(
+                        (int)
+                                TimeUnit.MILLISECONDS.toSeconds(
+                                        POLL_IPSEC_STATE_INTERVAL_MS + additionalPollIntervalMs));
+        mIpSecPacketLossDetector.setCarrierConfig(mCarrierConfig);
+        mTestLooper.dispatchAll();
+
+        // The already scheduled event is still fired with the old timeout
+        mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
+        mTestLooper.dispatchAll();
+
+        // The next scheduled event will take 10 more seconds to fire
+        mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
+        assertNull(mTestLooper.nextMessage());
+        mTestLooper.moveTimeForward(additionalPollIntervalMs);
+        assertNotNull(mTestLooper.nextMessage());
+    }
+
+    @Test
+    public void testStopMonitor() throws Exception {
+        mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(true /* setIsSelected */);
+        mIpSecPacketLossDetector.setInboundTransformInternal(mIpSecTransform);
+
+        assertTrue(mIpSecPacketLossDetector.isStarted());
+        assertNotNull(mTestLooper.nextMessage());
+
+        // Unselect the monitor
+        mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(false /* setIsSelected */);
+        verifyStopped();
+    }
+
+    @Test
+    public void testClose() throws Exception {
+        mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(true /* setIsSelected */);
+        mIpSecPacketLossDetector.setInboundTransformInternal(mIpSecTransform);
+
+        assertTrue(mIpSecPacketLossDetector.isStarted());
+        assertNotNull(mTestLooper.nextMessage());
+
+        // Stop the monitor
+        mIpSecPacketLossDetector.close();
+        verifyStopped();
+        verify(mIpSecTransform).close();
+    }
+
+    @Test
+    public void testTransformStateReceiverOnResultWhenStopped() throws Exception {
+        final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
+                startMonitorAndCaptureStateReceiver();
+        xfrmStateReceiver.onResult(mTransformStateInitial);
+
+        // Unselect the monitor
+        mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(false /* setIsSelected */);
+        verifyStopped();
+
+        xfrmStateReceiver.onResult(newTransformState(1, 1, newReplayBitmap(1)));
+        verify(mPacketLossCalculator, never())
+                .getPacketLossRatePercentage(any(), any(), anyString());
+    }
+
+    @Test
+    public void testTransformStateReceiverOnError() throws Exception {
+        final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
+                startMonitorAndCaptureStateReceiver();
+        xfrmStateReceiver.onResult(mTransformStateInitial);
+
+        xfrmStateReceiver.onError(new RuntimeException("Test"));
+        verify(mPacketLossCalculator, never())
+                .getPacketLossRatePercentage(any(), any(), anyString());
+    }
+
+    private void checkHandleLossRate(
+            int mockPacketLossRate, boolean isLastStateExpectedToUpdate, boolean isCallbackExpected)
+            throws Exception {
+        final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
+                startMonitorAndCaptureStateReceiver();
+        doReturn(mockPacketLossRate)
+                .when(mPacketLossCalculator)
+                .getPacketLossRatePercentage(any(), any(), anyString());
+
+        // Mock receiving two states with mTransformStateInitial and an arbitrary transformNew
+        final IpSecTransformState transformNew = newTransformState(1, 1, newReplayBitmap(1));
+        xfrmStateReceiver.onResult(mTransformStateInitial);
+        xfrmStateReceiver.onResult(transformNew);
+
+        // Verifications
+        verify(mPacketLossCalculator)
+                .getPacketLossRatePercentage(
+                        eq(mTransformStateInitial), eq(transformNew), anyString());
+
+        if (isLastStateExpectedToUpdate) {
+            assertEquals(transformNew, mIpSecPacketLossDetector.getLastTransformState());
+        } else {
+            assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
+        }
+
+        if (isCallbackExpected) {
+            verify(mMetricMonitorCallback).onValidationResultReceived();
+        } else {
+            verify(mMetricMonitorCallback, never()).onValidationResultReceived();
+        }
+    }
+
+    @Test
+    public void testHandleLossRate_validationPass() throws Exception {
+        checkHandleLossRate(
+                2, true /* isLastStateExpectedToUpdate */, true /* isCallbackExpected */);
+    }
+
+    @Test
+    public void testHandleLossRate_validationFail() throws Exception {
+        checkHandleLossRate(
+                22, true /* isLastStateExpectedToUpdate */, true /* isCallbackExpected */);
+    }
+
+    @Test
+    public void testHandleLossRate_resultUnavalaible() throws Exception {
+        checkHandleLossRate(
+                PACKET_LOSS_UNAVALAIBLE,
+                false /* isLastStateExpectedToUpdate */,
+                false /* isCallbackExpected */);
+    }
+
+    private void checkGetPacketLossRate(
+            IpSecTransformState oldState, IpSecTransformState newState, int expectedLossRate)
+            throws Exception {
+        assertEquals(
+                expectedLossRate,
+                mPacketLossCalculator.getPacketLossRatePercentage(oldState, newState, TAG));
+    }
+
+    private void checkGetPacketLossRate(
+            IpSecTransformState oldState,
+            int rxSeqNo,
+            int packetCount,
+            int packetInWin,
+            int expectedDataLossRate)
+            throws Exception {
+        final IpSecTransformState newState =
+                newTransformState(rxSeqNo, packetCount, newReplayBitmap(packetInWin));
+        checkGetPacketLossRate(oldState, newState, expectedDataLossRate);
+    }
+
+    @Test
+    public void testGetPacketLossRate_replayWindowUnchanged() throws Exception {
+        checkGetPacketLossRate(
+                mTransformStateInitial, mTransformStateInitial, PACKET_LOSS_UNAVALAIBLE);
+        checkGetPacketLossRate(mTransformStateInitial, 3000, 2000, 2000, PACKET_LOSS_UNAVALAIBLE);
+    }
+
+    @Test
+    public void testGetPacketLossRate_againstInitialState() throws Exception {
+        checkGetPacketLossRate(mTransformStateInitial, 7000, 7001, 4096, 0);
+        checkGetPacketLossRate(mTransformStateInitial, 7000, 6000, 4096, 15);
+        checkGetPacketLossRate(mTransformStateInitial, 7000, 6000, 4000, 14);
+    }
+
+    @Test
+    public void testGetPktLossRate_oldHiSeqSmallerThanWinSize_overlappedWithNewWin()
+            throws Exception {
+        final IpSecTransformState oldState = newTransformState(2000, 1500, newReplayBitmap(1500));
+
+        checkGetPacketLossRate(oldState, 5000, 5001, 4096, 0);
+        checkGetPacketLossRate(oldState, 5000, 4000, 4096, 29);
+        checkGetPacketLossRate(oldState, 5000, 4000, 4000, 27);
+    }
+
+    @Test
+    public void testGetPktLossRate_oldHiSeqSmallerThanWinSize_notOverlappedWithNewWin()
+            throws Exception {
+        final IpSecTransformState oldState = newTransformState(2000, 1500, newReplayBitmap(1500));
+
+        checkGetPacketLossRate(oldState, 7000, 7001, 4096, 0);
+        checkGetPacketLossRate(oldState, 7000, 5000, 4096, 37);
+        checkGetPacketLossRate(oldState, 7000, 5000, 3000, 21);
+    }
+
+    @Test
+    public void testGetPktLossRate_oldHiSeqLargerThanWinSize_overlappedWithNewWin()
+            throws Exception {
+        final IpSecTransformState oldState = newTransformState(10000, 5000, newReplayBitmap(3000));
+
+        checkGetPacketLossRate(oldState, 12000, 8096, 4096, 0);
+        checkGetPacketLossRate(oldState, 12000, 7000, 4096, 36);
+        checkGetPacketLossRate(oldState, 12000, 7000, 3000, 0);
+    }
+
+    @Test
+    public void testGetPktLossRate_oldHiSeqLargerThanWinSize_notOverlappedWithNewWin()
+            throws Exception {
+        final IpSecTransformState oldState = newTransformState(10000, 5000, newReplayBitmap(3000));
+
+        checkGetPacketLossRate(oldState, 20000, 16096, 4096, 0);
+        checkGetPacketLossRate(oldState, 20000, 14000, 4096, 19);
+        checkGetPacketLossRate(oldState, 20000, 14000, 3000, 10);
+    }
+}
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
new file mode 100644
index 0000000..355c221
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
@@ -0,0 +1,144 @@
+/*
+ * 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.vcn.routeselection;
+
+import static com.android.server.vcn.VcnTestUtils.setupSystemService;
+import static com.android.server.vcn.routeselection.UnderlyingNetworkControllerTest.getLinkPropertiesWithName;
+
+import static org.mockito.Mockito.doNothing;
+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.content.Context;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.FeatureFlags;
+import android.os.Handler;
+import android.os.IPowerManager;
+import android.os.IThermalService;
+import android.os.ParcelUuid;
+import android.os.PowerManager;
+import android.os.test.TestLooper;
+import android.telephony.TelephonyManager;
+
+import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
+import com.android.server.vcn.VcnContext;
+import com.android.server.vcn.VcnNetworkProvider;
+
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Set;
+import java.util.UUID;
+
+public abstract class NetworkEvaluationTestBase {
+    protected static final String SSID = "TestWifi";
+    protected static final String SSID_OTHER = "TestWifiOther";
+    protected static final String PLMN_ID = "123456";
+    protected static final String PLMN_ID_OTHER = "234567";
+
+    protected static final int SUB_ID = 1;
+    protected static final int WIFI_RSSI = -60;
+    protected static final int WIFI_RSSI_HIGH = -50;
+    protected static final int WIFI_RSSI_LOW = -80;
+    protected static final int CARRIER_ID = 1;
+    protected static final int CARRIER_ID_OTHER = 2;
+
+    protected static final int LINK_UPSTREAM_BANDWIDTH_KBPS = 1024;
+    protected static final int LINK_DOWNSTREAM_BANDWIDTH_KBPS = 2048;
+
+    protected static final int TEST_MIN_UPSTREAM_BANDWIDTH_KBPS = 100;
+    protected static final int TEST_MIN_DOWNSTREAM_BANDWIDTH_KBPS = 200;
+
+    protected static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0));
+
+    protected static final NetworkCapabilities WIFI_NETWORK_CAPABILITIES =
+            new NetworkCapabilities.Builder()
+                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                    .setSignalStrength(WIFI_RSSI)
+                    .setSsid(SSID)
+                    .setLinkUpstreamBandwidthKbps(LINK_UPSTREAM_BANDWIDTH_KBPS)
+                    .setLinkDownstreamBandwidthKbps(LINK_DOWNSTREAM_BANDWIDTH_KBPS)
+                    .build();
+
+    protected static final TelephonyNetworkSpecifier TEL_NETWORK_SPECIFIER =
+            new TelephonyNetworkSpecifier.Builder().setSubscriptionId(SUB_ID).build();
+    protected static final NetworkCapabilities CELL_NETWORK_CAPABILITIES =
+            new NetworkCapabilities.Builder()
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_DUN)
+                    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                    .setSubscriptionIds(Set.of(SUB_ID))
+                    .setNetworkSpecifier(TEL_NETWORK_SPECIFIER)
+                    .setLinkUpstreamBandwidthKbps(LINK_UPSTREAM_BANDWIDTH_KBPS)
+                    .setLinkDownstreamBandwidthKbps(LINK_DOWNSTREAM_BANDWIDTH_KBPS)
+                    .build();
+
+    protected static final LinkProperties LINK_PROPERTIES = getLinkPropertiesWithName("test_iface");
+
+    @Mock protected Context mContext;
+    @Mock protected Network mNetwork;
+    @Mock protected FeatureFlags mFeatureFlags;
+    @Mock protected com.android.net.flags.FeatureFlags mCoreNetFeatureFlags;
+    @Mock protected TelephonySubscriptionSnapshot mSubscriptionSnapshot;
+    @Mock protected TelephonyManager mTelephonyManager;
+    @Mock protected IPowerManager mPowerManagerService;
+
+    protected TestLooper mTestLooper;
+    protected VcnContext mVcnContext;
+    protected PowerManager mPowerManager;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mNetwork.getNetId()).thenReturn(-1);
+
+        mTestLooper = new TestLooper();
+        mVcnContext =
+                spy(
+                        new VcnContext(
+                                mContext,
+                                mTestLooper.getLooper(),
+                                mock(VcnNetworkProvider.class),
+                                false /* isInTestMode */));
+        doNothing().when(mVcnContext).ensureRunningOnLooperThread();
+
+        doReturn(true).when(mVcnContext).isFlagNetworkMetricMonitorEnabled();
+        doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled();
+
+        setupSystemService(
+                mContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class);
+        when(mTelephonyManager.createForSubscriptionId(SUB_ID)).thenReturn(mTelephonyManager);
+        when(mTelephonyManager.getNetworkOperator()).thenReturn(PLMN_ID);
+        when(mTelephonyManager.getSimSpecificCarrierId()).thenReturn(CARRIER_ID);
+
+        mPowerManager =
+                new PowerManager(
+                        mContext,
+                        mPowerManagerService,
+                        mock(IThermalService.class),
+                        mock(Handler.class));
+        setupSystemService(mContext, mPowerManager, Context.POWER_SERVICE, PowerManager.class);
+    }
+}
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
index 2266041..d85c515 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
@@ -24,152 +24,48 @@
 import static android.net.vcn.VcnUnderlyingNetworkTemplateTestBase.TEST_MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS;
 import static android.net.vcn.VcnUnderlyingNetworkTemplateTestBase.TEST_MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS;
 
-import static com.android.server.vcn.VcnTestUtils.setupSystemService;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_FALLBACK;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_INVALID;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesCellPriorityRule;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesPriorityRule;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesWifiPriorityRule;
-import static com.android.server.vcn.routeselection.UnderlyingNetworkControllerTest.getLinkPropertiesWithName;
 import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
-import android.content.Context;
-import android.net.LinkProperties;
-import android.net.Network;
 import android.net.NetworkCapabilities;
-import android.net.TelephonyNetworkSpecifier;
 import android.net.vcn.VcnCellUnderlyingNetworkTemplate;
 import android.net.vcn.VcnGatewayConnectionConfig;
 import android.net.vcn.VcnManager;
 import android.net.vcn.VcnUnderlyingNetworkTemplate;
 import android.net.vcn.VcnWifiUnderlyingNetworkTemplate;
-import android.os.ParcelUuid;
 import android.os.PersistableBundle;
-import android.os.test.TestLooper;
-import android.telephony.TelephonyManager;
 import android.util.ArraySet;
 
-import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
-import com.android.server.vcn.VcnContext;
-import com.android.server.vcn.VcnNetworkProvider;
-
 import org.junit.Before;
 import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
 
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
-import java.util.UUID;
 
-public class NetworkPriorityClassifierTest {
-    private static final String SSID = "TestWifi";
-    private static final String SSID_OTHER = "TestWifiOther";
-    private static final String PLMN_ID = "123456";
-    private static final String PLMN_ID_OTHER = "234567";
-
-    private static final int SUB_ID = 1;
-    private static final int WIFI_RSSI = -60;
-    private static final int WIFI_RSSI_HIGH = -50;
-    private static final int WIFI_RSSI_LOW = -80;
-    private static final int CARRIER_ID = 1;
-    private static final int CARRIER_ID_OTHER = 2;
-
-    private static final int LINK_UPSTREAM_BANDWIDTH_KBPS = 1024;
-    private static final int LINK_DOWNSTREAM_BANDWIDTH_KBPS = 2048;
-
-    private static final int TEST_MIN_UPSTREAM_BANDWIDTH_KBPS = 100;
-    private static final int TEST_MIN_DOWNSTREAM_BANDWIDTH_KBPS = 200;
-
-    private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0));
-
-    private static final NetworkCapabilities WIFI_NETWORK_CAPABILITIES =
-            new NetworkCapabilities.Builder()
-                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
-                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
-                    .setSignalStrength(WIFI_RSSI)
-                    .setSsid(SSID)
-                    .setLinkUpstreamBandwidthKbps(LINK_UPSTREAM_BANDWIDTH_KBPS)
-                    .setLinkDownstreamBandwidthKbps(LINK_DOWNSTREAM_BANDWIDTH_KBPS)
-                    .build();
-
-    private static final TelephonyNetworkSpecifier TEL_NETWORK_SPECIFIER =
-            new TelephonyNetworkSpecifier.Builder().setSubscriptionId(SUB_ID).build();
-    private static final NetworkCapabilities CELL_NETWORK_CAPABILITIES =
-            new NetworkCapabilities.Builder()
-                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
-                    .addCapability(NetworkCapabilities.NET_CAPABILITY_DUN)
-                    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
-                    .setSubscriptionIds(Set.of(SUB_ID))
-                    .setNetworkSpecifier(TEL_NETWORK_SPECIFIER)
-                    .setLinkUpstreamBandwidthKbps(LINK_UPSTREAM_BANDWIDTH_KBPS)
-                    .setLinkDownstreamBandwidthKbps(LINK_DOWNSTREAM_BANDWIDTH_KBPS)
-                    .build();
-
-    private static final LinkProperties LINK_PROPERTIES = getLinkPropertiesWithName("test_iface");
-
-    @Mock private Network mNetwork;
-    @Mock private TelephonySubscriptionSnapshot mSubscriptionSnapshot;
-    @Mock private TelephonyManager mTelephonyManager;
-
-    private TestLooper mTestLooper;
-    private VcnContext mVcnContext;
+public class NetworkPriorityClassifierTest extends NetworkEvaluationTestBase {
     private UnderlyingNetworkRecord mWifiNetworkRecord;
     private UnderlyingNetworkRecord mCellNetworkRecord;
 
     @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
+    public void setUp() throws Exception {
+        super.setUp();
 
-        final Context mockContext = mock(Context.class);
-        mTestLooper = new TestLooper();
-        mVcnContext =
-                spy(
-                        new VcnContext(
-                                mockContext,
-                                mTestLooper.getLooper(),
-                                mock(VcnNetworkProvider.class),
-                                false /* isInTestMode */));
-        doNothing().when(mVcnContext).ensureRunningOnLooperThread();
-
-        setupSystemService(
-                mockContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class);
-        when(mTelephonyManager.createForSubscriptionId(SUB_ID)).thenReturn(mTelephonyManager);
-        when(mTelephonyManager.getNetworkOperator()).thenReturn(PLMN_ID);
-        when(mTelephonyManager.getSimSpecificCarrierId()).thenReturn(CARRIER_ID);
-
-        mWifiNetworkRecord =
-                getTestNetworkRecord(
-                        WIFI_NETWORK_CAPABILITIES,
-                        VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES);
-        mCellNetworkRecord =
-                getTestNetworkRecord(
-                        CELL_NETWORK_CAPABILITIES,
-                        VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES);
+        mWifiNetworkRecord = getTestNetworkRecord(WIFI_NETWORK_CAPABILITIES);
+        mCellNetworkRecord = getTestNetworkRecord(CELL_NETWORK_CAPABILITIES);
     }
 
-    private UnderlyingNetworkRecord getTestNetworkRecord(
-            NetworkCapabilities nc, List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates) {
-        return new UnderlyingNetworkRecord(
-                mNetwork,
-                nc,
-                LINK_PROPERTIES,
-                false /* isBlocked */,
-                mVcnContext,
-                underlyingNetworkTemplates,
-                SUB_GROUP,
-                mSubscriptionSnapshot,
-                null /* currentlySelected */,
-                null /* carrierConfig */);
+    private UnderlyingNetworkRecord getTestNetworkRecord(NetworkCapabilities nc) {
+        return new UnderlyingNetworkRecord(mNetwork, nc, LINK_PROPERTIES, false /* isBlocked */);
     }
 
     @Test
@@ -186,14 +82,14 @@
                         mWifiNetworkRecord,
                         SUB_GROUP,
                         mSubscriptionSnapshot,
-                        null /* currentlySelecetd */,
+                        false /* isSelected */,
                         null /* carrierConfig */));
     }
 
     private void verifyMatchesPriorityRuleForUpstreamBandwidth(
             int entryUpstreamBandwidth,
             int exitUpstreamBandwidth,
-            UnderlyingNetworkRecord currentlySelected,
+            boolean isSelected,
             boolean expectMatch) {
         final VcnWifiUnderlyingNetworkTemplate wifiNetworkPriority =
                 new VcnWifiUnderlyingNetworkTemplate.Builder()
@@ -208,14 +104,14 @@
                         mWifiNetworkRecord,
                         SUB_GROUP,
                         mSubscriptionSnapshot,
-                        currentlySelected,
+                        isSelected,
                         null /* carrierConfig */));
     }
 
     private void verifyMatchesPriorityRuleForDownstreamBandwidth(
             int entryDownstreamBandwidth,
             int exitDownstreamBandwidth,
-            UnderlyingNetworkRecord currentlySelected,
+            boolean isSelected,
             boolean expectMatch) {
         final VcnWifiUnderlyingNetworkTemplate wifiNetworkPriority =
                 new VcnWifiUnderlyingNetworkTemplate.Builder()
@@ -231,7 +127,7 @@
                         mWifiNetworkRecord,
                         SUB_GROUP,
                         mSubscriptionSnapshot,
-                        currentlySelected,
+                        isSelected,
                         null /* carrierConfig */));
     }
 
@@ -240,7 +136,7 @@
         verifyMatchesPriorityRuleForUpstreamBandwidth(
                 TEST_MIN_ENTRY_UPSTREAM_BANDWIDTH_KBPS,
                 TEST_MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS,
-                null /* currentlySelected */,
+                false /* isSelected */,
                 true);
     }
 
@@ -249,7 +145,7 @@
         verifyMatchesPriorityRuleForUpstreamBandwidth(
                 LINK_UPSTREAM_BANDWIDTH_KBPS + 1,
                 LINK_UPSTREAM_BANDWIDTH_KBPS + 1,
-                null /* currentlySelected */,
+                false /* isSelected */,
                 false);
     }
 
@@ -258,7 +154,7 @@
         verifyMatchesPriorityRuleForDownstreamBandwidth(
                 TEST_MIN_ENTRY_DOWNSTREAM_BANDWIDTH_KBPS,
                 TEST_MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS,
-                null /* currentlySelected */,
+                false /* isSelected */,
                 true);
     }
 
@@ -267,7 +163,7 @@
         verifyMatchesPriorityRuleForDownstreamBandwidth(
                 LINK_DOWNSTREAM_BANDWIDTH_KBPS + 1,
                 LINK_DOWNSTREAM_BANDWIDTH_KBPS + 1,
-                null /* currentlySelected */,
+                false /* isSelected */,
                 false);
     }
 
@@ -276,7 +172,7 @@
         verifyMatchesPriorityRuleForUpstreamBandwidth(
                 TEST_MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS,
                 TEST_MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS,
-                mWifiNetworkRecord,
+                true /* isSelected */,
                 true);
     }
 
@@ -285,7 +181,7 @@
         verifyMatchesPriorityRuleForUpstreamBandwidth(
                 LINK_UPSTREAM_BANDWIDTH_KBPS + 1,
                 LINK_UPSTREAM_BANDWIDTH_KBPS + 1,
-                mWifiNetworkRecord,
+                true /* isSelected */,
                 false);
     }
 
@@ -294,7 +190,7 @@
         verifyMatchesPriorityRuleForDownstreamBandwidth(
                 TEST_MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS,
                 TEST_MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS,
-                mWifiNetworkRecord,
+                true /* isSelected */,
                 true);
     }
 
@@ -303,7 +199,7 @@
         verifyMatchesPriorityRuleForDownstreamBandwidth(
                 LINK_DOWNSTREAM_BANDWIDTH_KBPS + 1,
                 LINK_DOWNSTREAM_BANDWIDTH_KBPS + 1,
-                mWifiNetworkRecord,
+                true /* isSelected */,
                 false);
     }
 
@@ -318,14 +214,12 @@
                                 TEST_MIN_ENTRY_DOWNSTREAM_BANDWIDTH_KBPS,
                                 TEST_MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS)
                         .build();
-        final UnderlyingNetworkRecord selectedNetworkRecord =
-                isSelectedNetwork ? mWifiNetworkRecord : null;
         assertEquals(
                 expectMatch,
                 checkMatchesWifiPriorityRule(
                         wifiNetworkPriority,
                         mWifiNetworkRecord,
-                        selectedNetworkRecord,
+                        isSelectedNetwork,
                         carrierConfig == null
                                 ? null
                                 : new PersistableBundleWrapper(carrierConfig)));
@@ -381,7 +275,7 @@
                 checkMatchesWifiPriorityRule(
                         wifiNetworkPriority,
                         mWifiNetworkRecord,
-                        null /* currentlySelecetd */,
+                        false /* isSelected */,
                         null /* carrierConfig */));
     }
 
@@ -516,7 +410,7 @@
                         mCellNetworkRecord,
                         SUB_GROUP,
                         mSubscriptionSnapshot,
-                        null /* currentlySelected */,
+                        false /* isSelected */,
                         null /* carrierConfig */));
     }
 
@@ -543,7 +437,16 @@
 
     @Test
     public void testCalculatePriorityClass() throws Exception {
-        assertEquals(2, mCellNetworkRecord.priorityClass);
+        final int priorityClass =
+                NetworkPriorityClassifier.calculatePriorityClass(
+                        mVcnContext,
+                        mCellNetworkRecord,
+                        VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot,
+                        false /* isSelected */,
+                        null /* carrierConfig */);
+        assertEquals(2, priorityClass);
     }
 
     private void checkCalculatePriorityClassFailToMatchAny(
@@ -561,10 +464,19 @@
             ncBuilder.addCapability(NET_CAPABILITY_INTERNET);
         }
 
-        final UnderlyingNetworkRecord nonDunNetworkRecord =
-                getTestNetworkRecord(ncBuilder.build(), templatesRequireDun);
+        final UnderlyingNetworkRecord nonDunNetworkRecord = getTestNetworkRecord(ncBuilder.build());
 
-        assertEquals(expectedPriorityClass, nonDunNetworkRecord.priorityClass);
+        final int priorityClass =
+                NetworkPriorityClassifier.calculatePriorityClass(
+                        mVcnContext,
+                        nonDunNetworkRecord,
+                        templatesRequireDun,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot,
+                        false /* isSelected */,
+                        null /* carrierConfig */);
+
+        assertEquals(expectedPriorityClass, priorityClass);
     }
 
     @Test
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
index 2941fde..992f102 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
@@ -29,13 +29,12 @@
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.WIFI_EXIT_RSSI_THRESHOLD_DEFAULT;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -67,6 +66,7 @@
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.VcnContext;
 import com.android.server.vcn.VcnNetworkProvider;
+import com.android.server.vcn.routeselection.UnderlyingNetworkController.Dependencies;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.NetworkBringupCallback;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkControllerCallback;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkListener;
@@ -77,6 +77,7 @@
 import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -154,10 +155,13 @@
     @Mock private UnderlyingNetworkControllerCallback mNetworkControllerCb;
     @Mock private Network mNetwork;
 
+    @Spy private Dependencies mDependencies = new Dependencies();
+
     @Captor private ArgumentCaptor<UnderlyingNetworkListener> mUnderlyingNetworkListenerCaptor;
 
     private TestLooper mTestLooper;
     private VcnContext mVcnContext;
+    private UnderlyingNetworkEvaluator mNetworkEvaluator;
     private UnderlyingNetworkController mUnderlyingNetworkController;
 
     @Before
@@ -189,13 +193,28 @@
 
         when(mSubscriptionSnapshot.getAllSubIdsInGroup(eq(SUB_GROUP))).thenReturn(INITIAL_SUB_IDS);
 
+        mNetworkEvaluator =
+                spy(
+                        new UnderlyingNetworkEvaluator(
+                                mVcnContext,
+                                mNetwork,
+                                VcnGatewayConnectionConfigTest.buildTestConfig()
+                                        .getVcnUnderlyingNetworkPriorities(),
+                                SUB_GROUP,
+                                mSubscriptionSnapshot,
+                                null));
+        doReturn(mNetworkEvaluator)
+                .when(mDependencies)
+                .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any());
+
         mUnderlyingNetworkController =
                 new UnderlyingNetworkController(
                         mVcnContext,
                         VcnGatewayConnectionConfigTest.buildTestConfig(),
                         SUB_GROUP,
                         mSubscriptionSnapshot,
-                        mNetworkControllerCb);
+                        mNetworkControllerCb,
+                        mDependencies);
     }
 
     private void resetVcnContext() {
@@ -489,13 +508,7 @@
             NetworkCapabilities networkCapabilities,
             LinkProperties linkProperties,
             boolean isBlocked) {
-        return new UnderlyingNetworkRecord(
-                network,
-                networkCapabilities,
-                linkProperties,
-                isBlocked,
-                false /* isSelected */,
-                0 /* priorityClass */);
+        return new UnderlyingNetworkRecord(network, networkCapabilities, linkProperties, isBlocked);
     }
 
     @Test
@@ -515,24 +528,12 @@
         UnderlyingNetworkRecord recordC =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        INITIAL_NETWORK_CAPABILITIES,
-                        INITIAL_LINK_PROPERTIES,
-                        false /* isBlocked */,
-                        true /* isSelected */,
-                        -1 /* priorityClass */);
-        UnderlyingNetworkRecord recordD =
-                getTestNetworkRecord(
-                        mNetwork,
                         UPDATED_NETWORK_CAPABILITIES,
                         UPDATED_LINK_PROPERTIES,
                         false /* isBlocked */);
 
         assertEquals(recordA, recordB);
-        assertEquals(recordA, recordC);
-        assertNotEquals(recordA, recordD);
-
-        assertTrue(UnderlyingNetworkRecord.isEqualIncludingPriorities(recordA, recordB));
-        assertFalse(UnderlyingNetworkRecord.isEqualIncludingPriorities(recordA, recordC));
+        assertNotEquals(recordA, recordC);
     }
 
     @Test
@@ -540,6 +541,19 @@
         verifyRegistrationOnAvailableAndGetCallback();
     }
 
+    @Test
+    public void testUpdateSubscriptionSnapshotAndCarrierConfig() {
+        verifyRegistrationOnAvailableAndGetCallback();
+
+        TelephonySubscriptionSnapshot subscriptionUpdate =
+                mock(TelephonySubscriptionSnapshot.class);
+        when(subscriptionUpdate.getAllSubIdsInGroup(eq(SUB_GROUP))).thenReturn(UPDATED_SUB_IDS);
+
+        mUnderlyingNetworkController.updateSubscriptionSnapshot(subscriptionUpdate);
+
+        verify(mNetworkEvaluator).reevaluate(any(), any(), any(), any());
+    }
+
     private UnderlyingNetworkListener verifyRegistrationOnAvailableAndGetCallback() {
         return verifyRegistrationOnAvailableAndGetCallback(INITIAL_NETWORK_CAPABILITIES);
     }
@@ -583,6 +597,7 @@
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
         verifyOnSelectedUnderlyingNetworkChanged(expectedRecord);
+        verify(mNetworkEvaluator).setIsSelected(eq(true), any(), any(), any(), any());
         return cb;
     }
 
@@ -713,7 +728,8 @@
                 VcnGatewayConnectionConfigTest.buildTestConfig(networkTemplates),
                 SUB_GROUP,
                 mSubscriptionSnapshot,
-                mNetworkControllerCb);
+                mNetworkControllerCb,
+                mDependencies);
 
         verify(cm)
                 .registerNetworkCallback(
@@ -724,30 +740,43 @@
         return mUnderlyingNetworkListenerCaptor.getValue();
     }
 
-    private UnderlyingNetworkRecord bringupNetworkAndGetRecord(
+    private UnderlyingNetworkEvaluator bringupNetworkAndGetEvaluator(
             UnderlyingNetworkListener cb,
             NetworkCapabilities requestNetworkCaps,
-            List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
-            UnderlyingNetworkRecord currentlySelected) {
+            List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates) {
         final Network network = mock(Network.class);
         final NetworkCapabilities responseNetworkCaps =
                 buildResponseNwCaps(requestNetworkCaps, INITIAL_SUB_IDS);
+        final UnderlyingNetworkEvaluator evaluator =
+                spy(
+                        new UnderlyingNetworkEvaluator(
+                                mVcnContext,
+                                network,
+                                underlyingNetworkTemplates,
+                                SUB_GROUP,
+                                mSubscriptionSnapshot,
+                                null));
+        doReturn(evaluator)
+                .when(mDependencies)
+                .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any());
 
         cb.onAvailable(network);
         cb.onCapabilitiesChanged(network, responseNetworkCaps);
         cb.onLinkPropertiesChanged(network, INITIAL_LINK_PROPERTIES);
         cb.onBlockedStatusChanged(network, false /* isFalse */);
-        return new UnderlyingNetworkRecord(
-                network,
-                responseNetworkCaps,
-                INITIAL_LINK_PROPERTIES,
-                false /* isBlocked */,
-                mVcnContext,
-                underlyingNetworkTemplates,
-                SUB_GROUP,
-                mSubscriptionSnapshot,
-                currentlySelected,
-                null /* carrierConfig */);
+
+        return evaluator;
+    }
+
+    private void verifySelectNetwork(UnderlyingNetworkEvaluator expectedEvaluator) {
+        verifyOnSelectedUnderlyingNetworkChanged(expectedEvaluator.getNetworkRecord());
+        verify(expectedEvaluator).setIsSelected(eq(true), any(), any(), any(), any());
+    }
+
+    private void verifyNeverSelectNetwork(UnderlyingNetworkEvaluator expectedEvaluator) {
+        verify(mNetworkControllerCb, never())
+                .onSelectedUnderlyingNetworkChanged(eq(expectedEvaluator.getNetworkRecord()));
+        verify(expectedEvaluator, never()).setIsSelected(eq(true), any(), any(), any(), any());
     }
 
     @Test
@@ -759,19 +788,15 @@
         UnderlyingNetworkListener cb = setupControllerAndGetNetworkListener(networkTemplates);
 
         // Bring up CBS network
-        final UnderlyingNetworkRecord cbsNetworkRecord =
-                bringupNetworkAndGetRecord(
-                        cb,
-                        CBS_NETWORK_CAPABILITIES,
-                        networkTemplates,
-                        null /* currentlySelected */);
-        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(cbsNetworkRecord));
+        final UnderlyingNetworkEvaluator cbsNetworkEvaluator =
+                bringupNetworkAndGetEvaluator(cb, CBS_NETWORK_CAPABILITIES, networkTemplates);
+        verifySelectNetwork(cbsNetworkEvaluator);
 
         // Bring up DUN network
-        final UnderlyingNetworkRecord dunNetworkRecord =
-                bringupNetworkAndGetRecord(
-                        cb, DUN_NETWORK_CAPABILITIES, networkTemplates, cbsNetworkRecord);
-        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(dunNetworkRecord));
+        final UnderlyingNetworkEvaluator dunNetworkEvaluator =
+                bringupNetworkAndGetEvaluator(cb, DUN_NETWORK_CAPABILITIES, networkTemplates);
+        verifySelectNetwork(dunNetworkEvaluator);
+        verify(cbsNetworkEvaluator).setIsSelected(eq(false), any(), any(), any(), any());
     }
 
     @Test
@@ -783,20 +808,14 @@
         UnderlyingNetworkListener cb = setupControllerAndGetNetworkListener(networkTemplates);
 
         // Bring up DUN network
-        final UnderlyingNetworkRecord dunNetworkRecord =
-                bringupNetworkAndGetRecord(
-                        cb,
-                        DUN_NETWORK_CAPABILITIES,
-                        networkTemplates,
-                        null /* currentlySelected */);
-        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(dunNetworkRecord));
+        final UnderlyingNetworkEvaluator dunNetworkEvaluator =
+                bringupNetworkAndGetEvaluator(cb, DUN_NETWORK_CAPABILITIES, networkTemplates);
+        verifySelectNetwork(dunNetworkEvaluator);
 
         // Bring up CBS network
-        final UnderlyingNetworkRecord cbsNetworkRecord =
-                bringupNetworkAndGetRecord(
-                        cb, CBS_NETWORK_CAPABILITIES, networkTemplates, dunNetworkRecord);
-        verify(mNetworkControllerCb, never())
-                .onSelectedUnderlyingNetworkChanged(eq(cbsNetworkRecord));
+        final UnderlyingNetworkEvaluator cbsNetworkEvaluator =
+                bringupNetworkAndGetEvaluator(cb, CBS_NETWORK_CAPABILITIES, networkTemplates);
+        verifyNeverSelectNetwork(cbsNetworkEvaluator);
     }
 
     @Test
@@ -808,13 +827,9 @@
         UnderlyingNetworkListener cb = setupControllerAndGetNetworkListener(networkTemplates);
 
         // Bring up an Internet network without DUN capability
-        final UnderlyingNetworkRecord networkRecord =
-                bringupNetworkAndGetRecord(
-                        cb,
-                        INITIAL_NETWORK_CAPABILITIES,
-                        networkTemplates,
-                        null /* currentlySelected */);
-        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(networkRecord));
+        final UnderlyingNetworkEvaluator evaluator =
+                bringupNetworkAndGetEvaluator(cb, INITIAL_NETWORK_CAPABILITIES, networkTemplates);
+        verifySelectNetwork(evaluator);
     }
 
     @Test
@@ -825,10 +840,8 @@
                 new VcnCellUnderlyingNetworkTemplate.Builder().setDun(MATCH_REQUIRED).build());
         UnderlyingNetworkListener cb = setupControllerAndGetNetworkListener(networkTemplates);
 
-        bringupNetworkAndGetRecord(
-                cb, CBS_NETWORK_CAPABILITIES, networkTemplates, null /* currentlySelected */);
-
-        verify(mNetworkControllerCb, never())
-                .onSelectedUnderlyingNetworkChanged(any(UnderlyingNetworkRecord.class));
+        final UnderlyingNetworkEvaluator evaluator =
+                bringupNetworkAndGetEvaluator(cb, CBS_NETWORK_CAPABILITIES, networkTemplates);
+        verifyNeverSelectNetwork(evaluator);
     }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
new file mode 100644
index 0000000..985e70c
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.vcn.routeselection;
+
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_INVALID;
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.vcn.VcnGatewayConnectionConfig;
+import android.os.PersistableBundle;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase {
+    private PersistableBundleWrapper mCarrierConfig;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mCarrierConfig = new PersistableBundleWrapper(new PersistableBundle());
+    }
+
+    private UnderlyingNetworkEvaluator newUnderlyingNetworkEvaluator() {
+        return new UnderlyingNetworkEvaluator(
+                mVcnContext,
+                mNetwork,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+    }
+
+    @Test
+    public void testInitializedEvaluator() throws Exception {
+        final UnderlyingNetworkEvaluator evaluator = newUnderlyingNetworkEvaluator();
+
+        assertFalse(evaluator.isValid());
+        assertEquals(mNetwork, evaluator.getNetwork());
+        assertEquals(PRIORITY_INVALID, evaluator.getPriorityClass());
+
+        try {
+            evaluator.getNetworkRecord();
+            fail("Expected to fail because evaluator is not valid");
+        } catch (Exception expected) {
+        }
+    }
+
+    @Test
+    public void testValidEvaluator() {
+        final UnderlyingNetworkEvaluator evaluator = newUnderlyingNetworkEvaluator();
+        evaluator.setNetworkCapabilities(
+                CELL_NETWORK_CAPABILITIES,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        evaluator.setLinkProperties(
+                LINK_PROPERTIES,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        evaluator.setIsBlocked(
+                false /* isBlocked */,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+
+        final UnderlyingNetworkRecord expectedRecord =
+                new UnderlyingNetworkRecord(
+                        mNetwork,
+                        CELL_NETWORK_CAPABILITIES,
+                        LINK_PROPERTIES,
+                        false /* isBlocked */);
+
+        assertTrue(evaluator.isValid());
+        assertEquals(mNetwork, evaluator.getNetwork());
+        assertEquals(2, evaluator.getPriorityClass());
+        assertEquals(expectedRecord, evaluator.getNetworkRecord());
+    }
+}