Merge "Do not change strong auth for users stopped with delayed locking" 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 d1ec839..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";
@@ -33400,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
   }
 
@@ -33995,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[]);
@@ -47294,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();
@@ -56499,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 9a5652a..a532cdb 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -862,10 +862,6 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.app.AppOpsManager.PackageOps> CREATOR;
   }
 
-  @FlaggedApi("android.app.bic_client") public final class BackgroundInstallControlManager {
-    method @FlaggedApi("android.app.bic_client") @NonNull @RequiresPermission(android.Manifest.permission.QUERY_ALL_PACKAGES) public java.util.List<android.content.pm.PackageInfo> getBackgroundInstalledPackages(long);
-  }
-
   public class BroadcastOptions {
     method public void clearRequireCompatChange();
     method public int getPendingIntentBackgroundActivityStartMode();
@@ -15136,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);
@@ -17041,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;
   }
@@ -17081,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>);
@@ -17102,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
@@ -17172,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/BackgroundInstallControlManager.java b/core/java/android/app/BackgroundInstallControlManager.java
deleted file mode 100644
index f5b6878..0000000
--- a/core/java/android/app/BackgroundInstallControlManager.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app;
-
-import static android.Manifest.permission.QUERY_ALL_PACKAGES;
-import static android.annotation.SystemApi.Client.PRIVILEGED_APPS;
-
-import android.annotation.FlaggedApi;
-import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
-import android.annotation.SystemApi;
-import android.annotation.SystemService;
-import android.content.Context;
-import android.content.pm.IBackgroundInstallControlService;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.os.ServiceManager;
-
-import java.util.List;
-
-/**
- * BackgroundInstallControlManager client allows apps to query apps installed in background.
- *
- * <p>Any applications that was installed without an accompanying installer UI activity paired
- * with recorded user interaction event is considered background installed. This is determined by
- * analysis of user-activity logs.
- *
- * <p>Warning: BackgroundInstallControl should not be considered a reliable or accurate
- * determination of background install application. Consumers can use this as a supplementary
- * signal, but must perform additional due diligence to confirm the install nature of the package.
- *
- * @hide
- */
-@FlaggedApi(Flags.FLAG_BIC_CLIENT)
-@SystemApi(client = PRIVILEGED_APPS)
-@SystemService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE)
-public final class BackgroundInstallControlManager {
-
-    private static final String TAG = "BackgroundInstallControlManager";
-    private static IBackgroundInstallControlService sService;
-    private final Context mContext;
-
-    BackgroundInstallControlManager(Context context) {
-        mContext = context;
-    }
-
-    private static IBackgroundInstallControlService getService() {
-        if (sService == null) {
-            sService =
-                    IBackgroundInstallControlService.Stub.asInterface(
-                            ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE));
-        }
-        return sService;
-    }
-
-    /**
-     * Returns a full list of {@link PackageInfo} of apps currently installed that are considered
-     * installed in the background.
-     *
-     * <p>Refer to top level doc {@link BackgroundInstallControlManager} for more details on
-     * background-installed applications.
-     * <p>
-     *
-     * @param flags - Flags will be used to call
-     * {@link PackageManager#getInstalledPackages(PackageInfoFlags)} to retrieve installed packages.
-     * @return A list of packages retrieved from {@link PackageManager} with non-background
-     * installed app filter applied.
-     *
-     * @hide
-     */
-    @FlaggedApi(Flags.FLAG_BIC_CLIENT)
-    @SystemApi
-    @RequiresPermission(QUERY_ALL_PACKAGES)
-    public @NonNull List<PackageInfo> getBackgroundInstalledPackages(
-            @PackageManager.PackageInfoFlagsBits long flags) {
-        try {
-            return getService()
-                    .getBackgroundInstalledPackages(flags, mContext.getUserId())
-                    .getList();
-        } catch (SecurityException e) {
-            throw e;
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-}
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 390fa22..9cf732a 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -1603,20 +1603,6 @@
                     }
                 });
 
-        // DO NOT do a flag check like this unless the flag is read-only.
-        // (because this code is executed during preload in zygote.)
-        // If the flag is mutable, the check should be inside CachedServiceFetcher.
-        if (Flags.bicClient()) {
-            registerService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE,
-                    BackgroundInstallControlManager.class,
-                    new CachedServiceFetcher<BackgroundInstallControlManager>() {
-                        @Override
-                        public BackgroundInstallControlManager createService(ContextImpl ctx) {
-                            return new BackgroundInstallControlManager(ctx);
-                        }
-                    });
-        }
-
         sInitializing = true;
         try {
             // Note: the following functions need to be @SystemApis, once they become mainline
diff --git a/core/java/android/app/background_install_control_manager.aconfig b/core/java/android/app/background_install_control_manager.aconfig
deleted file mode 100644
index 029b93a..0000000
--- a/core/java/android/app/background_install_control_manager.aconfig
+++ /dev/null
@@ -1,9 +0,0 @@
-package: "android.app"
-
-flag {
-     namespace: "background_install_control"
-     name: "bic_client"
-     description: "System API for background install control."
-     is_fixed_read_only: true
-     bug: "287507984"
-}
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/IBackgroundInstallControlService.aidl b/core/java/android/content/pm/IBackgroundInstallControlService.aidl
index 4bc8fe1..c8e7cae 100644
--- a/core/java/android/content/pm/IBackgroundInstallControlService.aidl
+++ b/core/java/android/content/pm/IBackgroundInstallControlService.aidl
@@ -16,20 +16,11 @@
 
 package android.content.pm;
 
-
 import android.content.pm.ParceledListSlice;
-import android.os.IRemoteCallback;
 
 /**
  * {@hide}
  */
 interface IBackgroundInstallControlService {
-    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest.permission.QUERY_ALL_PACKAGES)")
     ParceledListSlice getBackgroundInstalledPackages(long flags, int userId);
-
-    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(allOf = {android.Manifest.permission.QUERY_ALL_PACKAGES, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})")
-    void registerBackgroundInstallCallback(IRemoteCallback callback);
-
-    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(allOf = {android.Manifest.permission.QUERY_ALL_PACKAGES, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})")
-    void unregisterBackgroundInstallCallback(IRemoteCallback callback);
-}
\ No newline at end of file
+}
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/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/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/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/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/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/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/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/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/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/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 921be05..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] -->
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/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/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/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/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/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/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/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/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/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/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/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/BackgroundInstallControlCallbackHelper.java b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java
deleted file mode 100644
index 4454601..0000000
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java
+++ /dev/null
@@ -1,97 +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.server.pm;
-
-import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
-
-import android.annotation.NonNull;
-import android.app.BackgroundInstallControlManager;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IRemoteCallback;
-import android.os.RemoteCallbackList;
-import android.os.RemoteException;
-import android.util.Slog;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.ServiceThread;
-
-public class BackgroundInstallControlCallbackHelper {
-
-    @VisibleForTesting static final String FLAGGED_PACKAGE_NAME_KEY = "packageName";
-    @VisibleForTesting static final String FLAGGED_USER_ID_KEY = "userId";
-    private static final String TAG = "BackgroundInstallControlCallbackHelper";
-
-    private final Handler mHandler;
-
-    BackgroundInstallControlCallbackHelper() {
-        HandlerThread backgroundThread =
-                new ServiceThread(
-                        "BackgroundInstallControlCallbackHelperBg",
-                        THREAD_PRIORITY_BACKGROUND,
-                        true);
-        backgroundThread.start();
-        mHandler = new Handler(backgroundThread.getLooper());
-    }
-
-    @NonNull @VisibleForTesting
-    final RemoteCallbackList<IRemoteCallback> mCallbacks = new RemoteCallbackList<>();
-
-    /** Registers callback that gets invoked upon detection of an MBA
-     *
-     * NOTE: The callback is user context agnostic and currently broadcasts to all users of other
-     * users app installs. This is fine because the API is for SystemServer use only.
-     */
-    public void registerBackgroundInstallCallback(IRemoteCallback callback) {
-        synchronized (mCallbacks) {
-            mCallbacks.register(callback, null);
-        }
-    }
-
-    /** Unregisters callback */
-    public void unregisterBackgroundInstallCallback(IRemoteCallback callback) {
-        synchronized (mCallbacks) {
-            mCallbacks.unregister(callback);
-        }
-    }
-
-    /**
-     * Invokes all registered callbacks Callbacks are processed through user provided-threads and
-     * parameters are passed in via {@link BackgroundInstallControlManager} InstallEvent
-     */
-    public void notifyAllCallbacks(int userId, String packageName) {
-        Bundle extras = new Bundle();
-        extras.putCharSequence(FLAGGED_PACKAGE_NAME_KEY, packageName);
-        extras.putInt(FLAGGED_USER_ID_KEY, userId);
-        synchronized (mCallbacks) {
-            mHandler.post(
-                    () ->
-                            mCallbacks.broadcast(
-                                    callback -> {
-                                        try {
-                                            callback.sendResult(extras);
-                                        } catch (RemoteException e) {
-                                            Slog.e(
-                                                    TAG,
-                                                    "error detected: " + e.getLocalizedMessage(),
-                                                    e);
-                                        }
-                                    }));
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index 3a9dedc..7f0aadc 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -16,12 +16,7 @@
 
 package com.android.server.pm;
 
-import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
-import static android.Manifest.permission.QUERY_ALL_PACKAGES;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-
 import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
 import android.app.usage.UsageEvents;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.Context;
@@ -35,7 +30,6 @@
 import android.os.Build;
 import android.os.Environment;
 import android.os.Handler;
-import android.os.IRemoteCallback;
 import android.os.Looper;
 import android.os.Message;
 import android.os.SystemClock;
@@ -75,10 +69,8 @@
     private static final String DISK_FILE_NAME = "states";
     private static final String DISK_DIR_NAME = "bic";
 
-    private static final String ENFORCE_PERMISSION_ERROR_MSG =
-            "User is not permitted to call service: ";
-
     private static final int MAX_FOREGROUND_TIME_FRAMES_SIZE = 10;
+
     private static final int MSG_USAGE_EVENT_RECEIVED = 0;
     private static final int MSG_PACKAGE_ADDED = 1;
     private static final int MSG_PACKAGE_REMOVED = 2;
@@ -86,20 +78,19 @@
     private final Context mContext;
     private final BinderService mBinderService;
     private final PackageManager mPackageManager;
-    // TODO migrate all internal PackageManager calls to PackageManagerInternal where possible.
-    // b/310983905
     private final PackageManagerInternal mPackageManagerInternal;
     private final UsageStatsManagerInternal mUsageStatsManagerInternal;
     private final PermissionManagerServiceInternal mPermissionManager;
     private final Handler mHandler;
     private final File mDiskFile;
 
+
     private SparseSetArray<String> mBackgroundInstalledPackages = null;
-    private final BackgroundInstallControlCallbackHelper mCallbackHelper;
 
     // User ID -> package name -> set of foreground time frame
-    private final SparseArrayMap<String, TreeSet<ForegroundTimeFrame>>
-            mInstallerForegroundTimeFrames = new SparseArrayMap<>();
+    private final SparseArrayMap<String,
+            TreeSet<ForegroundTimeFrame>> mInstallerForegroundTimeFrames =
+            new SparseArrayMap<>();
 
     public BackgroundInstallControlService(@NonNull Context context) {
         this(new InjectorImpl(context));
@@ -115,11 +106,13 @@
         mHandler = new EventHandler(injector.getLooper(), this);
         mDiskFile = injector.getDiskFile();
         mUsageStatsManagerInternal = injector.getUsageStatsManagerInternal();
-        mCallbackHelper = injector.getBackgroundInstallControlCallbackHelper();
         mUsageStatsManagerInternal.registerListener(
                 (userId, event) ->
-                        mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED, userId, 0, event)
-                                .sendToTarget());
+                        mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED,
+                                userId,
+                                0,
+                                event).sendToTarget()
+        );
         mBinderService = new BinderService(this);
     }
 
@@ -133,15 +126,12 @@
         @Override
         public ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
                 @PackageManager.PackageInfoFlagsBits long flags, int userId) {
-            mService.enforceCallerQueryPackagesPermissions();
             if (!Build.IS_DEBUGGABLE) {
                 return mService.getBackgroundInstalledPackages(flags, userId);
             }
             // The debug.transparency.bg-install-apps (only works for debuggable builds)
             // is used to set mock list of background installed apps for testing.
             // The list of apps' names is delimited by ",".
-            // TODO: Remove after migrating test to new background install method using
-            // {@link BackgroundInstallControlCallbackHelperTest}.installPackage b/310983905
             String propertyString = SystemProperties.get("debug.transparency.bg-install-apps");
             if (TextUtils.isEmpty(propertyString)) {
                 return mService.getBackgroundInstalledPackages(flags, userId);
@@ -149,41 +139,16 @@
                 return mService.getMockBackgroundInstalledPackages(propertyString);
             }
         }
-
-        @Override
-        public void registerBackgroundInstallCallback(IRemoteCallback callback) {
-            mService.enforceCallerQueryPackagesPermissions();
-            mService.enforceCallerInteractCrossUserPermissions();
-            mService.mCallbackHelper.registerBackgroundInstallCallback(callback);
-        }
-
-        @Override
-        public void unregisterBackgroundInstallCallback(IRemoteCallback callback) {
-            mService.enforceCallerQueryPackagesPermissions();
-            mService.enforceCallerInteractCrossUserPermissions();
-            mService.mCallbackHelper.unregisterBackgroundInstallCallback(callback);
-        }
-    }
-
-    @RequiresPermission(QUERY_ALL_PACKAGES)
-    void enforceCallerQueryPackagesPermissions() throws SecurityException {
-        mContext.enforceCallingPermission(QUERY_ALL_PACKAGES,
-                ENFORCE_PERMISSION_ERROR_MSG + QUERY_ALL_PACKAGES);
-    }
-
-    @RequiresPermission(INTERACT_ACROSS_USERS_FULL)
-    void enforceCallerInteractCrossUserPermissions() throws SecurityException {
-        mContext.enforceCallingPermission(INTERACT_ACROSS_USERS_FULL,
-                ENFORCE_PERMISSION_ERROR_MSG + INTERACT_ACROSS_USERS_FULL);
     }
 
     @VisibleForTesting
     ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
             @PackageManager.PackageInfoFlagsBits long flags, int userId) {
         List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(
-                PackageManager.PackageInfoFlags.of(flags), userId);
+                    PackageManager.PackageInfoFlags.of(flags), userId);
 
         initBackgroundInstalledPackages();
+
         ListIterator<PackageInfo> iter = packages.listIterator();
         while (iter.hasNext()) {
             String packageName = iter.next().packageName;
@@ -205,9 +170,8 @@
         List<PackageInfo> mockPackages = new ArrayList<>();
         for (String name : mockPackageNames) {
             try {
-                PackageInfo packageInfo =
-                        mPackageManager.getPackageInfo(
-                                name, PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL));
+                PackageInfo packageInfo = mPackageManager.getPackageInfo(name,
+                        PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL));
                 mockPackages.add(packageInfo);
             } catch (PackageManager.NameNotFoundException e) {
                 Slog.w(TAG, "Package's PackageInfo not found " + name);
@@ -228,16 +192,18 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MSG_USAGE_EVENT_RECEIVED:
-                    mService.handleUsageEvent(
-                            (UsageEvents.Event) msg.obj, msg.arg1 /* userId */);
+                case MSG_USAGE_EVENT_RECEIVED: {
+                    mService.handleUsageEvent((UsageEvents.Event) msg.obj, msg.arg1 /* userId */);
                     break;
-                case MSG_PACKAGE_ADDED:
+                }
+                case MSG_PACKAGE_ADDED: {
                     mService.handlePackageAdd((String) msg.obj, msg.arg1 /* userId */);
                     break;
-                case MSG_PACKAGE_REMOVED:
+                }
+                case MSG_PACKAGE_REMOVED: {
                     mService.handlePackageRemove((String) msg.obj, msg.arg1 /* userId */);
                     break;
+                }
                 default:
                     Slog.w(TAG, "Unknown message: " + msg.what);
             }
@@ -247,9 +213,8 @@
     void handlePackageAdd(String packageName, int userId) {
         ApplicationInfo appInfo = null;
         try {
-            appInfo =
-                    mPackageManager.getApplicationInfoAsUser(
-                            packageName, PackageManager.ApplicationInfoFlags.of(0), userId);
+            appInfo = mPackageManager.getApplicationInfoAsUser(packageName,
+                    PackageManager.ApplicationInfoFlags.of(0), userId);
         } catch (PackageManager.NameNotFoundException e) {
             Slog.w(TAG, "Package's appInfo not found " + packageName);
             return;
@@ -268,18 +233,15 @@
 
         // the installers without INSTALL_PACKAGES perm can't perform
         // the installation in background. So we can just filter out them.
-        if (mPermissionManager.checkPermission(
-                installerPackageName,
-                android.Manifest.permission.INSTALL_PACKAGES,
-                Context.DEVICE_ID_DEFAULT,
-                userId)
-                != PERMISSION_GRANTED) {
+        if (mPermissionManager.checkPermission(installerPackageName,
+                android.Manifest.permission.INSTALL_PACKAGES, Context.DEVICE_ID_DEFAULT,
+                userId) != PackageManager.PERMISSION_GRANTED) {
             return;
         }
 
         // convert up-time to current time.
-        final long installTimestamp =
-                System.currentTimeMillis() - (SystemClock.uptimeMillis() - appInfo.createTimestamp);
+        final long installTimestamp = System.currentTimeMillis()
+                - (SystemClock.uptimeMillis() - appInfo.createTimestamp);
 
         if (installedByAdb(initiatingPackageName)
                 || wasForegroundInstallation(installerPackageName, userId, installTimestamp)) {
@@ -288,7 +250,6 @@
 
         initBackgroundInstalledPackages();
         mBackgroundInstalledPackages.add(userId, packageName);
-        mCallbackHelper.notifyAllCallbacks(userId, packageName);
         writeBackgroundInstalledPackagesToDisk();
     }
 
@@ -298,8 +259,8 @@
         return PackageManagerServiceUtils.isInstalledByAdb(initiatingPackageName);
     }
 
-    private boolean wasForegroundInstallation(
-            String installerPackageName, int userId, long installTimestamp) {
+    private boolean wasForegroundInstallation(String installerPackageName,
+            int userId, long installTimestamp) {
         TreeSet<BackgroundInstallControlService.ForegroundTimeFrame> foregroundTimeFrames =
                 mInstallerForegroundTimeFrames.get(userId, installerPackageName);
 
@@ -388,12 +349,12 @@
             for (int i = 0; i < mBackgroundInstalledPackages.size(); i++) {
                 int userId = mBackgroundInstalledPackages.keyAt(i);
                 for (String packageName : mBackgroundInstalledPackages.get(userId)) {
-                    long token =
-                            protoOutputStream.start(
-                                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                    long token = protoOutputStream.start(
+                            BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
                     protoOutputStream.write(
                             BackgroundInstalledPackageProto.PACKAGE_NAME, packageName);
-                    protoOutputStream.write(BackgroundInstalledPackageProto.USER_ID, userId + 1);
+                    protoOutputStream.write(
+                            BackgroundInstalledPackageProto.USER_ID, userId + 1);
                     protoOutputStream.end(token);
                 }
             }
@@ -426,28 +387,23 @@
                         != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
                     continue;
                 }
-                long token =
-                        protoInputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                long token = protoInputStream.start(
+                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
                 String packageName = null;
                 int userId = UserHandle.USER_NULL;
                 while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
                     switch (protoInputStream.getFieldNumber()) {
                         case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
-                            packageName =
-                                    protoInputStream.readString(
-                                            BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            packageName = protoInputStream.readString(
+                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
                             break;
                         case (int) BackgroundInstalledPackageProto.USER_ID:
-                            userId =
-                                    protoInputStream.readInt(
-                                            BackgroundInstalledPackageProto.USER_ID)
-                                            - 1;
+                            userId = protoInputStream.readInt(
+                                    BackgroundInstalledPackageProto.USER_ID) - 1;
                             break;
                         default:
-                            Slog.w(
-                                    TAG,
-                                    "Undefined field in proto: "
-                                            + protoInputStream.getFieldNumber());
+                            Slog.w(TAG, "Undefined field in proto: "
+                                    + protoInputStream.getFieldNumber());
                     }
                 }
                 protoInputStream.end(token);
@@ -476,12 +432,9 @@
         if (mInstallerForegroundTimeFrames.contains(userId, pkgName)) {
             return true;
         }
-        return mPermissionManager.checkPermission(
-                pkgName,
-                android.Manifest.permission.INSTALL_PACKAGES,
-                Context.DEVICE_ID_DEFAULT,
-                userId)
-                == PERMISSION_GRANTED;
+        return mPermissionManager.checkPermission(pkgName,
+                android.Manifest.permission.INSTALL_PACKAGES, Context.DEVICE_ID_DEFAULT,
+                userId) == PackageManager.PERMISSION_GRANTED;
     }
 
     @Override
@@ -495,22 +448,21 @@
             publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService);
         }
 
-        mPackageManagerInternal.getPackageList(
-                new PackageManagerInternal.PackageListObserver() {
-                    @Override
-                    public void onPackageAdded(String packageName, int uid) {
-                        final int userId = UserHandle.getUserId(uid);
-                        mHandler.obtainMessage(MSG_PACKAGE_ADDED, userId, 0, packageName)
-                                .sendToTarget();
-                    }
+        mPackageManagerInternal.getPackageList(new PackageManagerInternal.PackageListObserver() {
+            @Override
+            public void onPackageAdded(String packageName, int uid) {
+                final int userId = UserHandle.getUserId(uid);
+                mHandler.obtainMessage(MSG_PACKAGE_ADDED,
+                        userId, 0, packageName).sendToTarget();
+            }
 
-                    @Override
-                    public void onPackageRemoved(String packageName, int uid) {
-                        final int userId = UserHandle.getUserId(uid);
-                        mHandler.obtainMessage(MSG_PACKAGE_REMOVED, userId, 0, packageName)
-                                .sendToTarget();
-                    }
-                });
+            @Override
+            public void onPackageRemoved(String packageName, int uid) {
+                final int userId = UserHandle.getUserId(uid);
+                mHandler.obtainMessage(MSG_PACKAGE_REMOVED,
+                        userId, 0, packageName).sendToTarget();
+            }
+        });
     }
 
     // The foreground time frame (ForegroundTimeFrame) represents the period
@@ -566,7 +518,7 @@
     }
 
     /**
-     * Dependency injector for {@link BackgroundInstallControlService}.
+     * Dependency injector for {@link #BackgroundInstallControlService)}.
      */
     interface Injector {
         Context getContext();
@@ -582,8 +534,6 @@
         Looper getLooper();
 
         File getDiskFile();
-
-        BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper();
     }
 
     private static final class InjectorImpl implements Injector {
@@ -620,11 +570,11 @@
 
         @Override
         public Looper getLooper() {
-            ServiceThread serviceThread =
-                    new ServiceThread(
-                            TAG, android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
+            ServiceThread serviceThread = new ServiceThread(TAG,
+                    android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
             serviceThread.start();
             return serviceThread.getLooper();
+
         }
 
         @Override
@@ -633,10 +583,5 @@
             File file = new File(dir, DISK_FILE_NAME);
             return file;
         }
-
-        @Override
-        public BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper() {
-            return new BackgroundInstallControlCallbackHelper();
-        }
     }
 }
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/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/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/BackgroundInstallControlServiceTests/host/Android.bp b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp
index e3954355..4fcdbfc 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp
+++ b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp
@@ -33,7 +33,6 @@
         ":BackgroundInstallControlServiceTestApp",
         ":BackgroundInstallControlMockApp1",
         ":BackgroundInstallControlMockApp2",
-        ":BackgroundInstallControlMockApp3",
     ],
     test_suites: [
         "general-tests",
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml b/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml
index 031d57f..1e7a78a 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml
+++ b/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml
@@ -29,14 +29,11 @@
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
         <option name="cleanup" value="true" />
         <option name="push-file"
-            key="BackgroundInstallControlMockApp1.apk"
-            value="/data/local/tmp/BackgroundInstallControlMockApp1.apk" />
+                key="BackgroundInstallControlMockApp1.apk"
+                value="/data/local/tmp/BackgroundInstallControlMockApp1.apk" />
         <option name="push-file"
-            key="BackgroundInstallControlMockApp2.apk"
-            value="/data/local/tmp/BackgroundInstallControlMockApp2.apk" />
-        <option name="push-file"
-            key="BackgroundInstallControlMockApp3.apk"
-            value="/data/local/tmp/BackgroundInstallControlMockApp3.apk" />
+                key="BackgroundInstallControlMockApp2.apk"
+                value="/data/local/tmp/BackgroundInstallControlMockApp2.apk" />
     </target_preparer>
 
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java
index 5092a46..7450607 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java
+++ b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java
@@ -41,26 +41,17 @@
     private static final String MOCK_APK_FILE_1 = "BackgroundInstallControlMockApp1.apk";
     private static final String MOCK_APK_FILE_2 = "BackgroundInstallControlMockApp2.apk";
 
-    // TODO: Move the silent installs to test-app using {@link
-    // BackgroundInstallControlServiceTest#installPackage(String, String)} and remove deviceConfig
-    // branch in BICS.
-    // b/310983905
     @Test
     public void testGetMockBackgroundInstalledPackages() throws Exception {
-        installPackage(TEST_DATA_DIR + MOCK_APK_FILE_1);
+        installPackage(TEST_DATA_DIR  + MOCK_APK_FILE_1);
         installPackage(TEST_DATA_DIR + MOCK_APK_FILE_2);
 
         assertThat(getDevice().getAppPackageInfo(MOCK_PACKAGE_NAME_1)).isNotNull();
         assertThat(getDevice().getAppPackageInfo(MOCK_PACKAGE_NAME_2)).isNotNull();
 
-        assertThat(
-                getDevice()
-                        .setProperty(
-                                "debug.transparency.bg-install-apps",
-                                MOCK_PACKAGE_NAME_1 + "," + MOCK_PACKAGE_NAME_2))
-                .isTrue();
-        runDeviceTest(
-                "BackgroundInstallControlServiceTest", "testGetMockBackgroundInstalledPackages");
+        assertThat(getDevice().setProperty("debug.transparency.bg-install-apps",
+                    MOCK_PACKAGE_NAME_1 + "," + MOCK_PACKAGE_NAME_2)).isTrue();
+        runDeviceTest("testGetMockBackgroundInstalledPackages");
         assertThat(getDevice().uninstallPackage(MOCK_PACKAGE_NAME_1)).isNull();
         assertThat(getDevice().uninstallPackage(MOCK_PACKAGE_NAME_2)).isNull();
 
@@ -68,30 +59,16 @@
         assertThat(getDevice().getAppPackageInfo(MOCK_PACKAGE_NAME_2)).isNull();
     }
 
-    @Test
-    public void testRegisterCallback() throws Exception {
-        runDeviceTest(
-                "BackgroundInstallControlServiceTest",
-                "testRegisterBackgroundInstallControlCallback");
-    }
-
-    @Test
-    public void testUnregisterCallback() throws Exception {
-        runDeviceTest(
-                "BackgroundInstallControlServiceTest",
-                "testUnregisterBackgroundInstallControlCallback");
-    }
-
     private void installPackage(String path) throws DeviceNotAvailableException {
         String cmd = "pm install -t --force-queryable " + path;
         CommandResult result = getDevice().executeShellV2Command(cmd);
         assertThat(result.getStatus() == CommandStatus.SUCCESS).isTrue();
     }
 
-    private void runDeviceTest(String testName, String method) throws DeviceNotAvailableException {
+    private void runDeviceTest(String method) throws DeviceNotAvailableException {
         var options = new DeviceTestRunOptions(PACKAGE_NAME);
-        options.setTestClassName(PACKAGE_NAME + "." + testName);
+        options.setTestClassName(PACKAGE_NAME + ".BackgroundInstallControlServiceTest");
         options.setTestMethodName(method);
         runDeviceTests(options);
     }
-}
\ No newline at end of file
+}
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/AndroidManifest.xml b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/AndroidManifest.xml
index b5b8ea0..1fa1f84 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/AndroidManifest.xml
+++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/AndroidManifest.xml
@@ -21,9 +21,6 @@
         <uses-library android:name="android.test.runner" />
     </application>
 
-    <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
-
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
         android:label="APCT tests for background install control service"
         android:targetPackage="com.android.server.pm.test.app" />
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java
index f033fed..b74e561 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java
+++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java
@@ -16,256 +16,54 @@
 
 package com.android.server.pm.test.app;
 
-import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
-import static android.Manifest.permission.QUERY_ALL_PACKAGES;
-
-import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
-
 import static com.google.common.truth.Truth.assertThat;
 
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.pm.IBackgroundInstallControlService;
 import android.content.pm.PackageInfo;
-import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
-import android.os.Bundle;
-import android.os.IRemoteCallback;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.util.Pair;
+import android.os.UserHandle;
 
-import androidx.annotation.NonNull;
-import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.compatibility.common.util.ShellIdentityUtils;
-import com.android.compatibility.common.util.ThrowingRunnable;
-
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.io.FileInputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
 import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 @RunWith(AndroidJUnit4.class)
 public class BackgroundInstallControlServiceTest {
     private static final String TAG = "BackgroundInstallControlServiceTest";
-    private static final String ACTION_INSTALL_COMMIT =
-            "com.android.server.pm.test.app.BackgroundInstallControlServiceTest"
-                    + ".ACTION_INSTALL_COMMIT";
-    private static final String MOCK_PACKAGE_NAME = "com.android.servicestests.apps.bicmockapp3";
 
-    private static final String TEST_DATA_DIR = "/data/local/tmp/";
-
-    private static final String MOCK_APK_FILE = "BackgroundInstallControlMockApp3.apk";
     private IBackgroundInstallControlService mIBics;
 
     @Before
     public void setUp() {
-        mIBics =
-                IBackgroundInstallControlService.Stub.asInterface(
-                        ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE));
+        mIBics = IBackgroundInstallControlService.Stub.asInterface(
+                ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE));
         assertThat(mIBics).isNotNull();
     }
 
-    @After
-    public void tearDown() {
-        runShellCommand("pm uninstall " + MOCK_PACKAGE_NAME);
-    }
-
     @Test
     public void testGetMockBackgroundInstalledPackages() throws RemoteException {
-        ParceledListSlice<PackageInfo> slice =
-                ShellIdentityUtils.invokeMethodWithShellPermissions(
-                        mIBics,
-                        (bics) -> {
-                            try {
-                                return bics.getBackgroundInstalledPackages(
-                                        PackageManager.MATCH_ALL, Process.myUserHandle()
-                                                .getIdentifier());
-                            } catch (RemoteException e) {
-                                throw new RuntimeException(e);
-                            }
-                        },
-                        QUERY_ALL_PACKAGES);
+        ParceledListSlice<PackageInfo> slice = mIBics.getBackgroundInstalledPackages(
+                    PackageManager.MATCH_ALL,
+                    UserHandle.USER_ALL);
         assertThat(slice).isNotNull();
 
         var packageList = slice.getList();
         assertThat(packageList).isNotNull();
         assertThat(packageList).hasSize(2);
 
-        var expectedPackageNames =
-                Set.of(
-                        "com.android.servicestests.apps.bicmockapp1",
-                        "com.android.servicestests.apps.bicmockapp2");
-        var actualPackageNames =
-                packageList.stream()
-                        .map((packageInfo) -> packageInfo.packageName)
-                        .collect(Collectors.toSet());
+        var expectedPackageNames = Set.of("com.android.servicestests.apps.bicmockapp1",
+                "com.android.servicestests.apps.bicmockapp2");
+        var actualPackageNames = packageList.stream().map((packageInfo) -> packageInfo.packageName)
+                .collect(Collectors.toSet());
         assertThat(actualPackageNames).containsExactlyElementsIn(expectedPackageNames);
     }
-
-    @Test
-    public void testRegisterBackgroundInstallControlCallback()
-            throws Exception {
-        String testPackageName = "test";
-        int testUserId = 1;
-        ArrayList<Pair<String, Integer>> sharedResource = new ArrayList<>();
-        IRemoteCallback testCallback =
-                new IRemoteCallback.Stub() {
-                    private final ArrayList<Pair<String, Integer>> mArray = sharedResource;
-
-                    @Override
-                    public void sendResult(Bundle data) throws RemoteException {
-                        mArray.add(new Pair(testPackageName, testUserId));
-                    }
-                };
-        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
-                mIBics,
-                (bics) -> {
-                    try {
-                        bics.registerBackgroundInstallCallback(testCallback);
-                    } catch (RemoteException e) {
-                        throw new RuntimeException(e);
-                    }
-                },
-                QUERY_ALL_PACKAGES,
-                INTERACT_ACROSS_USERS_FULL);
-        installPackage(TEST_DATA_DIR + MOCK_APK_FILE, MOCK_PACKAGE_NAME);
-
-        assertUntil(() -> sharedResource.size() == 1, 2000);
-        assertThat(sharedResource.get(0).first).isEqualTo(testPackageName);
-        assertThat(sharedResource.get(0).second).isEqualTo(testUserId);
-    }
-
-    @Test
-    public void testUnregisterBackgroundInstallControlCallback() {
-        String testValue = "test";
-        ArrayList<String> sharedResource = new ArrayList<>();
-        IRemoteCallback testCallback =
-                new IRemoteCallback.Stub() {
-                    private final ArrayList<String> mArray = sharedResource;
-
-                    @Override
-                    public void sendResult(Bundle data) throws RemoteException {
-                        mArray.add(testValue);
-                    }
-                };
-        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
-                mIBics,
-                (bics) -> {
-                    try {
-                        bics.registerBackgroundInstallCallback(testCallback);
-                        bics.unregisterBackgroundInstallCallback(testCallback);
-                    } catch (RemoteException e) {
-                        throw new RuntimeException(e);
-                    }
-                },
-                QUERY_ALL_PACKAGES,
-                INTERACT_ACROSS_USERS_FULL);
-        installPackage(TEST_DATA_DIR + MOCK_APK_FILE, MOCK_PACKAGE_NAME);
-
-        assertUntil(() -> sharedResource.isEmpty(), 2000);
-    }
-
-    private static boolean installPackage(String apkPath, String packageName) {
-        Context context = InstrumentationRegistry.getInstrumentation().getContext();
-        final CountDownLatch installLatch = new CountDownLatch(1);
-        final BroadcastReceiver installReceiver =
-                new BroadcastReceiver() {
-                    public void onReceive(Context context, Intent intent) {
-                        int packageInstallStatus =
-                                intent.getIntExtra(
-                                        PackageInstaller.EXTRA_STATUS,
-                                        PackageInstaller.STATUS_FAILURE_INVALID);
-                        if (packageInstallStatus == PackageInstaller.STATUS_SUCCESS) {
-                            installLatch.countDown();
-                        }
-                    }
-                };
-        final IntentFilter intentFilter = new IntentFilter(ACTION_INSTALL_COMMIT);
-        context.registerReceiver(installReceiver, intentFilter, Context.RECEIVER_EXPORTED);
-
-        PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
-        PackageInstaller.SessionParams params =
-                new PackageInstaller.SessionParams(
-                        PackageInstaller.SessionParams.MODE_FULL_INSTALL);
-        params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED);
-        try {
-            int sessionId = packageInstaller.createSession(params);
-            PackageInstaller.Session session = packageInstaller.openSession(sessionId);
-            OutputStream out = session.openWrite(packageName, 0, -1);
-            FileInputStream fis = new FileInputStream(apkPath);
-            byte[] buffer = new byte[65536];
-            int size;
-            while ((size = fis.read(buffer)) != -1) {
-                out.write(buffer, 0, size);
-            }
-            session.fsync(out);
-            fis.close();
-            out.close();
-
-            runWithShellPermissionIdentity(
-                    () -> {
-                        session.commit(createPendingIntent(context).getIntentSender());
-                        installLatch.await(5, TimeUnit.SECONDS);
-                    });
-            return true;
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    private static PendingIntent createPendingIntent(Context context) {
-        PendingIntent pendingIntent =
-                PendingIntent.getBroadcast(
-                        context,
-                        1,
-                        new Intent(ACTION_INSTALL_COMMIT)
-                                .setPackage(
-                                        BackgroundInstallControlServiceTest.class.getPackageName()),
-                        PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE);
-        return pendingIntent;
-    }
-
-    private static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable command)
-            throws Exception {
-        InstrumentationRegistry.getInstrumentation()
-                .getUiAutomation()
-                .adoptShellPermissionIdentity();
-        try {
-            command.run();
-        } finally {
-            InstrumentationRegistry.getInstrumentation()
-                    .getUiAutomation()
-                    .dropShellPermissionIdentity();
-        }
-    }
-
-    private static void assertUntil(Supplier<Boolean> condition, int timeoutMs) {
-        long endTime = System.currentTimeMillis() + timeoutMs;
-        while (System.currentTimeMillis() <= endTime) {
-            if (condition.get()) return;
-            try {
-                Thread.sleep(10);
-            } catch (InterruptedException e) {
-                throw new RuntimeException(e);
-            }
-        }
-        assertThat(condition.get()).isTrue();
-    }
 }
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp b/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp
index 39b0ff7..7804f4c 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp
+++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp
@@ -50,11 +50,3 @@
         "--rename-manifest-package com.android.servicestests.apps.bicmockapp2",
     ],
 }
-
-android_test_helper_app {
-    name: "BackgroundInstallControlMockApp3",
-    defaults: ["bic-mock-app-defaults"],
-    aaptflags: [
-        "--rename-manifest-package com.android.servicestests.apps.bicmockapp3",
-    ],
-}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java
deleted file mode 100644
index e1fce9b..0000000
--- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java
+++ /dev/null
@@ -1,95 +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.server.pm;
-
-import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_PACKAGE_NAME_KEY;
-import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_USER_ID_KEY;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.after;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import android.os.Bundle;
-import android.os.IRemoteCallback;
-import android.os.RemoteException;
-import android.platform.test.annotations.Presubmit;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.ArgumentCaptor;
-
-/** Unit tests for {@link BackgroundInstallControlCallbackHelperTest} */
-@Presubmit
-@RunWith(JUnit4.class)
-public class BackgroundInstallControlCallbackHelperTest {
-
-    private final IRemoteCallback mCallback =
-            spy(
-                    new IRemoteCallback.Stub() {
-                        @Override
-                        public void sendResult(Bundle extras) {}
-                    });
-
-    private BackgroundInstallControlCallbackHelper mCallbackHelper;
-
-    @Before
-    public void setup() {
-        mCallbackHelper = new BackgroundInstallControlCallbackHelper();
-    }
-
-    @Test
-    public void registerBackgroundInstallControlCallback_registers_successfully() {
-        mCallbackHelper.registerBackgroundInstallCallback(mCallback);
-
-        synchronized (mCallbackHelper.mCallbacks) {
-            assertEquals(1, mCallbackHelper.mCallbacks.getRegisteredCallbackCount());
-            assertEquals(mCallback, mCallbackHelper.mCallbacks.getRegisteredCallbackItem(0));
-        }
-    }
-
-    @Test
-    public void unregisterBackgroundInstallControlCallback_unregisters_successfully() {
-        synchronized (mCallbackHelper.mCallbacks) {
-            mCallbackHelper.mCallbacks.register(mCallback);
-        }
-
-        mCallbackHelper.unregisterBackgroundInstallCallback(mCallback);
-
-        synchronized (mCallbackHelper.mCallbacks) {
-            assertEquals(0, mCallbackHelper.mCallbacks.getRegisteredCallbackCount());
-        }
-    }
-
-    @Test
-    public void notifyAllCallbacks_broadcastsToCallbacks()
-            throws RemoteException {
-        String testPackageName = "testname";
-        int testUserId = 1;
-        mCallbackHelper.registerBackgroundInstallCallback(mCallback);
-
-        mCallbackHelper.notifyAllCallbacks(testUserId, testPackageName);
-
-        ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
-        verify(mCallback, after(1000).times(1)).sendResult(bundleCaptor.capture());
-        Bundle receivedBundle = bundleCaptor.getValue();
-        assertEquals(testPackageName, receivedBundle.getString(FLAGGED_PACKAGE_NAME_KEY));
-        assertEquals(testUserId, receivedBundle.getInt(FLAGGED_USER_ID_KEY));
-    }
-}
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/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
index 3069d25..daf18ed 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
@@ -16,10 +16,6 @@
 
 package com.android.server.pm;
 
-import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
-import static android.Manifest.permission.QUERY_ALL_PACKAGES;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -31,7 +27,6 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -102,6 +97,7 @@
     private Looper mLooper;
     private File mFile;
 
+
     @Mock
     private Context mContext;
     @Mock
@@ -112,12 +108,8 @@
     private UsageStatsManagerInternal mUsageStatsManagerInternal;
     @Mock
     private PermissionManagerServiceInternal mPermissionManager;
-    @Mock
-    private BackgroundInstallControlCallbackHelper mCallbackHelper;
-
     @Captor
     private ArgumentCaptor<PackageManagerInternal.PackageListObserver> mPackageListObserverCaptor;
-
     @Captor
     private ArgumentCaptor<UsageEventListener> mUsageEventListenerCaptor;
 
@@ -127,12 +119,11 @@
 
         mTestLooper = new TestLooper();
         mLooper = mTestLooper.getLooper();
-        mFile =
-                new File(
-                        InstrumentationRegistry.getInstrumentation().getContext().getCacheDir(),
-                        "test");
-        mBackgroundInstallControlService =
-                new BackgroundInstallControlService(new MockInjector(mContext));
+        mFile = new File(
+                InstrumentationRegistry.getInstrumentation().getContext().getCacheDir(),
+                "test");
+        mBackgroundInstallControlService = new BackgroundInstallControlService(
+                new MockInjector(mContext));
 
         verify(mUsageStatsManagerInternal).registerListener(mUsageEventListenerCaptor.capture());
         mUsageEventListener = mUsageEventListenerCaptor.getValue();
@@ -152,7 +143,8 @@
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
         mBackgroundInstallControlService.initBackgroundInstalledPackages();
         assertNotNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
-        assertEquals(0, mBackgroundInstallControlService.getBackgroundInstalledPackages().size());
+        assertEquals(0,
+                mBackgroundInstallControlService.getBackgroundInstalledPackages().size());
     }
 
     @Test
@@ -169,9 +161,12 @@
         // Write test data to the file on the disk.
         try {
             ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
-            long token = protoOutputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
-            protoOutputStream.write(BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1);
-            protoOutputStream.write(BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1);
+            long token = protoOutputStream.start(
+                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1);
             protoOutputStream.end(token);
             protoOutputStream.flush();
             atomicFile.finishWrite(fileOutputStream);
@@ -203,14 +198,20 @@
         try {
             ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
 
-            long token = protoOutputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
-            protoOutputStream.write(BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1);
-            protoOutputStream.write(BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1);
+            long token = protoOutputStream.start(
+                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1);
             protoOutputStream.end(token);
 
-            token = protoOutputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
-            protoOutputStream.write(BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_2);
-            protoOutputStream.write(BackgroundInstalledPackageProto.USER_ID, USER_ID_2 + 1);
+            token = protoOutputStream.start(
+                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_2);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.USER_ID, USER_ID_2 + 1);
             protoOutputStream.end(token);
 
             protoOutputStream.flush();
@@ -240,7 +241,7 @@
         // Read the file on the disk to verify
         var packagesInDisk = new SparseSetArray<>();
         AtomicFile atomicFile = new AtomicFile(mFile);
-        try (FileInputStream fileInputStream = atomicFile.openRead()) {
+        try (FileInputStream fileInputStream  = atomicFile.openRead()) {
             ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
 
             while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
@@ -248,25 +249,23 @@
                         != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
                     continue;
                 }
-                long token =
-                        protoInputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                long token = protoInputStream.start(
+                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
                 String packageName = null;
                 int userId = UserHandle.USER_NULL;
                 while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
                     switch (protoInputStream.getFieldNumber()) {
                         case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
-                            packageName =
-                                    protoInputStream.readString(
-                                            BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            packageName = protoInputStream.readString(
+                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
                             break;
                         case (int) BackgroundInstalledPackageProto.USER_ID:
-                            userId =
-                                    protoInputStream.readInt(
-                                            BackgroundInstalledPackageProto.USER_ID)
-                                            - 1;
+                            userId = protoInputStream.readInt(
+                                    BackgroundInstalledPackageProto.USER_ID) - 1;
                             break;
                         default:
-                            fail("Undefined field in proto: " + protoInputStream.getFieldNumber());
+                            fail("Undefined field in proto: "
+                                    + protoInputStream.getFieldNumber());
                     }
                 }
                 protoInputStream.end(token);
@@ -297,7 +296,7 @@
         // Read the file on the disk to verify
         var packagesInDisk = new SparseSetArray<>();
         AtomicFile atomicFile = new AtomicFile(mFile);
-        try (FileInputStream fileInputStream = atomicFile.openRead()) {
+        try (FileInputStream fileInputStream  = atomicFile.openRead()) {
             ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
 
             while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
@@ -305,25 +304,23 @@
                         != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
                     continue;
                 }
-                long token =
-                        protoInputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                long token = protoInputStream.start(
+                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
                 String packageName = null;
                 int userId = UserHandle.USER_NULL;
                 while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
                     switch (protoInputStream.getFieldNumber()) {
                         case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
-                            packageName =
-                                    protoInputStream.readString(
-                                            BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            packageName = protoInputStream.readString(
+                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
                             break;
                         case (int) BackgroundInstalledPackageProto.USER_ID:
-                            userId =
-                                    protoInputStream.readInt(
-                                            BackgroundInstalledPackageProto.USER_ID)
-                                            - 1;
+                            userId = protoInputStream.readInt(
+                                    BackgroundInstalledPackageProto.USER_ID) - 1;
                             break;
                         default:
-                            fail("Undefined field in proto: " + protoInputStream.getFieldNumber());
+                            fail("Undefined field in proto: "
+                                    + protoInputStream.getFieldNumber());
                     }
                 }
                 protoInputStream.end(token);
@@ -356,7 +353,7 @@
         // Read the file on the disk to verify
         var packagesInDisk = new SparseSetArray<>();
         AtomicFile atomicFile = new AtomicFile(mFile);
-        try (FileInputStream fileInputStream = atomicFile.openRead()) {
+        try (FileInputStream fileInputStream  = atomicFile.openRead()) {
             ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
 
             while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
@@ -364,25 +361,23 @@
                         != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
                     continue;
                 }
-                long token =
-                        protoInputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                long token = protoInputStream.start(
+                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
                 String packageName = null;
                 int userId = UserHandle.USER_NULL;
                 while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
                     switch (protoInputStream.getFieldNumber()) {
                         case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
-                            packageName =
-                                    protoInputStream.readString(
-                                            BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            packageName = protoInputStream.readString(
+                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
                             break;
                         case (int) BackgroundInstalledPackageProto.USER_ID:
-                            userId =
-                                    protoInputStream.readInt(
-                                            BackgroundInstalledPackageProto.USER_ID)
-                                            - 1;
+                            userId = protoInputStream.readInt(
+                                    BackgroundInstalledPackageProto.USER_ID) - 1;
                             break;
                         default:
-                            fail("Undefined field in proto: " + protoInputStream.getFieldNumber());
+                            fail("Undefined field in proto: "
+                                    + protoInputStream.getFieldNumber());
                     }
                 }
                 protoInputStream.end(token);
@@ -404,55 +399,51 @@
 
     @Test
     public void testHandleUsageEvent_permissionDenied() {
-        assertEquals(
-                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PackageManager.PERMISSION_DENIED)
-                .when(mPermissionManager)
-                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, USER_ID_1, INSTALLER_NAME_1, 0);
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_DENIED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, 0);
         mTestLooper.dispatchAll();
-        assertEquals(
-                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
     }
 
     @Test
     public void testHandleUsageEvent_permissionGranted() {
-        assertEquals(
-                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PERMISSION_GRANTED)
-                .when(mPermissionManager)
-                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, USER_ID_1, INSTALLER_NAME_1, 0);
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, 0);
         mTestLooper.dispatchAll();
-        assertEquals(
-                1, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        assertEquals(1,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
     }
 
     @Test
     public void testHandleUsageEvent_ignoredEvent() {
-        assertEquals(
-                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PERMISSION_GRANTED)
-                .when(mPermissionManager)
-                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(UsageEvents.Event.USER_INTERACTION, USER_ID_1, INSTALLER_NAME_1, 0);
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.USER_INTERACTION,
+                USER_ID_1, INSTALLER_NAME_1, 0);
         mTestLooper.dispatchAll();
-        assertEquals(
-                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
     }
 
     @Test
     public void testHandleUsageEvent_firstActivityResumedHalfTimeFrame() {
-        assertEquals(
-                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PERMISSION_GRANTED)
-                .when(mPermissionManager)
-                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(
-                UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1,
-                INSTALLER_NAME_1,
-                USAGE_EVENT_TIMESTAMP_1);
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
         mTestLooper.dispatchAll();
 
         var installerForegroundTimeFrames =
@@ -470,18 +461,14 @@
 
     @Test
     public void testHandleUsageEvent_firstActivityResumedOneTimeFrame() {
-        assertEquals(
-                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PERMISSION_GRANTED)
-                .when(mPermissionManager)
-                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(
-                UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1,
-                INSTALLER_NAME_1,
-                USAGE_EVENT_TIMESTAMP_1);
-        generateUsageEvent(
-                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
         mTestLooper.dispatchAll();
 
         var installerForegroundTimeFrames =
@@ -499,23 +486,16 @@
 
     @Test
     public void testHandleUsageEvent_firstActivityResumedOneAndHalfTimeFrame() {
-        assertEquals(
-                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PERMISSION_GRANTED)
-                .when(mPermissionManager)
-                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(
-                UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1,
-                INSTALLER_NAME_1,
-                USAGE_EVENT_TIMESTAMP_1);
-        generateUsageEvent(
-                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
-        generateUsageEvent(
-                UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1,
-                INSTALLER_NAME_1,
-                USAGE_EVENT_TIMESTAMP_3);
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
         mTestLooper.dispatchAll();
 
         var installerForegroundTimeFrames =
@@ -537,13 +517,12 @@
 
     @Test
     public void testHandleUsageEvent_firstNoneActivityResumed() {
-        assertEquals(
-                0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
-        doReturn(PERMISSION_GRANTED)
-                .when(mPermissionManager)
-                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(
-                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
         mTestLooper.dispatchAll();
 
         var installerForegroundTimeFrames =
@@ -556,26 +535,27 @@
     }
 
     @Test
-    public void testHandleUsageEvent_packageAddedNoUsageEvent()
-            throws NoSuchFieldException, PackageManager.NameNotFoundException {
+    public void testHandleUsageEvent_packageAddedNoUsageEvent() throws
+            NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
-        InstallSourceInfo installSourceInfo =
-                new InstallSourceInfo(
-                        /* initiatingPackageName= */ INSTALLER_NAME_1,
-                        /* initiatingPackageSigningInfo= */ null,
-                        /* originatingPackageName= */ null,
-                        /* installingPackageName= */ INSTALLER_NAME_1);
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ INSTALLER_NAME_1,
+                /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
         assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
         when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
         ApplicationInfo appInfo = mock(ApplicationInfo.class);
 
-        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
-                .thenReturn(appInfo);
+        when(mPackageManager.getApplicationInfoAsUser(
+                eq(PACKAGE_NAME_1),
+                any(),
+                anyInt())
+        ).thenReturn(appInfo);
 
-        long createTimestamp =
-                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
-        FieldSetter.setField(
-                appInfo,
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
                 createTimestamp);
 
@@ -592,26 +572,27 @@
     }
 
     @Test
-    public void testHandleUsageEvent_packageAddedInsideTimeFrame()
-            throws NoSuchFieldException, PackageManager.NameNotFoundException {
+    public void testHandleUsageEvent_packageAddedInsideTimeFrame() throws
+            NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
-        InstallSourceInfo installSourceInfo =
-                new InstallSourceInfo(
-                        /* initiatingPackageName= */ INSTALLER_NAME_1,
-                        /* initiatingPackageSigningInfo= */ null,
-                        /* originatingPackageName= */ null,
-                        /* installingPackageName= */ INSTALLER_NAME_1);
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ INSTALLER_NAME_1,
+                /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
         assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
         when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
         ApplicationInfo appInfo = mock(ApplicationInfo.class);
 
-        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
-                .thenReturn(appInfo);
+        when(mPackageManager.getApplicationInfoAsUser(
+                eq(PACKAGE_NAME_1),
+                any(),
+                anyInt())
+        ).thenReturn(appInfo);
 
-        long createTimestamp =
-                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
-        FieldSetter.setField(
-                appInfo,
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
                 createTimestamp);
 
@@ -623,16 +604,12 @@
         // The 2 usage events make the package adding inside a time frame.
         // So it's not a background install. Thus, it's null for the return of
         // mBackgroundInstallControlService.getBackgroundInstalledPackages()
-        doReturn(PERMISSION_GRANTED)
-                .when(mPermissionManager)
-                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(
-                UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1,
-                INSTALLER_NAME_1,
-                USAGE_EVENT_TIMESTAMP_1);
-        generateUsageEvent(
-                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
 
         mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
         mTestLooper.dispatchAll();
@@ -640,26 +617,27 @@
     }
 
     @Test
-    public void testHandleUsageEvent_packageAddedOutsideTimeFrame1()
-            throws NoSuchFieldException, PackageManager.NameNotFoundException {
+    public void testHandleUsageEvent_packageAddedOutsideTimeFrame1() throws
+            NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
-        InstallSourceInfo installSourceInfo =
-                new InstallSourceInfo(
-                        /* initiatingPackageName= */ INSTALLER_NAME_1,
-                        /* initiatingPackageSigningInfo= */ null,
-                        /* originatingPackageName= */ null,
-                        /* installingPackageName= */ INSTALLER_NAME_1);
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ INSTALLER_NAME_1,
+                /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
         assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
         when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
         ApplicationInfo appInfo = mock(ApplicationInfo.class);
 
-        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
-                .thenReturn(appInfo);
+        when(mPackageManager.getApplicationInfoAsUser(
+                eq(PACKAGE_NAME_1),
+                any(),
+                anyInt())
+        ).thenReturn(appInfo);
 
-        long createTimestamp =
-                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
-        FieldSetter.setField(
-                appInfo,
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
                 createTimestamp);
 
@@ -672,16 +650,12 @@
         // Compared to testHandleUsageEvent_packageAddedInsideTimeFrame,
         // it's a background install. Thus, it's not null for the return of
         // mBackgroundInstallControlService.getBackgroundInstalledPackages()
-        doReturn(PERMISSION_GRANTED)
-                .when(mPermissionManager)
-                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(
-                UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1,
-                INSTALLER_NAME_1,
-                USAGE_EVENT_TIMESTAMP_2);
-        generateUsageEvent(
-                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
 
         mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
         mTestLooper.dispatchAll();
@@ -691,28 +665,28 @@
         assertEquals(1, packages.size());
         assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
     }
-
     @Test
-    public void testHandleUsageEvent_packageAddedOutsideTimeFrame2()
-            throws NoSuchFieldException, PackageManager.NameNotFoundException {
+    public void testHandleUsageEvent_packageAddedOutsideTimeFrame2() throws
+            NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
-        InstallSourceInfo installSourceInfo =
-                new InstallSourceInfo(
-                        /* initiatingPackageName= */ INSTALLER_NAME_1,
-                        /* initiatingPackageSigningInfo= */ null,
-                        /* originatingPackageName= */ null,
-                        /* installingPackageName= */ INSTALLER_NAME_1);
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ INSTALLER_NAME_1,
+                /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
         assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
         when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
         ApplicationInfo appInfo = mock(ApplicationInfo.class);
 
-        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
-                .thenReturn(appInfo);
+        when(mPackageManager.getApplicationInfoAsUser(
+                eq(PACKAGE_NAME_1),
+                any(),
+                anyInt())
+        ).thenReturn(appInfo);
 
-        long createTimestamp =
-                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
-        FieldSetter.setField(
-                appInfo,
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
                 createTimestamp);
 
@@ -726,16 +700,12 @@
         // Compared to testHandleUsageEvent_packageAddedInsideTimeFrame,
         // it's a background install. Thus, it's not null for the return of
         // mBackgroundInstallControlService.getBackgroundInstalledPackages()
-        doReturn(PERMISSION_GRANTED)
-                .when(mPermissionManager)
-                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(
-                UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_2,
-                INSTALLER_NAME_2,
-                USAGE_EVENT_TIMESTAMP_2);
-        generateUsageEvent(
-                Event.ACTIVITY_STOPPED, USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_3);
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_3);
 
         mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
         mTestLooper.dispatchAll();
@@ -745,31 +715,31 @@
         assertEquals(1, packages.size());
         assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
     }
-
     @Test
-    public void testHandleUsageEvent_packageAddedThroughAdb()
-            throws NoSuchFieldException, PackageManager.NameNotFoundException {
+    public void testHandleUsageEvent_packageAddedThroughAdb() throws
+            NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
         // This test is a duplicate of testHandleUsageEvent_packageAddedThroughAdb except the
         // initiatingPackageName used to be null but is now "com.android.shell". This test ensures
         // that the behavior is still the same for when the initiatingPackageName is null.
-        InstallSourceInfo installSourceInfo =
-                new InstallSourceInfo(
-                        /* initiatingPackageName= */ null,
-                        /* initiatingPackageSigningInfo= */ null,
-                        /* originatingPackageName= */ null,
-                        /* installingPackageName= */ INSTALLER_NAME_1);
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ null,
+                /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
         // b/265203007
         when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
         ApplicationInfo appInfo = mock(ApplicationInfo.class);
 
-        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
-                .thenReturn(appInfo);
+        when(mPackageManager.getApplicationInfoAsUser(
+                eq(PACKAGE_NAME_1),
+                any(),
+                anyInt())
+        ).thenReturn(appInfo);
 
-        long createTimestamp =
-                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
-        FieldSetter.setField(
-                appInfo,
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
                 createTimestamp);
 
@@ -781,16 +751,12 @@
         // for ADB installs the initiatingPackageName used to be null, despite being detected
         // as a background install. Since we do not want to treat side-loaded apps as background
         // install getBackgroundInstalledPackages() is expected to return null
-        doReturn(PERMISSION_GRANTED)
-                .when(mPermissionManager)
-                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(
-                UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1,
-                INSTALLER_NAME_1,
-                USAGE_EVENT_TIMESTAMP_2);
-        generateUsageEvent(
-                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
 
         mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
         mTestLooper.dispatchAll();
@@ -798,31 +764,31 @@
         var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
         assertNull(packages);
     }
-
     @Test
-    public void testHandleUsageEvent_packageAddedThroughAdb2()
-            throws NoSuchFieldException, PackageManager.NameNotFoundException {
+    public void testHandleUsageEvent_packageAddedThroughAdb2() throws
+            NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
         // This test is a duplicate of testHandleUsageEvent_packageAddedThroughAdb except the
         // initiatingPackageName used to be null but is now "com.android.shell". This test ensures
         // that the behavior is still the same after this change.
-        InstallSourceInfo installSourceInfo =
-                new InstallSourceInfo(
-                        /* initiatingPackageName= */ "com.android.shell",
-                        /* initiatingPackageSigningInfo= */ null,
-                        /* originatingPackageName= */ null,
-                        /* installingPackageName= */ INSTALLER_NAME_1);
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ "com.android.shell",
+                /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
         // b/265203007
         when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
         ApplicationInfo appInfo = mock(ApplicationInfo.class);
 
-        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
-                .thenReturn(appInfo);
+        when(mPackageManager.getApplicationInfoAsUser(
+                eq(PACKAGE_NAME_1),
+                any(),
+                anyInt())
+        ).thenReturn(appInfo);
 
-        long createTimestamp =
-                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
-        FieldSetter.setField(
-                appInfo,
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
                 createTimestamp);
 
@@ -834,16 +800,12 @@
         // for ADB installs the initiatingPackageName is com.android.shell, despite being detected
         // as a background install. Since we do not want to treat side-loaded apps as background
         // install getBackgroundInstalledPackages() is expected to return null
-        doReturn(PERMISSION_GRANTED)
-                .when(mPermissionManager)
-                .checkPermission(anyString(), anyString(), anyInt(), anyInt());
-        generateUsageEvent(
-                UsageEvents.Event.ACTIVITY_RESUMED,
-                USER_ID_1,
-                INSTALLER_NAME_1,
-                USAGE_EVENT_TIMESTAMP_2);
-        generateUsageEvent(
-                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
 
         mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
         mTestLooper.dispatchAll();
@@ -851,7 +813,6 @@
         var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
         assertNull(packages);
     }
-
     @Test
     public void testPackageRemoved() {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
@@ -898,7 +859,8 @@
         packages.add(packageInfo2);
         var packageInfo3 = makePackageInfo(PACKAGE_NAME_3);
         packages.add(packageInfo3);
-        doReturn(packages).when(mPackageManager).getInstalledPackagesAsUser(any(), anyInt());
+        doReturn(packages).when(mPackageManager).getInstalledPackagesAsUser(
+                any(), anyInt());
 
         var resultPackages =
                 mBackgroundInstallControlService.getBackgroundInstalledPackages(0L, USER_ID_1);
@@ -908,44 +870,18 @@
         assertFalse(resultPackages.getList().contains(packageInfo3));
     }
 
-    @Test(expected = SecurityException.class)
-    public void enforceCallerQueryPackagesPermissionsThrowsSecurityException() {
-        doThrow(new SecurityException("test")).when(mContext)
-                .enforceCallingPermission(eq(QUERY_ALL_PACKAGES), anyString());
-
-        mBackgroundInstallControlService.enforceCallerQueryPackagesPermissions();
-    }
-
-    @Test
-    public void enforceCallerQueryPackagesPermissionsDoesNotThrowSecurityException() {
-        //enforceCallerQueryPackagesPermissions do not throw
-
-        mBackgroundInstallControlService.enforceCallerQueryPackagesPermissions();
-    }
-
-    @Test(expected = SecurityException.class)
-    public void enforceCallerInteractCrossUserPermissionsThrowsSecurityException() {
-        doThrow(new SecurityException("test")).when(mContext)
-                .enforceCallingPermission(eq(INTERACT_ACROSS_USERS_FULL), anyString());
-
-        mBackgroundInstallControlService.enforceCallerInteractCrossUserPermissions();
-    }
-    @Test
-    public void enforceCallerInteractCrossUserPermissionsDoesNotThrowSecurityException() {
-        //enforceCallerQueryPackagesPermissions do not throw
-
-        mBackgroundInstallControlService.enforceCallerInteractCrossUserPermissions();
-    }
-
     /**
      * Mock a usage event occurring.
      *
      * @param usageEventId id of a usage event
-     * @param userId       user id of a usage event
-     * @param pkgName      package name of a usage event
-     * @param timestamp    timestamp of a usage event
+     * @param userId user id of a usage event
+     * @param pkgName package name of a usage event
+     * @param timestamp timestamp of a usage event
      */
-    private void generateUsageEvent(int usageEventId, int userId, String pkgName, long timestamp) {
+    private void generateUsageEvent(int usageEventId,
+            int userId,
+            String pkgName,
+            long timestamp) {
         Event event = new Event(usageEventId, timestamp);
         event.mPackage = pkgName;
         mUsageEventListener.onUsageEvent(userId, event);
@@ -999,10 +935,5 @@
         public File getDiskFile() {
             return mFile;
         }
-
-        @Override
-        public BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper() {
-            return mCallbackHelper;
-        }
     }
 }
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/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/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 c7ffe50..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.
diff --git a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java
index fee1b25..2bc056e 100644
--- a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java
+++ b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java
@@ -16,9 +16,6 @@
 
 package android.transparency.test.app;
 
-import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
-import static android.Manifest.permission.QUERY_ALL_PACKAGES;
-
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
@@ -30,7 +27,6 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.internal.os.IBinaryTransparencyService.AppInfo;
 
 import org.junit.Before;
@@ -120,13 +116,7 @@
     @Test
     public void testCollectAllSilentInstalledMbaInfo() {
         // Action
-        var appInfoList =
-                ShellIdentityUtils.invokeMethodWithShellPermissions(
-                        mBt,
-                        (Bt) ->
-                                mBt.collectAllSilentInstalledMbaInfo(new Bundle()),
-                        QUERY_ALL_PACKAGES,
-                        INTERACT_ACROSS_USERS_FULL);
+        var appInfoList = mBt.collectAllSilentInstalledMbaInfo(new Bundle());
 
         // Verify
         assertThat(appInfoList).isNotEmpty();  // because we just installed from the host side
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/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());
+    }
+}