diff --git a/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl b/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl
new file mode 100644
index 0000000000000000000000000000000000000000..73ac333cfd89998843ac13a932838e6c55f0ffd3
--- /dev/null
+++ b/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.hardware.biometrics;
+
+/**
+ * Low-level callback interface between <Biometric>Manager and <Auth>Service. Allows core system
+ * services (e.g. SystemUI) to register a listener for updates about the current state of biometric
+ * authentication.
+ * @hide
+ */
+oneway interface AuthenticationStateListener {
+    /**
+     * Defines behavior in response to authentication starting
+     * @param requestReason reason from [BiometricRequestConstants.RequestReason] for requesting
+     * authentication starting
+     */
+    void onAuthenticationStarted(int requestReason);
+
+    /**
+     * Defines behavior in response to authentication stopping
+     */
+    void onAuthenticationStopped();
+}
diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java
index f82f79eba8c91cd5a63ada836cc9be390b7ea4f1..d7d1d1a7c6778027512d98d8996defdd9ed057bc 100644
--- a/core/java/android/hardware/biometrics/BiometricManager.java
+++ b/core/java/android/hardware/biometrics/BiometricManager.java
@@ -551,6 +551,44 @@ public class BiometricManager {
         }
     }
 
+    /**
+     * Registers listener for changes to biometric authentication state.
+     * Only sends callbacks for events that occur after the callback has been registered.
+     * @param listener Listener for changes to biometric authentication state
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void registerAuthenticationStateListener(AuthenticationStateListener listener) {
+        if (mService != null) {
+            try {
+                mService.registerAuthenticationStateListener(listener);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        } else {
+            Slog.w(TAG, "registerAuthenticationStateListener(): Service not connected");
+        }
+    }
+
+    /**
+     * Unregisters listener for changes to biometric authentication state.
+     * @param listener Listener for changes to biometric authentication state
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void unregisterAuthenticationStateListener(AuthenticationStateListener listener) {
+        if (mService != null) {
+            try {
+                mService.unregisterAuthenticationStateListener(listener);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        } else {
+            Slog.w(TAG, "unregisterAuthenticationStateListener(): Service not connected");
+        }
+    }
+
+
     /**
      * Requests all {@link Authenticators.Types#BIOMETRIC_STRONG} sensors to have their
      * authenticatorId invalidated for the specified user. This happens when enrollments have been
diff --git a/core/java/android/hardware/biometrics/BiometricOverlayConstants.java b/core/java/android/hardware/biometrics/BiometricRequestConstants.java
similarity index 69%
rename from core/java/android/hardware/biometrics/BiometricOverlayConstants.java
rename to core/java/android/hardware/biometrics/BiometricRequestConstants.java
index 065ae64a92ad227a58754253ed403e7159a63b1a..b036f309f7df4e114ff0666c74400dfe520adadc 100644
--- a/core/java/android/hardware/biometrics/BiometricOverlayConstants.java
+++ b/core/java/android/hardware/biometrics/BiometricRequestConstants.java
@@ -22,24 +22,27 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * Common constants for biometric overlays.
+ * Common constants for biometric requests.
  * @hide
  */
-public interface BiometricOverlayConstants {
+public class BiometricRequestConstants {
+
+    private BiometricRequestConstants() {}
+
     /** Unknown usage. */
-    int REASON_UNKNOWN = 0;
+    public static final int REASON_UNKNOWN = 0;
     /** User is about to enroll. */
-    int REASON_ENROLL_FIND_SENSOR = 1;
+    public static final int REASON_ENROLL_FIND_SENSOR = 1;
     /** User is enrolling. */
-    int REASON_ENROLL_ENROLLING = 2;
+    public static final int REASON_ENROLL_ENROLLING = 2;
     /** Usage from BiometricPrompt. */
-    int REASON_AUTH_BP = 3;
-    /** Usage from Keyguard. */
-    int REASON_AUTH_KEYGUARD = 4;
+    public static final int REASON_AUTH_BP = 3;
+    /** Usage from Device Entry. */
+    public static final int REASON_AUTH_KEYGUARD = 4;
     /** Non-specific usage (from FingerprintManager). */
-    int REASON_AUTH_OTHER = 5;
+    public static final int REASON_AUTH_OTHER = 5;
     /** Usage from Settings. */
-    int REASON_AUTH_SETTINGS = 6;
+    public static final int REASON_AUTH_SETTINGS = 6;
 
     @IntDef({REASON_UNKNOWN,
             REASON_ENROLL_FIND_SENSOR,
@@ -49,5 +52,5 @@ public interface BiometricOverlayConstants {
             REASON_AUTH_OTHER,
             REASON_AUTH_SETTINGS})
     @Retention(RetentionPolicy.SOURCE)
-    @interface ShowReason {}
+    public @interface RequestReason {}
 }
diff --git a/core/java/android/hardware/biometrics/IAuthService.aidl b/core/java/android/hardware/biometrics/IAuthService.aidl
index 5bdbe2b573b2f474a285bed42778f6ad0aa22d0c..8514f98fbf0dd03d986a2b36fe81294fb7ea486e 100644
--- a/core/java/android/hardware/biometrics/IAuthService.aidl
+++ b/core/java/android/hardware/biometrics/IAuthService.aidl
@@ -16,6 +16,7 @@
 
 package android.hardware.biometrics;
 
+import android.hardware.biometrics.AuthenticationStateListener;
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
 import android.hardware.biometrics.IBiometricServiceReceiver;
 import android.hardware.biometrics.IInvalidationCallback;
@@ -66,6 +67,12 @@ interface IAuthService {
     // Register callback for when keyguard biometric eligibility changes.
     void registerEnabledOnKeyguardCallback(IBiometricEnabledOnKeyguardCallback callback);
 
+    // Register listener for changes to authentication state.
+    void registerAuthenticationStateListener(AuthenticationStateListener listener);
+
+    // Unregister listener for changes to authentication state.
+    void unregisterAuthenticationStateListener(AuthenticationStateListener listener);
+
     // Requests all BIOMETRIC_STRONG sensors to have their authenticatorId invalidated for the
     // specified user. This happens when enrollments have been added on devices with multiple
     // biometric sensors.
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index 935157a48a4505a28d050569da1e5e59512cf3a3..fe7de83337842673623ad74b8867b2b8ff6b1d18 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -983,6 +983,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing
         }
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     /**
      * @hide
      */
diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
index 0100660669e9361c8c670d2f28255432058fc9fc..f594c00b0e4771f4405b07dc23d4b7fd6d9b34a6 100644
--- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl
+++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
@@ -15,6 +15,7 @@
  */
 package android.hardware.fingerprint;
 
+import android.hardware.biometrics.AuthenticationStateListener;
 import android.hardware.biometrics.IBiometricSensorReceiver;
 import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
 import android.hardware.biometrics.IBiometricStateListener;
@@ -203,6 +204,14 @@ interface IFingerprintService {
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     void setSidefpsController(in ISidefpsController controller);
 
+    // Registers AuthenticationStateListener.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void registerAuthenticationStateListener(AuthenticationStateListener listener);
+
+    // Unregisters AuthenticationStateListener.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void unregisterAuthenticationStateListener(AuthenticationStateListener listener);
+
     // Registers BiometricStateListener.
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     void registerBiometricStateListener(IBiometricStateListener listener);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index 695d888d94f5807e7f133f0801e26cbf4cf04641..f1701356c134ca85d73cad787d7de412df41609f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -18,7 +18,7 @@
 package com.android.keyguard
 
 import android.content.res.Configuration
-import android.hardware.biometrics.BiometricOverlayConstants
+import android.hardware.biometrics.BiometricRequestConstants
 import android.media.AudioManager
 import android.telephony.TelephonyManager
 import android.testing.TestableLooper.RunWithLooper
@@ -59,6 +59,7 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.ObservableTransitionState
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.DevicePostureController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
@@ -235,6 +236,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() {
                 sceneInteractor = sceneInteractor,
             )
 
+        mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         underTest =
             KeyguardSecurityContainerController(
                 view,
@@ -763,16 +765,18 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() {
 
     @Test
     fun sideFpsControllerShow() {
+        mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         underTest.updateSideFpsVisibility(/* isVisible= */ true)
         verify(sideFpsController)
             .show(
                 SideFpsUiRequestSource.PRIMARY_BOUNCER,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD
             )
     }
 
     @Test
     fun sideFpsControllerHide() {
+        mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         underTest.updateSideFpsVisibility(/* isVisible= */ false)
         verify(sideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index a1b801cd3d3f7f06620eb7207834ae1ed03c9256..f8321b7e7eb3d5d35d2e098d9aef065580dfe8a1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -22,9 +22,9 @@ import android.app.ActivityTaskManager
 import android.content.ComponentName
 import android.graphics.Insets
 import android.graphics.Rect
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS
+import android.hardware.biometrics.BiometricRequestConstants.REASON_UNKNOWN
 import android.hardware.biometrics.SensorLocationInternal
 import android.hardware.biometrics.SensorProperties
 import android.hardware.display.DisplayManager
@@ -65,6 +65,7 @@ import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.res.R
+import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.time.FakeSystemClock
@@ -144,6 +145,7 @@ class SideFpsControllerTest : SysuiTestCase() {
 
     @Before
     fun setup() {
+        mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         displayRepository = FakeDisplayRepository()
         displayStateRepository = FakeDisplayStateRepository()
         keyguardBouncerRepository = FakeKeyguardBouncerRepository()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 90d36e7fd81492167789caa664f432c00401190b..a726b7c2b075930343fd6143f28bd5afef302746 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -17,12 +17,12 @@
 package com.android.systemui.biometrics
 
 import android.graphics.Rect
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING
-import android.hardware.biometrics.BiometricOverlayConstants.ShowReason
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_OTHER
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS
+import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING
+import android.hardware.biometrics.BiometricRequestConstants.RequestReason
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
 import android.testing.TestableLooper.RunWithLooper
 import android.view.LayoutInflater
@@ -135,7 +135,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() {
     }
 
     private fun withReason(
-        @ShowReason reason: Int,
+        @RequestReason reason: Int,
         isDebuggable: Boolean = false,
         enableDeviceEntryUdfpsRefactor: Boolean = false,
         block: () -> Unit,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 97ee526b7108f4e043aed3d8fca283607e0e25f5..dddcf18c1ede5708bb874c18e65a3f191350abd0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -43,7 +43,7 @@ import static org.mockito.Mockito.when;
 
 import android.graphics.Rect;
 import android.hardware.biometrics.BiometricFingerprintConstants;
-import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.biometrics.BiometricRequestConstants;
 import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.SensorProperties;
 import android.hardware.display.DisplayManager;
@@ -359,7 +359,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
     @Test
     public void dozeTimeTick() throws RemoteException {
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
         mUdfpsController.dozeTimeTick();
         verify(mUdfpsView).dozeTimeTick();
@@ -455,7 +455,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
     public void hideUdfpsOverlay_resetsAltAuthBouncerWhenShowing() throws RemoteException {
         // GIVEN overlay was showing and the udfps bouncer is showing
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
 
         // WHEN the overlay is hidden
@@ -469,7 +469,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
     @Test
     public void showUdfpsOverlay_callsListener() throws RemoteException {
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         verify(mFingerprintManager).onUdfpsUiEvent(FingerprintManager.UDFPS_UI_OVERLAY_SHOWN,
@@ -479,7 +479,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
     @Test
     public void testSubscribesToOrientationChangesWhenShowingOverlay() throws Exception {
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         verify(mDisplayManager).registerDisplayListener(any(), eq(mHandler), anyLong());
@@ -520,7 +520,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
                             reset(mWindowManager);
                             mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID,
                                     mOpticalProps.sensorId,
-                                    BiometricOverlayConstants.REASON_ENROLL_ENROLLING,
+                                    BiometricRequestConstants.REASON_ENROLL_ENROLLING,
                                     mUdfpsOverlayControllerCallback);
                             mFgExecutor.runAllReady();
                             verify(mWindowManager).addView(any(), any());
@@ -555,7 +555,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
 
         // Show the overlay.
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
-                BiometricOverlayConstants.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
         verify(mWindowManager).addView(any(), any());
 
@@ -637,7 +637,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
 
         // Show the overlay.
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
-                BiometricOverlayConstants.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
 
@@ -720,7 +720,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
 
         initUdfpsController(testParams.sensorProps);
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         // WHEN ACTION_DOWN is received
@@ -778,7 +778,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
 
         // GIVEN that the overlay is showing and screen is on and fp is running
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
         // WHEN fingerprint is requested because of AOD interrupt
@@ -808,7 +808,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
 
         // GIVEN AOD interrupt
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
         mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
@@ -886,7 +886,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
 
         // GIVEN overlay is showing
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
         if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
             when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
@@ -917,7 +917,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
 
         // GIVEN AOD interrupt
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
         mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
@@ -958,7 +958,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
         final Pair<TouchProcessorResult, TouchProcessorResult> touchProcessorResult =
                 givenFingerEvent(InteractionEvent.DOWN, InteractionEvent.UP, false);
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
 
         if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
             // Configure UdfpsView to accept the ACTION_UP event
@@ -1019,7 +1019,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
 
         // GIVEN screen off
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOff();
         mFgExecutor.runAllReady();
 
@@ -1041,7 +1041,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
 
         // GIVEN showing overlay
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
 
@@ -1126,7 +1126,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
         // GIVEN that the overlay is showing and a11y touch exploration NOT enabled
         when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(a11y);
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         if (a11y) {
@@ -1148,7 +1148,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
         // GIVEN that the overlay is showing and a11y touch exploration NOT enabled
         when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
@@ -1197,7 +1197,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
                         -1 /* pointerId */, touchData);
 
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
@@ -1289,7 +1289,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
     public void onAodInterrupt_onAcquiredGood_fingerNoLongerDown() throws RemoteException {
         // GIVEN UDFPS overlay is showing
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         // GIVEN there's been an AoD interrupt
@@ -1316,7 +1316,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
             throws RemoteException {
         // GIVEN UDFPS overlay is showing
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+                BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         // GIVEN there's been an AoD interrupt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
index 90e0c19b7c65e8e9cced1b0617c29c878878e476..a3bf3f492e6e5ef3f4367e3e34198fc969005422 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
@@ -33,6 +33,7 @@ import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.data.repository.TrustRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
+import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.utils.os.FakeHandler
@@ -105,8 +106,10 @@ class KeyguardBouncerViewModelTest : SysuiTestCase() {
         job.cancel()
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Test
     fun shouldUpdateSideFps_show() = runTest {
+        mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         var count = 0
         val job = underTest.shouldUpdateSideFps.onEach { count++ }.launchIn(this)
         repository.setPrimaryShow(true)
@@ -116,8 +119,10 @@ class KeyguardBouncerViewModelTest : SysuiTestCase() {
         job.cancel()
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Test
     fun shouldUpdateSideFps_hide() = runTest {
+        mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         repository.setPrimaryShow(true)
         var count = 0
         val job = underTest.shouldUpdateSideFps.onEach { count++ }.launchIn(this)
@@ -128,8 +133,10 @@ class KeyguardBouncerViewModelTest : SysuiTestCase() {
         job.cancel()
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Test
     fun sideFpsShowing() = runTest {
+        mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         var sideFpsIsShowing = false
         val job = underTest.sideFpsShowing.onEach { sideFpsIsShowing = it }.launchIn(this)
         repository.setSideFpsShowing(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
index 2b7221ec192cb29b206e16d51c06c8cc27cf594c..6b7d2635ffa4e4b96560a1bf46f159419e707ed4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
@@ -304,4 +304,34 @@ class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() {
             )
             assertThat(authenticationStatus).isNull()
         }
+
+    @Test
+    fun onBiometricRunningStateChanged_shouldUpdateIndicatorVisibility() =
+        testScope.runTest {
+            val shouldUpdateIndicatorVisibility by
+                collectLastValue(underTest.shouldUpdateIndicatorVisibility)
+            runCurrent()
+
+            verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+            assertThat(shouldUpdateIndicatorVisibility).isFalse()
+
+            invokeOnCallback {
+                it.onBiometricRunningStateChanged(false, BiometricSourceType.FINGERPRINT)
+            }
+            assertThat(shouldUpdateIndicatorVisibility).isTrue()
+        }
+
+    @Test
+    fun onStrongAuthStateChanged_shouldUpdateIndicatorVisibility() =
+        testScope.runTest {
+            val shouldUpdateIndicatorVisibility by
+                collectLastValue(underTest.shouldUpdateIndicatorVisibility)
+            runCurrent()
+
+            verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+            assertThat(shouldUpdateIndicatorVisibility).isFalse()
+
+            invokeOnCallback { it.onStrongAuthStateChanged(0) }
+            assertThat(shouldUpdateIndicatorVisibility).isTrue()
+        }
 }
diff --git a/packages/SystemUI/res/layout/sidefps_view.xml b/packages/SystemUI/res/layout/sidefps_view.xml
index 4d952209da6fca254e0e3b609b732c90adadc4a0..fc4bf8a65643f41c78054382f389007efcde0719 100644
--- a/packages/SystemUI/res/layout/sidefps_view.xml
+++ b/packages/SystemUI/res/layout/sidefps_view.xml
@@ -14,7 +14,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<com.android.systemui.biometrics.SideFpsLottieViewWrapper
+<com.android.systemui.biometrics.SideFpsIndicatorView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/sidefps_animation"
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 0a4378e07b45c7304067294987ee59afe55109ed..cce2018f733f759ccb6f127450ef6f23b9b39ec8 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -38,7 +38,7 @@ import android.app.admin.DevicePolicyManager;
 import android.content.Intent;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
-import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.biometrics.BiometricRequestConstants;
 import android.media.AudioManager;
 import android.metrics.LogMaker;
 import android.os.SystemClock;
@@ -74,6 +74,7 @@ import com.android.systemui.Gefingerpoken;
 import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate;
 import com.android.systemui.biometrics.SideFpsController;
 import com.android.systemui.biometrics.SideFpsUiRequestSource;
+import com.android.systemui.biometrics.shared.SideFpsControllerRefactor;
 import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.classifier.FalsingA11yDelegate;
@@ -486,7 +487,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
         mSceneContainerFlags = sceneContainerFlags;
         mGlobalSettings = globalSettings;
         mSessionTracker = sessionTracker;
-        mSideFpsController = sideFpsController;
+        if (SideFpsControllerRefactor.isEnabled()) {
+            mSideFpsController = Optional.empty();
+        } else {
+            mSideFpsController = sideFpsController;
+        }
         mFalsingA11yDelegate = falsingA11yDelegate;
         mTelephonyManager = telephonyManager;
         mViewMediatorCallback = viewMediatorCallback;
@@ -569,12 +574,14 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
         mView.clearFocus();
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     /**
      * Shows and hides the side finger print sensor animation.
      *
      * @param isVisible sets whether we show or hide the side fps animation
      */
     public void updateSideFpsVisibility(boolean isVisible) {
+        SideFpsControllerRefactor.assertInLegacyMode();
         if (!mSideFpsController.isPresent()) {
             return;
         }
@@ -582,7 +589,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
         if (isVisible) {
             mSideFpsController.get().show(
                     SideFpsUiRequestSource.PRIMARY_BOUNCER,
-                    BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+                    BiometricRequestConstants.REASON_AUTH_KEYGUARD
             );
         } else {
             mSideFpsController.get().hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 877afce7fe6577d5aacf15fcffa7499bc76359d1..5fba761b2f0942ab2d3fe8b1f6eec5fb909d2965 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -70,6 +70,7 @@ import com.android.systemui.CoreStartable;
 import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
 import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
+import com.android.systemui.biometrics.shared.SideFpsControllerRefactor;
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams;
 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
@@ -88,6 +89,8 @@ import com.android.systemui.util.concurrency.Execution;
 
 import dagger.Lazy;
 
+import kotlin.Unit;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -101,7 +104,6 @@ import java.util.Set;
 import javax.inject.Inject;
 import javax.inject.Provider;
 
-import kotlin.Unit;
 import kotlinx.coroutines.CoroutineScope;
 
 /**
@@ -317,7 +319,9 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
 
         mSidefpsProps = !sidefpsProps.isEmpty() ? sidefpsProps : null;
         if (mSidefpsProps != null) {
-            mSideFpsController = mSidefpsControllerFactory.get();
+            if (!SideFpsControllerRefactor.isEnabled()) {
+                mSideFpsController = mSidefpsControllerFactory.get();
+            }
         }
 
         mFingerprintManager.registerBiometricStateListener(new BiometricStateListener() {
@@ -1194,7 +1198,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
      * Whether the passed userId has enrolled SFPS.
      */
     public boolean isSfpsEnrolled(int userId) {
-        if (mSideFpsController == null) {
+        if (mSidefpsProps == null) {
             return false;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index 91cee9e51a93b08f7495e904a20dde8250d08bc9..ac99fc69b2b5c11113f0ac0073c102f9f35cd67d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -23,9 +23,9 @@ import android.graphics.PixelFormat
 import android.graphics.PorterDuff
 import android.graphics.PorterDuffColorFilter
 import android.graphics.Rect
-import android.hardware.biometrics.BiometricOverlayConstants
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
+import android.hardware.biometrics.BiometricRequestConstants
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS
 import android.hardware.biometrics.SensorLocationInternal
 import android.hardware.display.DisplayManager
 import android.hardware.fingerprint.FingerprintManager
@@ -58,6 +58,7 @@ import com.android.internal.annotations.VisibleForTesting
 import com.android.keyguard.KeyguardPINView
 import com.android.systemui.Dumpable
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
+import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -91,7 +92,7 @@ constructor(
     @Main private val mainExecutor: DelayableExecutor,
     @Main private val handler: Handler,
     private val alternateBouncerInteractor: AlternateBouncerInteractor,
-    @Application private val scope: CoroutineScope,
+    @Application private val applicationScope: CoroutineScope,
     dumpManager: DumpManager,
     fpsUnlockTracker: FpsUnlockTracker
 ) : Dumpable {
@@ -110,7 +111,7 @@ constructor(
             handler,
             sensorProps,
             { reason -> onOrientationChanged(reason) },
-            BiometricOverlayConstants.REASON_UNKNOWN
+            BiometricRequestConstants.REASON_UNKNOWN
         )
 
     @VisibleForTesting val orientationListener = orientationReasonListener.orientationListener
@@ -169,25 +170,27 @@ constructor(
             }
 
     init {
-        fpsUnlockTracker.startTracking()
-        fingerprintManager?.setSidefpsController(
-            object : ISidefpsController.Stub() {
-                override fun show(
-                    sensorId: Int,
-                    @BiometricOverlayConstants.ShowReason reason: Int
-                ) =
-                    if (reason.isReasonToAutoShow(activityTaskManager)) {
-                        show(SideFpsUiRequestSource.AUTO_SHOW, reason)
-                    } else {
-                        hide(SideFpsUiRequestSource.AUTO_SHOW)
-                    }
-
-                override fun hide(sensorId: Int) = hide(SideFpsUiRequestSource.AUTO_SHOW)
-            }
-        )
-        listenForAlternateBouncerVisibility()
+        if (!SideFpsControllerRefactor.isEnabled) {
+            fpsUnlockTracker.startTracking()
+            fingerprintManager?.setSidefpsController(
+                object : ISidefpsController.Stub() {
+                    override fun show(
+                        sensorId: Int,
+                        @BiometricRequestConstants.RequestReason reason: Int
+                    ) =
+                        if (reason.isReasonToAutoShow(activityTaskManager)) {
+                            show(SideFpsUiRequestSource.AUTO_SHOW, reason)
+                        } else {
+                            hide(SideFpsUiRequestSource.AUTO_SHOW)
+                        }
+
+                    override fun hide(sensorId: Int) = hide(SideFpsUiRequestSource.AUTO_SHOW)
+                }
+            )
+            listenForAlternateBouncerVisibility()
 
-        dumpManager.registerDumpable(this)
+            dumpManager.registerDumpable(this)
+        }
     }
 
     private fun listenForAlternateBouncerVisibility() {
@@ -195,7 +198,7 @@ constructor(
             alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, "SideFpsController")
         }
 
-        scope.launch {
+        applicationScope.launch {
             alternateBouncerInteractor.isVisible.collect { isVisible: Boolean ->
                 if (isVisible) {
                     show(SideFpsUiRequestSource.ALTERNATE_BOUNCER, REASON_AUTH_KEYGUARD)
@@ -209,8 +212,10 @@ constructor(
     /** Shows the side fps overlay if not already shown. */
     fun show(
         request: SideFpsUiRequestSource,
-        @BiometricOverlayConstants.ShowReason reason: Int = BiometricOverlayConstants.REASON_UNKNOWN
+        @BiometricRequestConstants.RequestReason
+        reason: Int = BiometricRequestConstants.REASON_UNKNOWN
     ) {
+        SideFpsControllerRefactor.assertInLegacyMode()
         if (!displayStateInteractor.isInRearDisplayMode.value) {
             requests.add(request)
             mainExecutor.execute {
@@ -229,6 +234,7 @@ constructor(
 
     /** Hides the fps overlay if shown. */
     fun hide(request: SideFpsUiRequestSource) {
+        SideFpsControllerRefactor.assertInLegacyMode()
         requests.remove(request)
         mainExecutor.execute {
             if (requests.isEmpty()) {
@@ -239,6 +245,7 @@ constructor(
 
     /** Hide the arrow indicator. */
     fun hideIndicator() {
+        SideFpsControllerRefactor.assertInLegacyMode()
         val lottieAnimationView =
             overlayView?.findViewById(R.id.sidefps_animation) as LottieAnimationView?
         lottieAnimationView?.visibility = INVISIBLE
@@ -246,6 +253,7 @@ constructor(
 
     /** Show the arrow indicator. */
     fun showIndicator() {
+        SideFpsControllerRefactor.assertInLegacyMode()
         val lottieAnimationView =
             overlayView?.findViewById(R.id.sidefps_animation) as LottieAnimationView?
         lottieAnimationView?.visibility = VISIBLE
@@ -279,13 +287,13 @@ constructor(
         pw.println("currentRotation=${displayInfo.rotation}")
     }
 
-    private fun onOrientationChanged(@BiometricOverlayConstants.ShowReason reason: Int) {
+    private fun onOrientationChanged(@BiometricRequestConstants.RequestReason reason: Int) {
         if (overlayView != null) {
             createOverlayForDisplay(reason)
         }
     }
 
-    private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) {
+    private fun createOverlayForDisplay(@BiometricRequestConstants.RequestReason reason: Int) {
         val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
         overlayView = view
         val display = context.display!!
@@ -395,7 +403,7 @@ private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorProper
 /** Returns [True] when the device has a side fingerprint sensor. */
 fun FingerprintManager?.hasSideFpsSensor(): Boolean = this?.sideFpsSensorProperties != null
 
-@BiometricOverlayConstants.ShowReason
+@BiometricRequestConstants.RequestReason
 private fun Int.isReasonToAutoShow(activityTaskManager: ActivityTaskManager): Boolean =
     when (this) {
         REASON_AUTH_KEYGUARD -> false
@@ -434,7 +442,7 @@ private fun Display.isNaturalOrientation(): Boolean =
 
 private fun LottieAnimationView.addOverlayDynamicColor(
     context: Context,
-    @BiometricOverlayConstants.ShowReason reason: Int
+    @BiometricRequestConstants.RequestReason reason: Int
 ) {
     fun update() {
         val isKeyguard = reason == REASON_AUTH_KEYGUARD
@@ -501,7 +509,7 @@ class OrientationReasonListener(
     handler: Handler,
     sensorProps: FingerprintSensorPropertiesInternal,
     onOrientationChanged: (reason: Int) -> Unit,
-    @BiometricOverlayConstants.ShowReason var reason: Int
+    @BiometricRequestConstants.RequestReason var reason: Int
 ) {
     val orientationListener =
         BiometricDisplayListener(
@@ -516,7 +524,7 @@ class OrientationReasonListener(
 
 /**
  * The source of a request to show the side fps visual indicator. This is distinct from
- * [BiometricOverlayConstants] which corrresponds with the reason fingerprint authentication is
+ * [BiometricRequestConstants] which corresponds with the reason fingerprint authentication is
  * requested.
  */
 enum class SideFpsUiRequestSource {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsLottieViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsIndicatorView.kt
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/biometrics/SideFpsLottieViewWrapper.kt
rename to packages/SystemUI/src/com/android/systemui/biometrics/SideFpsIndicatorView.kt
index e98f6db12d348ce97c175378c422705b32426c6a..d5e25ac84abacea3d7b5513f7cbf0387904b3273 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsLottieViewWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsIndicatorView.kt
@@ -19,6 +19,6 @@ import android.content.Context
 import android.util.AttributeSet
 import com.android.systemui.util.wrapper.LottieViewWrapper
 
-class SideFpsLottieViewWrapper
+class SideFpsIndicatorView
 @JvmOverloads
 constructor(context: Context, attrs: AttributeSet? = null) : LottieViewWrapper(context, attrs)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index bb6ef41bdfd387a6b204874cfd83d598c6aab5b0..65668b56a9f3184ad55482fa4226e6c112d98f0f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -19,10 +19,10 @@ package com.android.systemui.biometrics;
 import static android.app.StatusBarManager.SESSION_BIOMETRIC_PROMPT;
 import static android.app.StatusBarManager.SESSION_KEYGUARD;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD;
-import static android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP;
-import static android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD;
-import static android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING;
-import static android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR;
+import static android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP;
+import static android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD;
+import static android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING;
+import static android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR;
 
 import static com.android.internal.util.LatencyTracker.ACTION_UDFPS_ILLUMINATE;
 import static com.android.internal.util.Preconditions.checkNotNull;
@@ -106,6 +106,8 @@ import com.android.systemui.util.time.SystemClock;
 
 import dagger.Lazy;
 
+import kotlin.Unit;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -115,8 +117,6 @@ import java.util.concurrent.Executor;
 import javax.inject.Inject;
 import javax.inject.Provider;
 
-import kotlin.Unit;
-
 import kotlinx.coroutines.ExperimentalCoroutinesApi;
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 452cd6a7c4df498d5a90470fc4f6203a0ba62031..dae6d08f733198a8677f735b2a38250fea12242b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -21,13 +21,13 @@ import android.annotation.UiThread
 import android.content.Context
 import android.graphics.PixelFormat
 import android.graphics.Rect
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
-import android.hardware.biometrics.BiometricOverlayConstants.ShowReason
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_OTHER
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS
+import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING
+import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR
+import android.hardware.biometrics.BiometricRequestConstants.RequestReason
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
 import android.os.Build
 import android.os.RemoteException
@@ -96,7 +96,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor(
     private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
     private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
     val requestId: Long,
-    @ShowReason val requestReason: Int,
+    @RequestReason val requestReason: Int,
     private val controllerCallback: IUdfpsOverlayControllerCallback,
     private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
     private val activityLaunchAnimator: ActivityLaunchAnimator,
@@ -461,7 +461,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor(
     }
 }
 
-@ShowReason
+@RequestReason
 private fun Int.isImportantForAccessibility() =
     this == REASON_ENROLL_FIND_SENSOR ||
             this == REASON_ENROLL_ENROLLING ||
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
index e3dbcb52325856c7d2b44973f4c5238090feb61a..88b9e1bdfd9716eefad35a8ae28718e319106df5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
@@ -18,13 +18,13 @@ package com.android.systemui.biometrics
 
 import android.content.Context
 import android.graphics.Rect
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_OTHER
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS
+import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING
+import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR
+import android.hardware.biometrics.BiometricRequestConstants.REASON_UNKNOWN
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
 import android.util.Log
 import android.view.LayoutInflater
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index 72fcfe777e792e0a95cdedd657e077b70ac2f383..8ae6f87f4f8316066560addfc3be534edc63758d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -18,8 +18,11 @@ package com.android.systemui.biometrics.dagger
 
 import android.content.res.Resources
 import com.android.internal.R
+import com.android.systemui.CoreStartable
 import com.android.systemui.biometrics.EllipseOverlapDetectorParams
 import com.android.systemui.biometrics.UdfpsUtils
+import com.android.systemui.biometrics.data.repository.BiometricStatusRepository
+import com.android.systemui.biometrics.data.repository.BiometricStatusRepositoryImpl
 import com.android.systemui.biometrics.data.repository.DisplayStateRepository
 import com.android.systemui.biometrics.data.repository.DisplayStateRepositoryImpl
 import com.android.systemui.biometrics.data.repository.FacePropertyRepository
@@ -33,11 +36,14 @@ import com.android.systemui.biometrics.data.repository.PromptRepositoryImpl
 import com.android.systemui.biometrics.udfps.BoundingBoxOverlapDetector
 import com.android.systemui.biometrics.udfps.EllipseOverlapDetector
 import com.android.systemui.biometrics.udfps.OverlapDetector
+import com.android.systemui.biometrics.ui.binder.SideFpsOverlayViewBinder
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.util.concurrency.ThreadFactory
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
 import java.util.concurrent.Executor
 import javax.inject.Qualifier
 
@@ -45,6 +51,11 @@ import javax.inject.Qualifier
 @Module
 interface BiometricsModule {
 
+    @Binds
+    @IntoMap
+    @ClassKey(SideFpsOverlayViewBinder::class)
+    fun bindsSideFpsOverlayViewBinder(viewBinder: SideFpsOverlayViewBinder): CoreStartable
+
     @Binds
     @SysUISingleton
     fun faceSettings(impl: FaceSettingsRepositoryImpl): FaceSettingsRepository
@@ -55,6 +66,10 @@ interface BiometricsModule {
     @SysUISingleton
     fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository
 
+    @Binds
+    @SysUISingleton
+    fun biometricStatusRepository(impl: BiometricStatusRepositoryImpl): BiometricStatusRepository
+
     @Binds
     @SysUISingleton
     fun fingerprintRepository(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ad2136af4b860698fe92407ec0f8afe3993ee60e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.data.repository
+
+import android.hardware.biometrics.AuthenticationStateListener
+import android.hardware.biometrics.BiometricManager
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_OTHER
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS
+import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING
+import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
+import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.shareIn
+
+/** A repository for the state of biometric authentication. */
+interface BiometricStatusRepository {
+    /**
+     * The logical reason for the current fingerprint auth operation if one is on-going, otherwise
+     * [NotRunning].
+     */
+    val fingerprintAuthenticationReason: Flow<AuthenticationReason>
+}
+
+@SysUISingleton
+class BiometricStatusRepositoryImpl
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    private val biometricManager: BiometricManager?
+) : BiometricStatusRepository {
+
+    override val fingerprintAuthenticationReason: Flow<AuthenticationReason> =
+        conflatedCallbackFlow {
+                val updateFingerprintAuthenticateReason = { reason: AuthenticationReason ->
+                    trySendWithFailureLogging(
+                        reason,
+                        TAG,
+                        "Error sending fingerprintAuthenticateReason reason"
+                    )
+                }
+
+                val authenticationStateListener =
+                    object : AuthenticationStateListener.Stub() {
+                        override fun onAuthenticationStarted(requestReason: Int) {
+                            val authenticationReason =
+                                when (requestReason) {
+                                    REASON_AUTH_BP ->
+                                        AuthenticationReason.BiometricPromptAuthentication
+                                    REASON_AUTH_KEYGUARD ->
+                                        AuthenticationReason.DeviceEntryAuthentication
+                                    REASON_AUTH_OTHER -> AuthenticationReason.OtherAuthentication
+                                    REASON_AUTH_SETTINGS ->
+                                        AuthenticationReason.SettingsAuthentication(
+                                            SettingsOperations.OTHER
+                                        )
+                                    REASON_ENROLL_ENROLLING ->
+                                        AuthenticationReason.SettingsAuthentication(
+                                            SettingsOperations.ENROLL_ENROLLING
+                                        )
+                                    REASON_ENROLL_FIND_SENSOR ->
+                                        AuthenticationReason.SettingsAuthentication(
+                                            SettingsOperations.ENROLL_FIND_SENSOR
+                                        )
+                                    else -> AuthenticationReason.Unknown
+                                }
+                            updateFingerprintAuthenticateReason(authenticationReason)
+                        }
+
+                        override fun onAuthenticationStopped() {
+                            updateFingerprintAuthenticateReason(AuthenticationReason.NotRunning)
+                        }
+                    }
+
+                updateFingerprintAuthenticateReason(AuthenticationReason.NotRunning)
+                biometricManager?.registerAuthenticationStateListener(authenticationStateListener)
+                awaitClose {
+                    biometricManager?.unregisterAuthenticationStateListener(
+                        authenticationStateListener
+                    )
+                }
+            }
+            .shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1)
+
+    companion object {
+        private const val TAG = "BiometricStatusRepositoryImpl"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt
index b9b2fd8875d9f3051efefe96f42c465d4e89e6cc..ec3fd9f7da359cd76b1bb32cbe60fb13aaca2636 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.biometrics.domain
 
+import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
+import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractorImpl
 import com.android.systemui.biometrics.domain.interactor.CredentialInteractor
 import com.android.systemui.biometrics.domain.interactor.CredentialInteractorImpl
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
@@ -36,6 +38,12 @@ interface BiometricsDomainLayerModule {
         impl: PromptSelectorInteractorImpl
     ): PromptSelectorInteractor
 
+    @Binds
+    @SysUISingleton
+    fun providesBiometricStatusInteractor(
+        impl: BiometricStatusInteractorImpl
+    ): BiometricStatusInteractor
+
     @Binds
     @SysUISingleton
     fun providesCredentialInteractor(impl: CredentialInteractorImpl): CredentialInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..55a2d3d7563e4f34aa0546ea4185fa8cc5a57545
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.interactor
+
+import android.app.ActivityTaskManager
+import com.android.systemui.biometrics.data.repository.BiometricStatusRepository
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
+import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** Encapsulates business logic for interacting with biometric authentication state. */
+interface BiometricStatusInteractor {
+    /**
+     * The logical reason for the current side fingerprint sensor auth operation if one is on-going,
+     * filtered for when the overlay should be shown, otherwise [NotRunning].
+     */
+    val sfpsAuthenticationReason: Flow<AuthenticationReason>
+}
+
+class BiometricStatusInteractorImpl
+@Inject
+constructor(
+    private val activityTaskManager: ActivityTaskManager,
+    biometricStatusRepository: BiometricStatusRepository,
+) : BiometricStatusInteractor {
+
+    override val sfpsAuthenticationReason: Flow<AuthenticationReason> =
+        biometricStatusRepository.fingerprintAuthenticationReason.map { reason: AuthenticationReason
+            ->
+            if (reason.isReasonToAlwaysUpdateSfpsOverlay(activityTaskManager)) {
+                reason
+            } else {
+                AuthenticationReason.NotRunning
+            }
+        }
+
+    companion object {
+        private const val TAG = "BiometricStatusInteractor"
+    }
+}
+
+/** True if the sfps overlay should always be updated for this request source, false otherwise. */
+private fun AuthenticationReason.isReasonToAlwaysUpdateSfpsOverlay(
+    activityTaskManager: ActivityTaskManager
+): Boolean =
+    when (this) {
+        AuthenticationReason.DeviceEntryAuthentication -> false
+        AuthenticationReason.SettingsAuthentication(SettingsOperations.OTHER) ->
+            when (activityTaskManager.topClass()) {
+                // TODO(b/186176653): exclude fingerprint overlays from this list view
+                "com.android.settings.biometrics.fingerprint.FingerprintSettings" -> false
+                else -> true
+            }
+        else -> true
+    }
+
+internal fun ActivityTaskManager.topClass(): String =
+    getTasks(1).firstOrNull()?.topActivity?.className ?: ""
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt
index f51379940640b0fbfcbf0bdf213620a885f5e4d4..f4231ac01fee90989a3ee9928ec2ed595f8402c6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt
@@ -141,7 +141,6 @@ constructor(
                             }
                         }
                     }
-
                 SideFpsSensorLocation(
                     left = sensorLeft,
                     top = sensorTop,
@@ -149,7 +148,15 @@ constructor(
                     isSensorVerticalInDefaultOrientation = isSensorVerticalInDefaultOrientation
                 )
             }
-            .distinctUntilChanged()
+            .distinctUntilChanged(
+                areEquivalent = { old: SideFpsSensorLocation, new: SideFpsSensorLocation ->
+                    old.left == new.left &&
+                        old.top == new.top &&
+                        old.length == new.length &&
+                        old.isSensorVerticalInDefaultOrientation ==
+                            new.isSensorVerticalInDefaultOrientation
+                }
+            )
             .onEach {
                 logger.sensorLocationStateChanged(
                     it.left,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/SideFpsControllerRefactor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/SideFpsControllerRefactor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..899b07e89964257d2410e55b36c54da8443a579d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/SideFpsControllerRefactor.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.shared
+
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+import com.android.systemui.shared.Flags
+
+/** Helper for reading or using the sidefps controller refactor flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object SideFpsControllerRefactor {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.sidefpsControllerRefactor()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationReason.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationReason.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0c3debbe0fc47046688838f5a01fd2002b5b4fdf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationReason.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.shared.model
+
+/**
+ * The logical reason for a fingerprint auth operation if one is on-going, otherwise [NotRunning].
+ */
+sealed interface AuthenticationReason {
+    /** Device entry requested authentication */
+    data object DeviceEntryAuthentication : AuthenticationReason
+
+    /** Settings requested authentication */
+    data class SettingsAuthentication(val settingsOperation: SettingsOperations) :
+        AuthenticationReason
+
+    /** App requested authentication */
+    data object BiometricPromptAuthentication : AuthenticationReason
+
+    /** Authentication requested for other reason */
+    data object OtherAuthentication : AuthenticationReason
+
+    /** Authentication requested for unknown reason */
+    data object Unknown : AuthenticationReason
+
+    /** Authentication is not running */
+    data object NotRunning : AuthenticationReason
+
+    /** Settings operations that request biometric authentication */
+    enum class SettingsOperations {
+        ENROLL_ENROLLING,
+        ENROLL_FIND_SENSOR,
+        OTHER
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/LottieCallback.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/LottieCallback.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0b3005530e6d29d2921b3c2d6b9cf89812f9d6c8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/LottieCallback.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.shared.model
+
+import com.airbnb.lottie.model.KeyPath
+
+/** Represents properties of a LottieAnimationView callback */
+data class LottieCallback(val keypath: KeyPath, val color: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a8c9446fd689ea78093a7f7d01198d1a7c1d9d14
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.biometrics.ui.binder
+
+import android.content.Context
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.view.LayoutInflater
+import android.view.View
+import android.view.WindowManager
+import android.view.accessibility.AccessibilityEvent
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.airbnb.lottie.LottieAnimationView
+import com.airbnb.lottie.LottieComposition
+import com.airbnb.lottie.LottieProperty
+import com.android.app.animation.Interpolators
+import com.android.keyguard.KeyguardPINView
+import com.android.systemui.CoreStartable
+import com.android.systemui.biometrics.FpsUnlockTracker
+import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
+import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor
+import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
+import com.android.systemui.biometrics.shared.model.AuthenticationReason.NotRunning
+import com.android.systemui.biometrics.shared.model.LottieCallback
+import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor
+import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+/** Binds the side fingerprint sensor indicator view to [SideFpsOverlayViewModel]. */
+@SysUISingleton
+class SideFpsOverlayViewBinder
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Application private val applicationContext: Context,
+    private val biometricStatusInteractor: BiometricStatusInteractor,
+    private val displayStateInteractor: DisplayStateInteractor,
+    private val deviceEntrySideFpsOverlayInteractor: DeviceEntrySideFpsOverlayInteractor,
+    private val fpsUnlockTracker: FpsUnlockTracker,
+    private val layoutInflater: LayoutInflater,
+    private val sideFpsProgressBarViewModel: SideFpsProgressBarViewModel,
+    private val sfpsSensorInteractor: SideFpsSensorInteractor,
+    private val windowManager: WindowManager
+) : CoreStartable {
+
+    override fun start() {
+        if (!SideFpsControllerRefactor.isEnabled) {
+            return
+        }
+        applicationScope
+            .launch {
+                combine(
+                        biometricStatusInteractor.sfpsAuthenticationReason,
+                        deviceEntrySideFpsOverlayInteractor.showIndicatorForDeviceEntry,
+                        sideFpsProgressBarViewModel.isVisible,
+                        ::Triple
+                    )
+                    .sample(displayStateInteractor.isInRearDisplayMode, ::Pair)
+                    .collect { (combinedFlows, isInRearDisplayMode: Boolean) ->
+                        val (
+                            systemServerAuthReason,
+                            showIndicatorForDeviceEntry,
+                            progressBarIsVisible) =
+                            combinedFlows
+                        if (!isInRearDisplayMode) {
+                            if (progressBarIsVisible) {
+                                hide()
+                            } else if (systemServerAuthReason != NotRunning) {
+                                show()
+                            } else if (showIndicatorForDeviceEntry) {
+                                show()
+                            } else {
+                                hide()
+                            }
+                        }
+                    }
+            }
+            .invokeOnCompletion { fpsUnlockTracker.stopTracking() }
+    }
+
+    private var overlayView: View? = null
+    private var lottie: LottieAnimationView? = null
+
+    /** Show the side fingerprint sensor indicator */
+    private fun show() {
+        overlayView?.let {
+            if (it.isAttachedToWindow) {
+                lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation)
+                lottie?.pauseAnimation()
+                windowManager.removeView(it)
+            }
+        }
+
+        overlayView = layoutInflater.inflate(R.layout.sidefps_view, null, false)
+        val overlayViewModel =
+            SideFpsOverlayViewModel(
+                applicationContext,
+                biometricStatusInteractor,
+                deviceEntrySideFpsOverlayInteractor,
+                displayStateInteractor,
+                sfpsSensorInteractor,
+                sideFpsProgressBarViewModel
+            )
+        bind(overlayView!!, overlayViewModel, fpsUnlockTracker, windowManager)
+        overlayView!!.visibility = View.INVISIBLE
+        windowManager.addView(overlayView, overlayViewModel.defaultOverlayViewParams)
+    }
+
+    /** Hide the side fingerprint sensor indicator */
+    private fun hide() {
+        if (overlayView != null) {
+            windowManager.removeView(overlayView)
+            overlayView = null
+        }
+    }
+
+    companion object {
+        private const val TAG = "SideFpsOverlayViewBinder"
+
+        /** Binds overlayView (side fingerprint sensor indicator view) to SideFpsOverlayViewModel */
+        fun bind(
+            overlayView: View,
+            viewModel: SideFpsOverlayViewModel,
+            fpsUnlockTracker: FpsUnlockTracker,
+            windowManager: WindowManager
+        ) {
+            overlayView.repeatWhenAttached {
+                fpsUnlockTracker.startTracking()
+
+                val lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation)
+                lottie.addLottieOnCompositionLoadedListener { composition: LottieComposition ->
+                    viewModel.setLottieBounds(composition.bounds)
+                    overlayView.visibility = View.VISIBLE
+                }
+                it.alpha = 0f
+                val overlayShowAnimator =
+                    it.animate()
+                        .alpha(1f)
+                        .setDuration(KeyguardPINView.ANIMATION_DURATION)
+                        .setInterpolator(Interpolators.ALPHA_IN)
+
+                overlayShowAnimator.start()
+
+                it.setAccessibilityDelegate(
+                    object : View.AccessibilityDelegate() {
+                        override fun dispatchPopulateAccessibilityEvent(
+                            host: View,
+                            event: AccessibilityEvent
+                        ): Boolean {
+                            return if (
+                                event.getEventType() ===
+                                    android.view.accessibility.AccessibilityEvent
+                                        .TYPE_WINDOW_STATE_CHANGED
+                            ) {
+                                true
+                            } else {
+                                super.dispatchPopulateAccessibilityEvent(host, event)
+                            }
+                        }
+                    }
+                )
+
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    launch {
+                        viewModel.lottieCallbacks.collect { callbacks ->
+                            lottie.addOverlayDynamicColor(callbacks)
+                        }
+                    }
+
+                    launch {
+                        viewModel.overlayViewParams.collect { params ->
+                            windowManager.updateViewLayout(it, params)
+                            lottie.resumeAnimation()
+                        }
+                    }
+
+                    launch {
+                        viewModel.overlayViewProperties.collect { properties ->
+                            it.rotation = properties.overlayViewRotation
+                            lottie.setAnimation(properties.indicatorAsset)
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+private fun LottieAnimationView.addOverlayDynamicColor(colorCallbacks: List<LottieCallback>) {
+    addLottieOnCompositionLoadedListener {
+        for (callback in colorCallbacks) {
+            addValueCallback(callback.keypath, LottieProperty.COLOR_FILTER) {
+                PorterDuffColorFilter(callback.color, PorterDuff.Mode.SRC_ATOP)
+            }
+        }
+        resumeAnimation()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ce726034913fc2ad09c641ee10aa3e9747bc4686
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.biometrics.ui.viewmodel
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Color
+import android.graphics.PixelFormat
+import android.graphics.Point
+import android.graphics.Rect
+import android.view.Gravity
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import com.airbnb.lottie.model.KeyPath
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
+import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor
+import com.android.systemui.biometrics.domain.model.SideFpsSensorLocation
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
+import com.android.systemui.biometrics.shared.model.DisplayRotation
+import com.android.systemui.biometrics.shared.model.LottieCallback
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor
+import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+/** Models UI of the side fingerprint sensor indicator view. */
+class SideFpsOverlayViewModel
+@Inject
+constructor(
+    @Application private val applicationContext: Context,
+    biometricStatusInteractor: BiometricStatusInteractor,
+    deviceEntrySideFpsOverlayInteractor: DeviceEntrySideFpsOverlayInteractor,
+    displayStateInteractor: DisplayStateInteractor,
+    sfpsSensorInteractor: SideFpsSensorInteractor,
+    sideFpsProgressBarViewModel: SideFpsProgressBarViewModel
+) {
+    /** Contains properties of the side fingerprint sensor indicator */
+    data class OverlayViewProperties(
+        /** The raw asset for the indicator animation */
+        val indicatorAsset: Int,
+        /** Rotation of the overlayView */
+        val overlayViewRotation: Float,
+    )
+
+    private val _lottieBounds: MutableStateFlow<Rect?> = MutableStateFlow(null)
+
+    /** Used for setting lottie bounds once the composition has loaded. */
+    fun setLottieBounds(bounds: Rect) {
+        _lottieBounds.value = bounds
+    }
+
+    private val displayRotation = displayStateInteractor.currentRotation
+    private val sensorLocation = sfpsSensorInteractor.sensorLocation
+
+    /** Default LayoutParams for the overlayView */
+    val defaultOverlayViewParams: WindowManager.LayoutParams
+        get() =
+            WindowManager.LayoutParams(
+                    WindowManager.LayoutParams.WRAP_CONTENT,
+                    WindowManager.LayoutParams.WRAP_CONTENT,
+                    WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+                    Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS,
+                    PixelFormat.TRANSLUCENT
+                )
+                .apply {
+                    title = TAG
+                    fitInsetsTypes = 0 // overrides default, avoiding status bars during layout
+                    gravity = Gravity.TOP or Gravity.LEFT
+                    layoutInDisplayCutoutMode =
+                        WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+                    privateFlags = PRIVATE_FLAG_TRUSTED_OVERLAY or PRIVATE_FLAG_NO_MOVE_ANIMATION
+                }
+
+    private val indicatorAsset: Flow<Int> =
+        combine(displayRotation, sensorLocation) { rotation: DisplayRotation, sensorLocation ->
+                val yAligned = sensorLocation.isSensorVerticalInDefaultOrientation
+                val newAsset: Int =
+                    when (rotation) {
+                        DisplayRotation.ROTATION_0 ->
+                            if (yAligned) {
+                                R.raw.sfps_pulse
+                            } else {
+                                R.raw.sfps_pulse_landscape
+                            }
+                        DisplayRotation.ROTATION_180 ->
+                            if (yAligned) {
+                                R.raw.sfps_pulse
+                            } else {
+                                R.raw.sfps_pulse_landscape
+                            }
+                        else ->
+                            if (yAligned) {
+                                R.raw.sfps_pulse_landscape
+                            } else {
+                                R.raw.sfps_pulse
+                            }
+                    }
+                newAsset
+            }
+            .distinctUntilChanged()
+
+    private val overlayViewRotation: Flow<Float> =
+        combine(
+                displayRotation,
+                sensorLocation,
+            ) { rotation: DisplayRotation, sensorLocation ->
+                val yAligned = sensorLocation.isSensorVerticalInDefaultOrientation
+                when (rotation) {
+                    DisplayRotation.ROTATION_90 -> if (yAligned) 0f else 180f
+                    DisplayRotation.ROTATION_180 -> 180f
+                    DisplayRotation.ROTATION_270 -> if (yAligned) 180f else 0f
+                    else -> 0f
+                }
+            }
+            .distinctUntilChanged()
+
+    /** Contains properties (animation asset and view rotation) for overlayView */
+    val overlayViewProperties: Flow<OverlayViewProperties> =
+        combine(indicatorAsset, overlayViewRotation) { asset: Int, rotation: Float ->
+            OverlayViewProperties(asset, rotation)
+        }
+
+    /** LayoutParams for placement of overlayView (the side fingerprint sensor indicator view) */
+    val overlayViewParams: Flow<WindowManager.LayoutParams> =
+        combine(
+            _lottieBounds,
+            sensorLocation,
+            displayRotation,
+        ) { bounds: Rect?, sensorLocation: SideFpsSensorLocation, displayRotation: DisplayRotation
+            ->
+            val topLeft = Point(sensorLocation.left, sensorLocation.top)
+
+            if (sensorLocation.isSensorVerticalInDefaultOrientation) {
+                if (displayRotation == DisplayRotation.ROTATION_0) {
+                    topLeft.x -= bounds!!.width()
+                } else if (displayRotation == DisplayRotation.ROTATION_270) {
+                    topLeft.y -= bounds!!.height()
+                }
+            } else {
+                if (displayRotation == DisplayRotation.ROTATION_180) {
+                    topLeft.y -= bounds!!.height()
+                } else if (displayRotation == DisplayRotation.ROTATION_270) {
+                    topLeft.x -= bounds!!.width()
+                }
+            }
+            defaultOverlayViewParams.apply {
+                x = topLeft.x
+                y = topLeft.y
+            }
+        }
+
+    /** List of LottieCallbacks use for adding dynamic color to the overlayView */
+    val lottieCallbacks: Flow<List<LottieCallback>> =
+        combine(
+            biometricStatusInteractor.sfpsAuthenticationReason,
+            deviceEntrySideFpsOverlayInteractor.showIndicatorForDeviceEntry.distinctUntilChanged(),
+            sideFpsProgressBarViewModel.isVisible,
+        ) { reason: AuthenticationReason, showIndicatorForDeviceEntry: Boolean, progressBarIsVisible
+            ->
+            val callbacks = mutableListOf<LottieCallback>()
+            if (showIndicatorForDeviceEntry) {
+                val indicatorColor =
+                    com.android.settingslib.Utils.getColorAttrDefaultColor(
+                        applicationContext,
+                        com.android.internal.R.attr.materialColorPrimaryFixed
+                    )
+                val outerRimColor =
+                    com.android.settingslib.Utils.getColorAttrDefaultColor(
+                        applicationContext,
+                        com.android.internal.R.attr.materialColorPrimaryFixedDim
+                    )
+                val chevronFill =
+                    com.android.settingslib.Utils.getColorAttrDefaultColor(
+                        applicationContext,
+                        com.android.internal.R.attr.materialColorOnPrimaryFixed
+                    )
+                callbacks.add(LottieCallback(KeyPath(".blue600", "**"), indicatorColor))
+                callbacks.add(LottieCallback(KeyPath(".blue400", "**"), outerRimColor))
+                callbacks.add(LottieCallback(KeyPath(".black", "**"), chevronFill))
+            } else {
+                if (!isDarkMode(applicationContext)) {
+                    callbacks.add(LottieCallback(KeyPath(".black", "**"), Color.WHITE))
+                }
+                for (key in listOf(".blue600", ".blue400")) {
+                    callbacks.add(
+                        LottieCallback(
+                            KeyPath(key, "**"),
+                            applicationContext.getColor(
+                                com.android.settingslib.color.R.color.settingslib_color_blue400
+                            ),
+                        )
+                    )
+                }
+            }
+            callbacks
+        }
+
+    companion object {
+        private const val TAG = "SideFpsOverlayViewModel"
+    }
+}
+
+private fun isDarkMode(context: Context): Boolean {
+    val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+    return darkMode == Configuration.UI_MODE_NIGHT_YES
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
index 1e0e16c5472f9b9eca31052488b72266bdf6baf7..c2a1d8fe26c3bcc23d91a6ae92cc32a4add5c09d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
@@ -18,6 +18,7 @@ package com.android.systemui.bouncer.data.repository
 
 import android.os.Build
 import android.util.Log
+import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
 import com.android.systemui.dagger.SysUISingleton
@@ -121,6 +122,7 @@ interface KeyguardBouncerRepository {
 
     fun setAlternateBouncerUIAvailable(isAvailable: Boolean)
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     fun setSideFpsShowing(isShowing: Boolean)
 }
 
@@ -261,7 +263,9 @@ constructor(
         _isBackButtonEnabled.value = isBackButtonEnabled
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     override fun setSideFpsShowing(isShowing: Boolean) {
+        SideFpsControllerRefactor.assertInLegacyMode()
         _sideFpsShowing.value = isShowing
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
index aa7758f9380f80eefff3ae6e6f8441a0a909b6c7..621ca5dc6f5a36749d2ce002ecc94f23d9816982 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
@@ -28,6 +28,7 @@ import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.DejankUtils
+import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
@@ -116,9 +117,11 @@ constructor(
 
     /** Allow for interaction when just about fully visible */
     val isInteractable: Flow<Boolean> = bouncerExpansion.map { it > 0.9 }
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     val sideFpsShowing: Flow<Boolean> = repository.sideFpsShowing
     private var currentUserActiveUnlockRunning = false
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     /** This callback needs to be a class field so it does not get garbage collected. */
     val keyguardUpdateMonitorCallback =
         object : KeyguardUpdateMonitorCallback() {
@@ -135,7 +138,10 @@ constructor(
         }
 
     init {
-        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+        // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+        if (!SideFpsControllerRefactor.isEnabled) {
+            keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+        }
         applicationScope.launch {
             trustRepository.isCurrentUserActiveUnlockRunning.collect {
                 currentUserActiveUnlockRunning = it
@@ -333,8 +339,10 @@ constructor(
         repository.setPrimaryStartDisappearAnimation(runnable)
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     /** Determine whether to show the side fps animation. */
     fun updateSideFpsVisibility() {
+        SideFpsControllerRefactor.assertInLegacyMode()
         val sfpsEnabled: Boolean =
             context.resources.getBoolean(R.bool.config_show_sidefps_hint_on_bouncer)
         val fpsDetectionRunning: Boolean = keyguardUpdateMonitor.isFingerprintDetectionRunning
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
index ac3d4b6c853b84f18cc1c97c8b51112b797f41c3..5dcd6615509d93639456be1dfcc230fd52042560 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
@@ -27,6 +27,7 @@ import com.android.keyguard.KeyguardSecurityContainerController
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardSecurityView
 import com.android.keyguard.dagger.KeyguardBouncerComponent
+import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
 import com.android.systemui.bouncer.ui.BouncerViewDelegate
@@ -233,15 +234,21 @@ object KeyguardBouncerViewBinder {
                             .collect { view.systemUiVisibility = it }
                     }
 
-                    launch {
-                        viewModel.shouldUpdateSideFps.collect {
-                            viewModel.updateSideFpsVisibility()
+                    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+                    if (!SideFpsControllerRefactor.isEnabled) {
+                        launch {
+                            viewModel.shouldUpdateSideFps.collect {
+                                viewModel.updateSideFpsVisibility()
+                            }
                         }
                     }
 
-                    launch {
-                        viewModel.sideFpsShowing.collect {
-                            securityContainerController.updateSideFpsVisibility(it)
+                    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+                    if (!SideFpsControllerRefactor.isEnabled) {
+                        launch {
+                            viewModel.sideFpsShowing.collect {
+                                securityContainerController.updateSideFpsVisibility(it)
+                            }
                         }
                     }
                     awaitCancellation()
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModel.kt
index 649ae2fb1007fd1282e3c46c86119d214f899f89..1c9d1f01e89e7a6927e68cedbd72d6aa592ee69a 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.bouncer.ui.viewmodel
 
 import android.view.View
+import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
 import com.android.systemui.bouncer.ui.BouncerView
@@ -61,9 +62,11 @@ constructor(
     /** Observe whether keyguard is authenticated already. */
     val keyguardAuthenticated: Flow<Boolean> = interactor.keyguardAuthenticatedBiometrics
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     /** Observe whether the side fps is showing. */
     val sideFpsShowing: Flow<Boolean> = interactor.sideFpsShowing
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     /** Observe whether we should update fps is showing. */
     val shouldUpdateSideFps: Flow<Unit> =
         merge(
@@ -87,7 +90,9 @@ constructor(
         interactor.onMessageShown()
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     fun updateSideFpsVisibility() {
+        SideFpsControllerRefactor.assertInLegacyMode()
         interactor.updateSideFpsVisibility()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
index 96386f3dc59693e59f9d725e207e40409ed5afbd..9a13558d3327e4f1c4b2af2b04e307fa4c5226a7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
@@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
 
 /** Encapsulates state about device entry fingerprint auth mechanism. */
@@ -61,6 +62,9 @@ interface DeviceEntryFingerprintAuthRepository {
 
     /** Provide the current status of fingerprint authentication. */
     val authenticationStatus: Flow<FingerprintAuthenticationStatus>
+
+    /** Indicates whether to update the side fingerprint sensor indicator visibility. */
+    val shouldUpdateIndicatorVisibility: Flow<Boolean>
 }
 
 /**
@@ -256,6 +260,37 @@ constructor(
             awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
         }
 
+    override val shouldUpdateIndicatorVisibility: Flow<Boolean> =
+        conflatedCallbackFlow {
+                val sendShouldUpdateIndicatorVisibility =
+                    { shouldUpdateIndicatorVisibility: Boolean ->
+                        trySendWithFailureLogging(
+                            shouldUpdateIndicatorVisibility,
+                            TAG,
+                            "Error sending shouldUpdateIndicatorVisibility " +
+                                "$shouldUpdateIndicatorVisibility"
+                        )
+                    }
+
+                val callback =
+                    object : KeyguardUpdateMonitorCallback() {
+                        override fun onBiometricRunningStateChanged(
+                            running: Boolean,
+                            biometricSourceType: BiometricSourceType?
+                        ) {
+                            sendShouldUpdateIndicatorVisibility(true)
+                        }
+                        override fun onStrongAuthStateChanged(userId: Int) {
+                            sendShouldUpdateIndicatorVisibility(true)
+                        }
+                    }
+                sendShouldUpdateIndicatorVisibility(false)
+                keyguardUpdateMonitor.registerCallback(callback)
+                awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+            }
+            .flowOn(mainDispatcher)
+            .shareIn(scope, started = SharingStarted.WhileSubscribed(), replay = 1)
+
     companion object {
         const val TAG = "DeviceEntryFingerprintAuthRepositoryImpl"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..de15fd6a958fdfa7b0a099c174cab3d32f13896b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
@@ -0,0 +1,92 @@
+/*
+ *   Copyright (C) 2023 The Android Open Source Project
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.content.Context
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+
+/**
+ * Encapsulates business logic for device entry events that impact the side fingerprint sensor
+ * overlay.
+ */
+@SysUISingleton
+class DeviceEntrySideFpsOverlayInteractor
+@Inject
+constructor(
+    @Application private val context: Context,
+    deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+    private val primaryBouncerInteractor: PrimaryBouncerInteractor,
+    alternateBouncerInteractor: AlternateBouncerInteractor,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+) {
+
+    init {
+        alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, TAG)
+    }
+
+    private val showIndicatorForPrimaryBouncer: Flow<Boolean> =
+        merge(
+                primaryBouncerInteractor.isShowing,
+                primaryBouncerInteractor.startingToHide,
+                primaryBouncerInteractor.startingDisappearAnimation.filterNotNull(),
+                deviceEntryFingerprintAuthRepository.shouldUpdateIndicatorVisibility.filter { it }
+            )
+            .map { shouldShowIndicatorForPrimaryBouncer() }
+
+    private val showIndicatorForAlternateBouncer: Flow<Boolean> =
+        alternateBouncerInteractor.isVisible
+
+    /**
+     * Indicates whether the primary or alternate bouncers request showing the side fingerprint
+     * sensor indicator.
+     */
+    val showIndicatorForDeviceEntry: Flow<Boolean> =
+        combine(showIndicatorForPrimaryBouncer, showIndicatorForAlternateBouncer) {
+            showForPrimaryBouncer,
+            showForAlternateBouncer ->
+            showForPrimaryBouncer || showForAlternateBouncer
+        }
+
+    private fun shouldShowIndicatorForPrimaryBouncer(): Boolean {
+        val sfpsEnabled: Boolean =
+            context.resources.getBoolean(R.bool.config_show_sidefps_hint_on_bouncer)
+        val sfpsDetectionRunning = keyguardUpdateMonitor.isFingerprintDetectionRunning
+        val isUnlockingWithFpAllowed = keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed
+        return primaryBouncerInteractor.isBouncerShowing() &&
+            sfpsEnabled &&
+            sfpsDetectionRunning &&
+            isUnlockingWithFpAllowed &&
+            !primaryBouncerInteractor.isAnimatingAway()
+    }
+
+    companion object {
+        private const val TAG = "DeviceEntrySideFpsOverlayInteractor"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/SideFpsProgressBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/SideFpsProgressBarViewBinder.kt
index 560e4adf956a96ba3ebaf1ebac30bc6e424e3ec4..9a6dca3c495879d042411f0de8739aae4ae70bc4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/SideFpsProgressBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/SideFpsProgressBarViewBinder.kt
@@ -21,6 +21,7 @@ import android.graphics.Point
 import com.android.systemui.CoreStartable
 import com.android.systemui.Flags
 import com.android.systemui.biometrics.SideFpsController
+import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.ui.view.SideFpsProgressBar
@@ -46,6 +47,7 @@ constructor(
     private val viewModel: SideFpsProgressBarViewModel,
     private val view: SideFpsProgressBar,
     @Application private val applicationScope: CoroutineScope,
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     private val sfpsController: dagger.Lazy<SideFpsController>,
     private val logger: SideFpsLogger,
     private val commandRegistry: CommandRegistry,
@@ -109,12 +111,14 @@ constructor(
         view.updateView(visible, location, length, thickness, rotation)
         // We have to hide the SFPS indicator as the progress bar will
         // be shown at the same location
-        if (visible) {
-            logger.hidingSfpsIndicator()
-            sfpsController.get().hideIndicator()
-        } else if (fpDetectRunning) {
-            logger.showingSfpsIndicator()
-            sfpsController.get().showIndicator()
+        if (!SideFpsControllerRefactor.isEnabled) {
+            if (visible) {
+                logger.hidingSfpsIndicator()
+                sfpsController.get().hideIndicator()
+            } else if (fpDetectRunning) {
+                logger.showingSfpsIndicator()
+                sfpsController.get().showIndicator()
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
index 2d0712c7f9cc2ffce22e6c94753f72a59a3f90ac..1dbf1f14b569e00ff2d31a00e35ce05fea81fc5a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
@@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel
 import android.animation.ValueAnimator
 import android.content.Context
 import android.graphics.Point
+import androidx.annotation.VisibleForTesting
 import androidx.core.animation.doOnEnd
 import com.android.systemui.Flags
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
@@ -213,4 +214,9 @@ constructor(
             }
         }
     }
+
+    @VisibleForTesting
+    fun setVisible(isVisible: Boolean) {
+        _visible.value = isVisible
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..27d93eb31922ce32e1d4a666cabbf8bd498aa27e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.data.repository
+
+import android.hardware.biometrics.AuthenticationStateListener
+import android.hardware.biometrics.BiometricManager
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_OTHER
+import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS
+import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING
+import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR
+import android.platform.test.annotations.RequiresFlagsEnabled
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
+import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@RequiresFlagsEnabled(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class BiometricStatusRepositoryTest : SysuiTestCase() {
+    @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
+    @Mock private lateinit var biometricManager: BiometricManager
+
+    private lateinit var underTest: BiometricStatusRepository
+
+    private val testScope = TestScope(StandardTestDispatcher())
+
+    @Before
+    fun setUp() {
+        underTest = BiometricStatusRepositoryImpl(testScope.backgroundScope, biometricManager)
+    }
+
+    @Test
+    fun updatesFingerprintAuthenticationReason_whenBiometricPromptAuthenticationStarted() =
+        testScope.runTest {
+            val fingerprintAuthenticationReason by
+                collectLastValue(underTest.fingerprintAuthenticationReason)
+            runCurrent()
+
+            val listener = biometricManager.captureListener()
+
+            assertThat(fingerprintAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+
+            listener.onAuthenticationStarted(REASON_AUTH_BP)
+            assertThat(fingerprintAuthenticationReason)
+                .isEqualTo(AuthenticationReason.BiometricPromptAuthentication)
+        }
+
+    @Test
+    fun updatesFingerprintAuthenticationReason_whenDeviceEntryAuthenticationStarted() =
+        testScope.runTest {
+            val fingerprintAuthenticationReason by
+                collectLastValue(underTest.fingerprintAuthenticationReason)
+            runCurrent()
+
+            val listener = biometricManager.captureListener()
+
+            assertThat(fingerprintAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+
+            listener.onAuthenticationStarted(REASON_AUTH_KEYGUARD)
+            assertThat(fingerprintAuthenticationReason)
+                .isEqualTo(AuthenticationReason.DeviceEntryAuthentication)
+        }
+
+    @Test
+    fun updatesFingerprintAuthenticationReason_whenOtherAuthenticationStarted() =
+        testScope.runTest {
+            val fingerprintAuthenticationReason by
+                collectLastValue(underTest.fingerprintAuthenticationReason)
+            runCurrent()
+
+            val listener = biometricManager.captureListener()
+
+            assertThat(fingerprintAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+
+            listener.onAuthenticationStarted(REASON_AUTH_OTHER)
+            assertThat(fingerprintAuthenticationReason)
+                .isEqualTo(AuthenticationReason.OtherAuthentication)
+        }
+
+    @Test
+    fun updatesFingerprintAuthenticationReason_whenSettingsAuthenticationStarted() =
+        testScope.runTest {
+            val fingerprintAuthenticationReason by
+                collectLastValue(underTest.fingerprintAuthenticationReason)
+            runCurrent()
+
+            val listener = biometricManager.captureListener()
+
+            assertThat(fingerprintAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+
+            listener.onAuthenticationStarted(REASON_AUTH_SETTINGS)
+            assertThat(fingerprintAuthenticationReason)
+                .isEqualTo(AuthenticationReason.SettingsAuthentication(SettingsOperations.OTHER))
+        }
+
+    @Test
+    fun updatesFingerprintAuthenticationReason_whenEnrollmentAuthenticationStarted() =
+        testScope.runTest {
+            val fingerprintAuthenticationReason by
+                collectLastValue(underTest.fingerprintAuthenticationReason)
+            runCurrent()
+
+            val listener = biometricManager.captureListener()
+
+            assertThat(fingerprintAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+
+            listener.onAuthenticationStarted(REASON_ENROLL_FIND_SENSOR)
+            assertThat(fingerprintAuthenticationReason)
+                .isEqualTo(
+                    AuthenticationReason.SettingsAuthentication(
+                        SettingsOperations.ENROLL_FIND_SENSOR
+                    )
+                )
+
+            listener.onAuthenticationStarted(REASON_ENROLL_ENROLLING)
+            assertThat(fingerprintAuthenticationReason)
+                .isEqualTo(
+                    AuthenticationReason.SettingsAuthentication(SettingsOperations.ENROLL_ENROLLING)
+                )
+        }
+
+    @Test
+    fun updatesFingerprintAuthenticationReason_whenAuthenticationStopped() =
+        testScope.runTest {
+            val fingerprintAuthenticationReason by
+                collectLastValue(underTest.fingerprintAuthenticationReason)
+            runCurrent()
+
+            val listener = biometricManager.captureListener()
+
+            listener.onAuthenticationStarted(REASON_AUTH_BP)
+            listener.onAuthenticationStopped()
+            assertThat(fingerprintAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+        }
+}
+
+private fun BiometricManager.captureListener() =
+    withArgCaptor<AuthenticationStateListener> {
+        verify(this@captureListener).registerAuthenticationStateListener(capture())
+    }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6978923879b45ef79bfaa97734e6301a27387a76
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.interactor
+
+import android.app.ActivityManager
+import android.app.ActivityTaskManager
+import android.content.ComponentName
+import android.platform.test.annotations.RequiresFlagsEnabled
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
+import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@RequiresFlagsEnabled(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class BiometricStatusInteractorImplTest : SysuiTestCase() {
+    @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
+    @Mock private lateinit var activityTaskManager: ActivityTaskManager
+
+    private lateinit var biometricStatusRepository: FakeBiometricStatusRepository
+    private lateinit var underTest: BiometricStatusInteractorImpl
+
+    private val testScope = TestScope(StandardTestDispatcher())
+
+    @Before
+    fun setup() {
+        biometricStatusRepository = FakeBiometricStatusRepository()
+        underTest = BiometricStatusInteractorImpl(activityTaskManager, biometricStatusRepository)
+    }
+
+    @Test
+    fun updatesSfpsAuthenticationReason_whenBiometricPromptAuthenticationStarted() =
+        testScope.runTest {
+            val sfpsAuthenticationReason by collectLastValue(underTest.sfpsAuthenticationReason)
+            runCurrent()
+
+            assertThat(sfpsAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.BiometricPromptAuthentication
+            )
+            assertThat(sfpsAuthenticationReason)
+                .isEqualTo(AuthenticationReason.BiometricPromptAuthentication)
+        }
+
+    @Test
+    fun doesNotUpdateSfpsAuthenticationReason_whenDeviceEntryAuthenticationStarted() =
+        testScope.runTest {
+            val sfpsAuthenticationReason by collectLastValue(underTest.sfpsAuthenticationReason)
+            runCurrent()
+
+            assertThat(sfpsAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.DeviceEntryAuthentication
+            )
+            assertThat(sfpsAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+        }
+
+    @Test
+    fun updatesSfpsAuthenticationReason_whenOtherAuthenticationStarted() =
+        testScope.runTest {
+            val sfpsAuthenticationReason by collectLastValue(underTest.sfpsAuthenticationReason)
+            runCurrent()
+
+            assertThat(sfpsAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.OtherAuthentication
+            )
+            assertThat(sfpsAuthenticationReason).isEqualTo(AuthenticationReason.OtherAuthentication)
+        }
+
+    @Test
+    fun doesNotUpdateSfpsAuthenticationReason_whenOtherSettingsAuthenticationStarted() =
+        testScope.runTest {
+            val sfpsAuthenticationReason by collectLastValue(underTest.sfpsAuthenticationReason)
+            runCurrent()
+
+            assertThat(sfpsAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+
+            `when`(activityTaskManager.getTasks(Mockito.anyInt()))
+                .thenReturn(listOf(fpSettingsTask()))
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.SettingsAuthentication(SettingsOperations.OTHER)
+            )
+            assertThat(sfpsAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+        }
+
+    @Test
+    fun updatesSfpsAuthenticationReason_whenEnrollmentAuthenticationStarted() =
+        testScope.runTest {
+            val sfpsAuthenticationReason by collectLastValue(underTest.sfpsAuthenticationReason)
+            runCurrent()
+
+            assertThat(sfpsAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.SettingsAuthentication(SettingsOperations.ENROLL_FIND_SENSOR)
+            )
+            assertThat(sfpsAuthenticationReason)
+                .isEqualTo(
+                    AuthenticationReason.SettingsAuthentication(
+                        SettingsOperations.ENROLL_FIND_SENSOR
+                    )
+                )
+
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.SettingsAuthentication(SettingsOperations.ENROLL_ENROLLING)
+            )
+            assertThat(sfpsAuthenticationReason)
+                .isEqualTo(
+                    AuthenticationReason.SettingsAuthentication(SettingsOperations.ENROLL_ENROLLING)
+                )
+        }
+
+    @Test
+    fun updatesFingerprintAuthenticationReason_whenAuthenticationStopped() =
+        testScope.runTest {
+            val sfpsAuthenticationReason by collectLastValue(underTest.sfpsAuthenticationReason)
+            runCurrent()
+
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.BiometricPromptAuthentication
+            )
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.NotRunning
+            )
+            assertThat(sfpsAuthenticationReason).isEqualTo(AuthenticationReason.NotRunning)
+        }
+}
+
+private fun fpSettingsTask() = settingsTask(".biometrics.fingerprint.FingerprintSettings")
+
+private fun settingsTask(cls: String) =
+    ActivityManager.RunningTaskInfo().apply {
+        topActivity = ComponentName.createRelative("com.android.settings", cls)
+    }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b4ae00ddd608c75ba4d78ce4763c3e71f705047a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -0,0 +1,484 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.binder
+
+import android.animation.Animator
+import android.app.ActivityTaskManager
+import android.graphics.Rect
+import android.hardware.biometrics.SensorLocationInternal
+import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManagerGlobal
+import android.os.Handler
+import android.testing.TestableLooper
+import android.view.Display
+import android.view.DisplayInfo
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewPropertyAnimator
+import android.view.WindowInsets
+import android.view.WindowManager
+import android.view.WindowMetrics
+import androidx.test.filters.SmallTest
+import com.airbnb.lottie.LottieAnimationView
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider
+import com.android.systemui.biometrics.FpsUnlockTracker
+import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository
+import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
+import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
+import com.android.systemui.biometrics.shared.model.DisplayRotation
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerView
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.display.data.repository.FakeDisplayRepository
+import com.android.systemui.dump.logcatLogBuffer
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
+import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel
+import com.android.systemui.log.SideFpsLogger
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.res.R
+import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.unfold.compat.ScreenSizeFoldProvider
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.any
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class SideFpsOverlayViewBinderTest : SysuiTestCase() {
+    @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
+    @Mock private lateinit var activityTaskManager: ActivityTaskManager
+    @Mock private lateinit var displayManager: DisplayManager
+    @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor
+    @Mock
+    private lateinit var fingerprintInteractiveToAuthProvider: FingerprintInteractiveToAuthProvider
+    @Mock private lateinit var fpsUnlockTracker: FpsUnlockTracker
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var layoutInflater: LayoutInflater
+    @Mock private lateinit var screenSizeFoldProvider: ScreenSizeFoldProvider
+    @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor
+    @Mock private lateinit var sideFpsView: View
+    @Mock private lateinit var windowManager: WindowManager
+
+    private val contextDisplayInfo = DisplayInfo()
+
+    private val bouncerRepository = FakeKeyguardBouncerRepository()
+    private val biometricSettingsRepository = FakeBiometricSettingsRepository()
+    private val biometricStatusRepository = FakeBiometricStatusRepository()
+    private val deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
+    private val displayRepository = FakeDisplayRepository()
+    private val displayStateRepository = FakeDisplayStateRepository()
+    private val fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+
+    private lateinit var underTest: SideFpsOverlayViewBinder
+
+    private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
+    private lateinit var biometricStatusInteractor: BiometricStatusInteractor
+    private lateinit var deviceEntrySideFpsOverlayInteractor: DeviceEntrySideFpsOverlayInteractor
+    private lateinit var displayStateInteractor: DisplayStateInteractorImpl
+    private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
+    private lateinit var sfpsSensorInteractor: SideFpsSensorInteractor
+
+    private lateinit var sideFpsProgressBarViewModel: SideFpsProgressBarViewModel
+
+    private lateinit var viewModel: SideFpsOverlayViewModel
+
+    private var displayWidth: Int = 0
+    private var displayHeight: Int = 0
+    private var boundsWidth: Int = 0
+    private var boundsHeight: Int = 0
+
+    private lateinit var deviceConfig: DeviceConfig
+    private lateinit var sensorLocation: SensorLocationInternal
+
+    private val testScope = TestScope(StandardTestDispatcher())
+    private val fakeExecutor = FakeExecutor(FakeSystemClock())
+
+    enum class DeviceConfig {
+        X_ALIGNED,
+        Y_ALIGNED,
+    }
+
+    @Before
+    fun setup() {
+        mSetFlagsRule.enableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
+
+        allowTestableLooperAsMainThread() // repeatWhenAttached requires the main thread
+
+        mContext = spy(mContext)
+
+        val resources = mContext.resources
+        whenever(mContext.display)
+            .thenReturn(
+                Display(mock(DisplayManagerGlobal::class.java), 1, contextDisplayInfo, resources)
+            )
+
+        alternateBouncerInteractor =
+            AlternateBouncerInteractor(
+                mock(StatusBarStateController::class.java),
+                mock(KeyguardStateController::class.java),
+                bouncerRepository,
+                fingerprintPropertyRepository,
+                biometricSettingsRepository,
+                FakeSystemClock(),
+                keyguardUpdateMonitor,
+                testScope.backgroundScope,
+            )
+
+        biometricStatusInteractor =
+            BiometricStatusInteractorImpl(activityTaskManager, biometricStatusRepository)
+
+        displayStateInteractor =
+            DisplayStateInteractorImpl(
+                testScope.backgroundScope,
+                mContext,
+                fakeExecutor,
+                displayStateRepository,
+                displayRepository,
+            )
+        displayStateInteractor.setScreenSizeFoldProvider(screenSizeFoldProvider)
+
+        primaryBouncerInteractor =
+            PrimaryBouncerInteractor(
+                bouncerRepository,
+                mock(BouncerView::class.java),
+                mock(Handler::class.java),
+                mock(KeyguardStateController::class.java),
+                mock(KeyguardSecurityModel::class.java),
+                mock(PrimaryBouncerCallbackInteractor::class.java),
+                mock(FalsingCollector::class.java),
+                mock(DismissCallbackRegistry::class.java),
+                mContext,
+                keyguardUpdateMonitor,
+                FakeTrustRepository(),
+                testScope.backgroundScope,
+                selectedUserInteractor,
+                faceAuthInteractor
+            )
+
+        deviceEntrySideFpsOverlayInteractor =
+            DeviceEntrySideFpsOverlayInteractor(
+                mContext,
+                deviceEntryFingerprintAuthRepository,
+                primaryBouncerInteractor,
+                alternateBouncerInteractor,
+                keyguardUpdateMonitor
+            )
+
+        whenever(fingerprintInteractiveToAuthProvider.enabledForCurrentUser)
+            .thenReturn(MutableStateFlow(false))
+
+        sfpsSensorInteractor =
+            SideFpsSensorInteractor(
+                mContext,
+                fingerprintPropertyRepository,
+                windowManager,
+                displayStateInteractor,
+                Optional.of(fingerprintInteractiveToAuthProvider),
+                SideFpsLogger(logcatLogBuffer("SfpsLogger"))
+            )
+
+        sideFpsProgressBarViewModel =
+            SideFpsProgressBarViewModel(
+                mContext,
+                deviceEntryFingerprintAuthRepository,
+                sfpsSensorInteractor,
+                displayStateInteractor,
+                testScope.backgroundScope,
+            )
+
+        viewModel =
+            SideFpsOverlayViewModel(
+                mContext,
+                biometricStatusInteractor,
+                deviceEntrySideFpsOverlayInteractor,
+                displayStateInteractor,
+                sfpsSensorInteractor,
+                sideFpsProgressBarViewModel
+            )
+
+        underTest =
+            SideFpsOverlayViewBinder(
+                testScope.backgroundScope,
+                mContext,
+                biometricStatusInteractor,
+                displayStateInteractor,
+                deviceEntrySideFpsOverlayInteractor,
+                fpsUnlockTracker,
+                layoutInflater,
+                sideFpsProgressBarViewModel,
+                sfpsSensorInteractor,
+                windowManager
+            )
+
+        context.addMockSystemService(DisplayManager::class.java, displayManager)
+        context.addMockSystemService(WindowManager::class.java, windowManager)
+
+        `when`(layoutInflater.inflate(R.layout.sidefps_view, null, false)).thenReturn(sideFpsView)
+        `when`(sideFpsView.requireViewById<LottieAnimationView>(eq(R.id.sidefps_animation)))
+            .thenReturn(mock(LottieAnimationView::class.java))
+        with(mock(ViewPropertyAnimator::class.java)) {
+            `when`(sideFpsView.animate()).thenReturn(this)
+            `when`(alpha(Mockito.anyFloat())).thenReturn(this)
+            `when`(setStartDelay(Mockito.anyLong())).thenReturn(this)
+            `when`(setDuration(Mockito.anyLong())).thenReturn(this)
+            `when`(setListener(any())).thenAnswer {
+                (it.arguments[0] as Animator.AnimatorListener).onAnimationEnd(
+                    mock(Animator::class.java)
+                )
+                this
+            }
+        }
+    }
+
+    @Test
+    fun verifyIndicatorNotAdded_whenInRearDisplayMode() {
+        testScope.runTest {
+            setupTestConfiguration(
+                DeviceConfig.X_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = true
+            )
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.NotRunning
+            )
+            sideFpsProgressBarViewModel.setVisible(false)
+            updatePrimaryBouncer(
+                isShowing = true,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
+            runCurrent()
+
+            verify(windowManager, never()).addView(any(), any())
+        }
+    }
+
+    @Test
+    fun verifyIndicatorShowAndHide_onPrimaryBouncerShowAndHide() {
+        testScope.runTest {
+            setupTestConfiguration(
+                DeviceConfig.X_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = false
+            )
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.NotRunning
+            )
+            sideFpsProgressBarViewModel.setVisible(false)
+            // Show primary bouncer
+            updatePrimaryBouncer(
+                isShowing = true,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
+            runCurrent()
+
+            verify(windowManager).addView(any(), any())
+
+            // Hide primary bouncer
+            updatePrimaryBouncer(
+                isShowing = false,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
+            runCurrent()
+
+            verify(windowManager).removeView(any())
+        }
+    }
+
+    @Test
+    fun verifyIndicatorShowAndHide_onAlternateBouncerShowAndHide() {
+        testScope.runTest {
+            setupTestConfiguration(
+                DeviceConfig.X_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = false
+            )
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.NotRunning
+            )
+            sideFpsProgressBarViewModel.setVisible(false)
+            // Show alternate bouncer
+            bouncerRepository.setAlternateVisible(true)
+            runCurrent()
+
+            verify(windowManager).addView(any(), any())
+
+            // Hide alternate bouncer
+            bouncerRepository.setAlternateVisible(false)
+            runCurrent()
+
+            verify(windowManager).removeView(any())
+        }
+    }
+
+    @Test
+    fun verifyIndicatorShownAndHidden_onSystemServerAuthenticationStartedAndStopped() {
+        testScope.runTest {
+            setupTestConfiguration(
+                DeviceConfig.X_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = false
+            )
+            sideFpsProgressBarViewModel.setVisible(false)
+            updatePrimaryBouncer(
+                isShowing = false,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
+            // System server authentication started
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.BiometricPromptAuthentication
+            )
+            runCurrent()
+
+            verify(windowManager).addView(any(), any())
+
+            // System server authentication stopped
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.NotRunning
+            )
+            runCurrent()
+
+            verify(windowManager).removeView(any())
+        }
+    }
+
+    private fun updatePrimaryBouncer(
+        isShowing: Boolean,
+        isAnimatingAway: Boolean,
+        fpsDetectionRunning: Boolean,
+        isUnlockingWithFpAllowed: Boolean,
+    ) {
+        bouncerRepository.setPrimaryShow(isShowing)
+        bouncerRepository.setPrimaryStartingToHide(false)
+        val primaryStartDisappearAnimation = if (isAnimatingAway) Runnable {} else null
+        bouncerRepository.setPrimaryStartDisappearAnimation(primaryStartDisappearAnimation)
+
+        whenever(keyguardUpdateMonitor.isFingerprintDetectionRunning)
+            .thenReturn(fpsDetectionRunning)
+        whenever(keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
+            .thenReturn(isUnlockingWithFpAllowed)
+        mContext.orCreateTestableResources.addOverride(
+            R.bool.config_show_sidefps_hint_on_bouncer,
+            true
+        )
+    }
+
+    private suspend fun TestScope.setupTestConfiguration(
+        deviceConfig: DeviceConfig,
+        rotation: DisplayRotation = DisplayRotation.ROTATION_0,
+        isInRearDisplayMode: Boolean,
+    ) {
+        this@SideFpsOverlayViewBinderTest.deviceConfig = deviceConfig
+
+        when (deviceConfig) {
+            DeviceConfig.X_ALIGNED -> {
+                displayWidth = 3000
+                displayHeight = 1500
+                boundsWidth = 200
+                boundsHeight = 100
+                sensorLocation = SensorLocationInternal("", 2500, 0, boundsWidth / 2)
+            }
+            DeviceConfig.Y_ALIGNED -> {
+                displayWidth = 2500
+                displayHeight = 2000
+                boundsWidth = 100
+                boundsHeight = 200
+                sensorLocation = SensorLocationInternal("", displayWidth, 300, boundsHeight / 2)
+            }
+        }
+
+        whenever(windowManager.maximumWindowMetrics)
+            .thenReturn(
+                WindowMetrics(
+                    Rect(0, 0, displayWidth, displayHeight),
+                    mock(WindowInsets::class.java),
+                )
+            )
+
+        contextDisplayInfo.uniqueId = DISPLAY_ID
+
+        fingerprintPropertyRepository.setProperties(
+            sensorId = 1,
+            strength = SensorStrength.STRONG,
+            sensorType = FingerprintSensorType.POWER_BUTTON,
+            sensorLocations = mapOf(DISPLAY_ID to sensorLocation)
+        )
+
+        displayStateRepository.setIsInRearDisplayMode(isInRearDisplayMode)
+        displayStateRepository.setCurrentRotation(rotation)
+        displayRepository.emitDisplayChangeEvent(0)
+        underTest.start()
+        runCurrent()
+    }
+
+    companion object {
+        private const val DISPLAY_ID = "displayId"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2267bdcafad840051e8065c01f894a5e1fc6270e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
@@ -0,0 +1,576 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.viewmodel
+
+import android.app.ActivityTaskManager
+import android.content.res.Configuration.UI_MODE_NIGHT_NO
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import android.graphics.Color
+import android.graphics.Rect
+import android.hardware.biometrics.SensorLocationInternal
+import android.hardware.display.DisplayManagerGlobal
+import android.os.Handler
+import android.view.Display
+import android.view.DisplayInfo
+import android.view.WindowInsets
+import android.view.WindowManager
+import android.view.WindowMetrics
+import androidx.test.filters.SmallTest
+import com.airbnb.lottie.model.KeyPath
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.settingslib.Utils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider
+import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository
+import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
+import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
+import com.android.systemui.biometrics.shared.model.DisplayRotation
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.biometrics.shared.model.LottieCallback
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerView
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.display.data.repository.FakeDisplayRepository
+import com.android.systemui.dump.logcatLogBuffer
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
+import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel
+import com.android.systemui.log.SideFpsLogger
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.res.R
+import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.unfold.compat.ScreenSizeFoldProvider
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class SideFpsOverlayViewModelTest : SysuiTestCase() {
+    @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var activityTaskManager: ActivityTaskManager
+    @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor
+    @Mock
+    private lateinit var fingerprintInteractiveToAuthProvider: FingerprintInteractiveToAuthProvider
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var screenSizeFoldProvider: ScreenSizeFoldProvider
+    @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor
+    @Mock private lateinit var windowManager: WindowManager
+
+    private val contextDisplayInfo = DisplayInfo()
+
+    private val bouncerRepository = FakeKeyguardBouncerRepository()
+    private val biometricSettingsRepository = FakeBiometricSettingsRepository()
+    private val biometricStatusRepository = FakeBiometricStatusRepository()
+    private val deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
+    private val displayRepository = FakeDisplayRepository()
+    private val displayStateRepository = FakeDisplayStateRepository()
+    private val fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+
+    private val indicatorColor =
+        Utils.getColorAttrDefaultColor(
+            context,
+            com.android.internal.R.attr.materialColorPrimaryFixed
+        )
+    private val outerRimColor =
+        Utils.getColorAttrDefaultColor(
+            context,
+            com.android.internal.R.attr.materialColorPrimaryFixedDim
+        )
+    private val chevronFill =
+        Utils.getColorAttrDefaultColor(
+            context,
+            com.android.internal.R.attr.materialColorOnPrimaryFixed
+        )
+    private val color_blue400 =
+        context.getColor(com.android.settingslib.color.R.color.settingslib_color_blue400)
+
+    private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
+    private lateinit var biometricStatusInteractor: BiometricStatusInteractor
+    private lateinit var deviceEntrySideFpsOverlayInteractor: DeviceEntrySideFpsOverlayInteractor
+    private lateinit var displayStateInteractor: DisplayStateInteractorImpl
+    private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
+    private lateinit var sfpsSensorInteractor: SideFpsSensorInteractor
+
+    private lateinit var sideFpsProgressBarViewModel: SideFpsProgressBarViewModel
+
+    private lateinit var underTest: SideFpsOverlayViewModel
+
+    private var displayWidth: Int = 0
+    private var displayHeight: Int = 0
+    private var boundsWidth: Int = 0
+    private var boundsHeight: Int = 0
+
+    private lateinit var deviceConfig: DeviceConfig
+    private lateinit var sensorLocation: SensorLocationInternal
+
+    private val testScope = TestScope(StandardTestDispatcher())
+    private val fakeExecutor = FakeExecutor(FakeSystemClock())
+
+    enum class DeviceConfig {
+        X_ALIGNED,
+        Y_ALIGNED,
+    }
+
+    @Before
+    fun setup() {
+        mSetFlagsRule.enableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
+
+        mContext = spy(mContext)
+
+        val resources = mContext.resources
+        whenever(mContext.display)
+            .thenReturn(
+                Display(mock(DisplayManagerGlobal::class.java), 1, contextDisplayInfo, resources)
+            )
+
+        alternateBouncerInteractor =
+            AlternateBouncerInteractor(
+                mock(StatusBarStateController::class.java),
+                mock(KeyguardStateController::class.java),
+                bouncerRepository,
+                fingerprintPropertyRepository,
+                biometricSettingsRepository,
+                FakeSystemClock(),
+                keyguardUpdateMonitor,
+                testScope.backgroundScope,
+            )
+
+        biometricStatusInteractor =
+            BiometricStatusInteractorImpl(activityTaskManager, biometricStatusRepository)
+
+        displayStateInteractor =
+            DisplayStateInteractorImpl(
+                testScope.backgroundScope,
+                mContext,
+                fakeExecutor,
+                displayStateRepository,
+                displayRepository,
+            )
+        displayStateInteractor.setScreenSizeFoldProvider(screenSizeFoldProvider)
+
+        primaryBouncerInteractor =
+            PrimaryBouncerInteractor(
+                bouncerRepository,
+                mock(BouncerView::class.java),
+                mock(Handler::class.java),
+                mock(KeyguardStateController::class.java),
+                mock(KeyguardSecurityModel::class.java),
+                mock(PrimaryBouncerCallbackInteractor::class.java),
+                mock(FalsingCollector::class.java),
+                mock(DismissCallbackRegistry::class.java),
+                mContext,
+                keyguardUpdateMonitor,
+                FakeTrustRepository(),
+                testScope.backgroundScope,
+                selectedUserInteractor,
+                faceAuthInteractor
+            )
+
+        deviceEntrySideFpsOverlayInteractor =
+            DeviceEntrySideFpsOverlayInteractor(
+                mContext,
+                deviceEntryFingerprintAuthRepository,
+                primaryBouncerInteractor,
+                alternateBouncerInteractor,
+                keyguardUpdateMonitor
+            )
+
+        whenever(fingerprintInteractiveToAuthProvider.enabledForCurrentUser)
+            .thenReturn(MutableStateFlow(false))
+
+        sfpsSensorInteractor =
+            SideFpsSensorInteractor(
+                mContext,
+                fingerprintPropertyRepository,
+                windowManager,
+                displayStateInteractor,
+                Optional.of(fingerprintInteractiveToAuthProvider),
+                SideFpsLogger(logcatLogBuffer("SfpsLogger"))
+            )
+
+        sideFpsProgressBarViewModel =
+            SideFpsProgressBarViewModel(
+                mContext,
+                deviceEntryFingerprintAuthRepository,
+                sfpsSensorInteractor,
+                displayStateInteractor,
+                testScope.backgroundScope,
+            )
+
+        underTest =
+            SideFpsOverlayViewModel(
+                mContext,
+                biometricStatusInteractor,
+                deviceEntrySideFpsOverlayInteractor,
+                displayStateInteractor,
+                sfpsSensorInteractor,
+                sideFpsProgressBarViewModel,
+            )
+    }
+
+    @Test
+    fun updatesOverlayViewProperties_onDisplayRotationChange_xAlignedSensor() {
+        testScope.runTest {
+            setupTestConfiguration(
+                DeviceConfig.X_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = false
+            )
+
+            val overlayViewProperties by collectLastValue(underTest.overlayViewProperties)
+
+            runCurrent()
+
+            assertThat(overlayViewProperties?.indicatorAsset).isEqualTo(R.raw.sfps_pulse_landscape)
+            assertThat(overlayViewProperties?.overlayViewRotation).isEqualTo(0f)
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
+
+            assertThat(overlayViewProperties?.indicatorAsset).isEqualTo(R.raw.sfps_pulse)
+            assertThat(overlayViewProperties?.overlayViewRotation).isEqualTo(180f)
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180)
+
+            assertThat(overlayViewProperties?.indicatorAsset).isEqualTo(R.raw.sfps_pulse_landscape)
+            assertThat(overlayViewProperties?.overlayViewRotation).isEqualTo(180f)
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
+
+            assertThat(overlayViewProperties?.indicatorAsset).isEqualTo(R.raw.sfps_pulse)
+            assertThat(overlayViewProperties?.overlayViewRotation).isEqualTo(0f)
+        }
+    }
+
+    @Test
+    fun updatesOverlayViewProperties_onDisplayRotationChange_yAlignedSensor() {
+        testScope.runTest {
+            setupTestConfiguration(
+                DeviceConfig.Y_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = false
+            )
+
+            val overlayViewProperties by collectLastValue(underTest.overlayViewProperties)
+
+            runCurrent()
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
+            assertThat(overlayViewProperties?.indicatorAsset).isEqualTo(R.raw.sfps_pulse)
+            assertThat(overlayViewProperties?.overlayViewRotation).isEqualTo(0f)
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
+
+            assertThat(overlayViewProperties?.indicatorAsset).isEqualTo(R.raw.sfps_pulse_landscape)
+            assertThat(overlayViewProperties?.overlayViewRotation).isEqualTo(0f)
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180)
+            assertThat(overlayViewProperties?.indicatorAsset).isEqualTo(R.raw.sfps_pulse)
+            assertThat(overlayViewProperties?.overlayViewRotation).isEqualTo(180f)
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
+
+            assertThat(overlayViewProperties?.indicatorAsset).isEqualTo(R.raw.sfps_pulse_landscape)
+            assertThat(overlayViewProperties?.overlayViewRotation).isEqualTo(180f)
+        }
+    }
+
+    @Test
+    fun updatesOverlayViewParams_onDisplayRotationChange_xAlignedSensor() {
+        testScope.runTest {
+            setupTestConfiguration(
+                DeviceConfig.X_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = false
+            )
+
+            val overlayViewParams by collectLastValue(underTest.overlayViewParams)
+
+            underTest.setLottieBounds(Rect(0, 0, boundsWidth, boundsHeight))
+            runCurrent()
+
+            assertThat(overlayViewParams).isNotNull()
+            assertThat(overlayViewParams!!.x).isEqualTo(sensorLocation.sensorLocationX)
+            assertThat(overlayViewParams!!.y).isEqualTo(0)
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
+            assertThat(overlayViewParams).isNotNull()
+            assertThat(overlayViewParams!!.x).isEqualTo(0)
+            assertThat(overlayViewParams!!.y)
+                .isEqualTo(
+                    displayHeight - sensorLocation.sensorLocationX - sensorLocation.sensorRadius * 2
+                )
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180)
+            assertThat(overlayViewParams).isNotNull()
+            assertThat(overlayViewParams!!.x)
+                .isEqualTo(
+                    displayWidth - sensorLocation.sensorLocationX - sensorLocation.sensorRadius * 2
+                )
+            assertThat(overlayViewParams!!.y).isEqualTo(displayHeight - boundsHeight)
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
+            assertThat(overlayViewParams).isNotNull()
+            assertThat(overlayViewParams!!.x).isEqualTo(displayWidth - boundsWidth)
+            assertThat(overlayViewParams!!.y).isEqualTo(sensorLocation.sensorLocationX)
+        }
+    }
+
+    @Test
+    fun updatesOverlayViewParams_onDisplayRotationChange_yAlignedSensor() {
+        testScope.runTest {
+            setupTestConfiguration(
+                DeviceConfig.Y_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = false
+            )
+
+            val overlayViewParams by collectLastValue(underTest.overlayViewParams)
+
+            underTest.setLottieBounds(Rect(0, 0, boundsWidth, boundsHeight))
+            runCurrent()
+
+            assertThat(overlayViewParams).isNotNull()
+            assertThat(overlayViewParams!!.x).isEqualTo(displayWidth - boundsWidth)
+            assertThat(overlayViewParams!!.y).isEqualTo(sensorLocation.sensorLocationY)
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
+            assertThat(overlayViewParams).isNotNull()
+            assertThat(overlayViewParams!!.x).isEqualTo(sensorLocation.sensorLocationY)
+            assertThat(overlayViewParams!!.y).isEqualTo(0)
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180)
+            assertThat(overlayViewParams).isNotNull()
+            assertThat(overlayViewParams!!.x).isEqualTo(0)
+            assertThat(overlayViewParams!!.y)
+                .isEqualTo(
+                    displayHeight - sensorLocation.sensorLocationY - sensorLocation.sensorRadius * 2
+                )
+
+            displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
+            assertThat(overlayViewParams).isNotNull()
+            assertThat(overlayViewParams!!.x)
+                .isEqualTo(
+                    displayWidth - sensorLocation.sensorLocationY - sensorLocation.sensorRadius * 2
+                )
+            assertThat(overlayViewParams!!.y).isEqualTo(displayHeight - boundsHeight)
+        }
+    }
+
+    @Test
+    fun updatesLottieCallbacks_onShowIndicatorForDeviceEntry() {
+        testScope.runTest {
+            val lottieCallbacks by collectLastValue(underTest.lottieCallbacks)
+
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.NotRunning
+            )
+            sideFpsProgressBarViewModel.setVisible(false)
+
+            updatePrimaryBouncer(
+                isShowing = true,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
+            runCurrent()
+
+            assertThat(lottieCallbacks)
+                .contains(LottieCallback(KeyPath(".blue600", "**"), indicatorColor))
+            assertThat(lottieCallbacks)
+                .contains(LottieCallback(KeyPath(".blue400", "**"), outerRimColor))
+            assertThat(lottieCallbacks)
+                .contains(LottieCallback(KeyPath(".black", "**"), chevronFill))
+        }
+    }
+
+    @Test
+    fun updatesLottieCallbacks_onShowIndicatorForSystemServer_inDarkMode() {
+        testScope.runTest {
+            val lottieCallbacks by collectLastValue(underTest.lottieCallbacks)
+            setDarkMode(true)
+
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.BiometricPromptAuthentication
+            )
+            sideFpsProgressBarViewModel.setVisible(false)
+
+            updatePrimaryBouncer(
+                isShowing = false,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
+            runCurrent()
+
+            assertThat(lottieCallbacks)
+                .contains(LottieCallback(KeyPath(".blue600", "**"), color_blue400))
+            assertThat(lottieCallbacks)
+                .contains(LottieCallback(KeyPath(".blue400", "**"), color_blue400))
+        }
+    }
+
+    @Test
+    fun updatesLottieCallbacks_onShowIndicatorForSystemServer_inLightMode() {
+        testScope.runTest {
+            val lottieCallbacks by collectLastValue(underTest.lottieCallbacks)
+            setDarkMode(false)
+
+            biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.BiometricPromptAuthentication
+            )
+            sideFpsProgressBarViewModel.setVisible(false)
+
+            updatePrimaryBouncer(
+                isShowing = false,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
+            runCurrent()
+
+            assertThat(lottieCallbacks)
+                .contains(LottieCallback(KeyPath(".black", "**"), Color.WHITE))
+            assertThat(lottieCallbacks)
+                .contains(LottieCallback(KeyPath(".blue600", "**"), color_blue400))
+            assertThat(lottieCallbacks)
+                .contains(LottieCallback(KeyPath(".blue400", "**"), color_blue400))
+        }
+    }
+
+    private fun setDarkMode(inDarkMode: Boolean) {
+        val uiMode =
+            if (inDarkMode) {
+                UI_MODE_NIGHT_YES
+            } else {
+                UI_MODE_NIGHT_NO
+            }
+
+        mContext.resources.configuration.uiMode = uiMode
+    }
+
+    private fun updatePrimaryBouncer(
+        isShowing: Boolean,
+        isAnimatingAway: Boolean,
+        fpsDetectionRunning: Boolean,
+        isUnlockingWithFpAllowed: Boolean,
+    ) {
+        bouncerRepository.setPrimaryShow(isShowing)
+        bouncerRepository.setPrimaryStartingToHide(false)
+        val primaryStartDisappearAnimation = if (isAnimatingAway) Runnable {} else null
+        bouncerRepository.setPrimaryStartDisappearAnimation(primaryStartDisappearAnimation)
+
+        whenever(keyguardUpdateMonitor.isFingerprintDetectionRunning)
+            .thenReturn(fpsDetectionRunning)
+        whenever(keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
+            .thenReturn(isUnlockingWithFpAllowed)
+        mContext.orCreateTestableResources.addOverride(
+            R.bool.config_show_sidefps_hint_on_bouncer,
+            true
+        )
+    }
+
+    private suspend fun TestScope.setupTestConfiguration(
+        deviceConfig: DeviceConfig,
+        rotation: DisplayRotation = DisplayRotation.ROTATION_0,
+        isInRearDisplayMode: Boolean,
+    ) {
+        this@SideFpsOverlayViewModelTest.deviceConfig = deviceConfig
+
+        when (deviceConfig) {
+            DeviceConfig.X_ALIGNED -> {
+                displayWidth = 3000
+                displayHeight = 1500
+                boundsWidth = 200
+                boundsHeight = 100
+                sensorLocation = SensorLocationInternal("", 2500, 0, boundsWidth / 2)
+            }
+            DeviceConfig.Y_ALIGNED -> {
+                displayWidth = 2500
+                displayHeight = 2000
+                boundsWidth = 100
+                boundsHeight = 200
+                sensorLocation = SensorLocationInternal("", displayWidth, 300, boundsHeight / 2)
+            }
+        }
+
+        whenever(windowManager.maximumWindowMetrics)
+            .thenReturn(
+                WindowMetrics(
+                    Rect(0, 0, displayWidth, displayHeight),
+                    mock(WindowInsets::class.java),
+                )
+            )
+
+        contextDisplayInfo.uniqueId = DISPLAY_ID
+
+        fingerprintPropertyRepository.setProperties(
+            sensorId = 1,
+            strength = SensorStrength.STRONG,
+            sensorType = FingerprintSensorType.POWER_BUTTON,
+            sensorLocations = mapOf(DISPLAY_ID to sensorLocation)
+        )
+
+        displayStateRepository.setIsInRearDisplayMode(isInRearDisplayMode)
+
+        displayStateRepository.setCurrentRotation(rotation)
+
+        displayRepository.emitDisplayChangeEvent(0)
+        runCurrent()
+    }
+
+    companion object {
+        private const val DISPLAY_ID = "displayId"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
index 37a093e4c23f472319862c3d8a238f4c7cc84a2b..dacf23a5a567b625b08c3020e8b61b75d91f513b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -37,6 +37,7 @@ import com.android.systemui.keyguard.data.repository.FakeTrustRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
+import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.any
@@ -342,6 +343,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
         assertThat(underTest.willDismissWithAction()).isFalse()
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Test
     fun testSideFpsVisibility() {
         updateSideFpsVisibilityParameters(
@@ -355,6 +357,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
         verify(repository).setSideFpsShowing(true)
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Test
     fun testSideFpsVisibility_notVisible() {
         updateSideFpsVisibilityParameters(
@@ -368,6 +371,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
         verify(repository).setSideFpsShowing(false)
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Test
     fun testSideFpsVisibility_sfpsNotEnabled() {
         updateSideFpsVisibilityParameters(
@@ -381,6 +385,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
         verify(repository).setSideFpsShowing(false)
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Test
     fun testSideFpsVisibility_fpsDetectionNotRunning() {
         updateSideFpsVisibilityParameters(
@@ -394,6 +399,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
         verify(repository).setSideFpsShowing(false)
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Test
     fun testSideFpsVisibility_UnlockingWithFpNotAllowed() {
         updateSideFpsVisibilityParameters(
@@ -407,6 +413,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
         verify(repository).setSideFpsShowing(false)
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Test
     fun testSideFpsVisibility_AnimatingAway() {
         updateSideFpsVisibilityParameters(
@@ -492,6 +499,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
         isUnlockingWithFpAllowed: Boolean,
         isAnimatingAway: Boolean
     ) {
+        mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         whenever(repository.primaryBouncerShow.value).thenReturn(isVisible)
         resources.addOverride(R.bool.config_show_sidefps_hint_on_bouncer, sfpsEnabled)
         whenever(keyguardUpdateMonitor.isFingerprintDetectionRunning)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..70d3f81de35f03f8a9c44b53499c472c56309837
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.os.Handler
+import android.platform.test.annotations.RequiresFlagsEnabled
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerView
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.res.R
+import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RequiresFlagsEnabled(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
+@SmallTest
+@RunWith(JUnit4::class)
+class DeviceEntrySideFpsOverlayInteractorTest : SysuiTestCase() {
+    @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
+
+    private val bouncerRepository = FakeKeyguardBouncerRepository()
+    private val biometricSettingsRepository = FakeBiometricSettingsRepository()
+
+    private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
+    private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
+
+    private lateinit var underTest: DeviceEntrySideFpsOverlayInteractor
+
+    private val testScope = TestScope(StandardTestDispatcher())
+
+    @Before
+    fun setup() {
+        primaryBouncerInteractor =
+            PrimaryBouncerInteractor(
+                bouncerRepository,
+                mock(BouncerView::class.java),
+                mock(Handler::class.java),
+                mock(KeyguardStateController::class.java),
+                mock(KeyguardSecurityModel::class.java),
+                mock(PrimaryBouncerCallbackInteractor::class.java),
+                mock(FalsingCollector::class.java),
+                mock(DismissCallbackRegistry::class.java),
+                mContext,
+                keyguardUpdateMonitor,
+                FakeTrustRepository(),
+                testScope.backgroundScope,
+                mSelectedUserInteractor,
+                faceAuthInteractor
+            )
+        alternateBouncerInteractor =
+            AlternateBouncerInteractor(
+                mock(StatusBarStateController::class.java),
+                mock(KeyguardStateController::class.java),
+                bouncerRepository,
+                FakeFingerprintPropertyRepository(),
+                biometricSettingsRepository,
+                FakeSystemClock(),
+                keyguardUpdateMonitor,
+                testScope.backgroundScope,
+            )
+        underTest =
+            DeviceEntrySideFpsOverlayInteractor(
+                mContext,
+                FakeDeviceEntryFingerprintAuthRepository(),
+                primaryBouncerInteractor,
+                alternateBouncerInteractor,
+                keyguardUpdateMonitor
+            )
+    }
+
+    @Test
+    fun updatesShowIndicatorForDeviceEntry_onPrimaryBouncerShowing() =
+        testScope.runTest {
+            val showIndicatorForDeviceEntry by
+                collectLastValue(underTest.showIndicatorForDeviceEntry)
+            runCurrent()
+
+            updatePrimaryBouncer(
+                isShowing = true,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
+            assertThat(showIndicatorForDeviceEntry).isEqualTo(true)
+        }
+
+    @Test
+    fun updatesShowIndicatorForDeviceEntry_onPrimaryBouncerHidden() =
+        testScope.runTest {
+            val showIndicatorForDeviceEntry by
+                collectLastValue(underTest.showIndicatorForDeviceEntry)
+            runCurrent()
+
+            updatePrimaryBouncer(
+                isShowing = false,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
+            assertThat(showIndicatorForDeviceEntry).isEqualTo(false)
+        }
+
+    @Test
+    fun updatesShowIndicatorForDeviceEntry_fromPrimaryBouncer_whenFpsDetectionNotRunning() {
+        testScope.runTest {
+            val showIndicatorForDeviceEntry by
+                collectLastValue(underTest.showIndicatorForDeviceEntry)
+            runCurrent()
+
+            updatePrimaryBouncer(
+                isShowing = true,
+                isAnimatingAway = false,
+                fpsDetectionRunning = false,
+                isUnlockingWithFpAllowed = true
+            )
+            assertThat(showIndicatorForDeviceEntry).isEqualTo(false)
+        }
+    }
+
+    @Test
+    fun updatesShowIndicatorForDeviceEntry_fromPrimaryBouncer_onUnlockingWithFpDisallowed() {
+        testScope.runTest {
+            val showIndicatorForDeviceEntry by
+                collectLastValue(underTest.showIndicatorForDeviceEntry)
+            runCurrent()
+
+            updatePrimaryBouncer(
+                isShowing = true,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = false
+            )
+            assertThat(showIndicatorForDeviceEntry).isEqualTo(false)
+        }
+    }
+
+    @Test
+    fun updatesShowIndicatorForDeviceEntry_onPrimaryBouncerAnimatingAway() {
+        testScope.runTest {
+            val showIndicatorForDeviceEntry by
+                collectLastValue(underTest.showIndicatorForDeviceEntry)
+            runCurrent()
+
+            updatePrimaryBouncer(
+                isShowing = true,
+                isAnimatingAway = true,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
+            assertThat(showIndicatorForDeviceEntry).isEqualTo(false)
+        }
+    }
+
+    @Test
+    fun updatesShowIndicatorForDeviceEntry_onAlternateBouncerRequest() =
+        testScope.runTest {
+            val showIndicatorForDeviceEntry by
+                collectLastValue(underTest.showIndicatorForDeviceEntry)
+            runCurrent()
+
+            bouncerRepository.setAlternateVisible(true)
+            assertThat(showIndicatorForDeviceEntry).isEqualTo(true)
+
+            bouncerRepository.setAlternateVisible(false)
+            assertThat(showIndicatorForDeviceEntry).isEqualTo(false)
+        }
+
+    private fun updatePrimaryBouncer(
+        isShowing: Boolean,
+        isAnimatingAway: Boolean,
+        fpsDetectionRunning: Boolean,
+        isUnlockingWithFpAllowed: Boolean,
+    ) {
+        bouncerRepository.setPrimaryShow(isShowing)
+        bouncerRepository.setPrimaryStartingToHide(false)
+        val primaryStartDisappearAnimation = if (isAnimatingAway) Runnable {} else null
+        bouncerRepository.setPrimaryStartDisappearAnimation(primaryStartDisappearAnimation)
+
+        whenever(keyguardUpdateMonitor.isFingerprintDetectionRunning)
+            .thenReturn(fpsDetectionRunning)
+        whenever(keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
+            .thenReturn(isUnlockingWithFpAllowed)
+        mContext.orCreateTestableResources.addOverride(
+            R.bool.config_show_sidefps_hint_on_bouncer,
+            true
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1c8bd3b58dfe8e7687e1d76ea8ce609d0c75f6e5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeBiometricStatusRepository.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.biometrics.data.repository
+
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeBiometricStatusRepository : BiometricStatusRepository {
+    private val _fingerprintAuthenticationReason =
+        MutableStateFlow<AuthenticationReason>(AuthenticationReason.NotRunning)
+    override val fingerprintAuthenticationReason: StateFlow<AuthenticationReason> =
+        _fingerprintAuthenticationReason.asStateFlow()
+
+    fun setFingerprintAuthenticationReason(reason: AuthenticationReason) {
+        _fingerprintAuthenticationReason.value = reason
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
index f84481c556644aa6a39c683dcd318a0133865b12..ff5179a7042c2f150f82af0ad71da186fdb2f5b9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
@@ -1,5 +1,6 @@
 package com.android.systemui.bouncer.data.repository
 
+import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
 import com.android.systemui.dagger.SysUISingleton
@@ -113,7 +114,9 @@ class FakeKeyguardBouncerRepository @Inject constructor() : KeyguardBouncerRepos
         _isBackButtonEnabled.value = isBackButtonEnabled
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     override fun setSideFpsShowing(isShowing: Boolean) {
+        SideFpsControllerRefactor.assertInLegacyMode()
         _sideFpsShowing.value = isShowing
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
index c9160efb75a472f624d9c2ae5acc3da3009dc622..1d44929a20f0db70bdf6acc79e7d16c30f43d98b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
@@ -54,6 +54,11 @@ class FakeDeviceEntryFingerprintAuthRepository @Inject constructor() :
     private var _authenticationStatus = MutableStateFlow<FingerprintAuthenticationStatus?>(null)
     override val authenticationStatus: Flow<FingerprintAuthenticationStatus>
         get() = _authenticationStatus.filterNotNull()
+
+    private var _shouldUpdateIndicatorVisibility = MutableStateFlow(false)
+    override val shouldUpdateIndicatorVisibility: Flow<Boolean>
+        get() = _shouldUpdateIndicatorVisibility
+
     fun setAuthenticationStatus(status: FingerprintAuthenticationStatus) {
         _authenticationStatus.value = status
     }
diff --git a/services/core/Android.bp b/services/core/Android.bp
index a0ccbf3acf0a67ffc98b0e05a8d134ad5f680565..f5a80d80f2713577d1e5d31c23c3405a10bbdbf5 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -200,6 +200,7 @@ java_library_static {
         "notification_flags_lib",
         "biometrics_flags_lib",
         "am_flags_lib",
+        "com_android_systemui_shared_flags_lib",
         "com_android_wm_shell_flags_lib",
         "com.android.server.utils_aconfig-java",
         "service-jobscheduler-deviceidle.flags-aconfig-java",
diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java
index 0629e6373b6eda08fd0619f8933a5b235828cc62..dafea9a199fdc6a1616efca58b8549b002e8827b 100644
--- a/services/core/java/com/android/server/biometrics/AuthService.java
+++ b/services/core/java/com/android/server/biometrics/AuthService.java
@@ -36,6 +36,7 @@ import android.annotation.Nullable;
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.hardware.biometrics.AuthenticationStateListener;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.ComponentInfoInternal;
@@ -384,6 +385,26 @@ public class AuthService extends SystemService {
             }
         }
 
+        @Override
+        public void registerAuthenticationStateListener(AuthenticationStateListener listener)
+                throws RemoteException {
+            checkInternalPermission();
+            final IFingerprintService fingerprintService = mInjector.getFingerprintService();
+            if (fingerprintService != null) {
+                fingerprintService.registerAuthenticationStateListener(listener);
+            }
+        }
+
+        @Override
+        public void unregisterAuthenticationStateListener(AuthenticationStateListener listener)
+                throws RemoteException {
+            checkInternalPermission();
+            final IFingerprintService fingerprintService = mInjector.getFingerprintService();
+            if (fingerprintService != null) {
+                fingerprintService.unregisterAuthenticationStateListener(listener);
+            }
+        }
+
         @Override
         public void invalidateAuthenticatorIds(int userId, int fromSensorId,
                 IInvalidationCallback callback) throws RemoteException {
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index a47135fd032ff8843d42685ad9ed6ef2ebf39f44..f9568ea9d72bad6ce27c16eae82a365415ebd529 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -27,7 +27,7 @@ import android.hardware.biometrics.AuthenticateOptions;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager;
-import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.biometrics.BiometricRequestConstants;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.security.KeyStore;
@@ -430,19 +430,19 @@ public abstract class AuthenticationClient<T, O extends AuthenticateOptions>
         return mLockoutTracker;
     }
 
-    protected int getShowOverlayReason() {
+    protected int getRequestReason() {
         if (isKeyguard()) {
-            return BiometricOverlayConstants.REASON_AUTH_KEYGUARD;
+            return BiometricRequestConstants.REASON_AUTH_KEYGUARD;
         } else if (isBiometricPrompt()) {
             // BP reason always takes precedent over settings, since callers from within
             // settings can always invoke BP.
-            return BiometricOverlayConstants.REASON_AUTH_BP;
+            return BiometricRequestConstants.REASON_AUTH_BP;
         } else if (isSettings()) {
             // This is pretty much only for FingerprintManager#authenticate usage from
             // FingerprintSettings.
-            return BiometricOverlayConstants.REASON_AUTH_SETTINGS;
+            return BiometricRequestConstants.REASON_AUTH_SETTINGS;
         } else {
-            return BiometricOverlayConstants.REASON_AUTH_OTHER;
+            return BiometricRequestConstants.REASON_AUTH_OTHER;
         }
     }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java
new file mode 100644
index 0000000000000000000000000000000000000000..58635353c7806c90627b8f7fbb96afc9d11730a5
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.biometrics.sensors;
+
+import android.annotation.NonNull;
+import android.hardware.biometrics.AuthenticationStateListener;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Low-level callback interface between BiometricManager and AuthService. Allows core system
+ * services (e.g. SystemUI) to register and unregister listeners for updates about the current
+ * state of biometric authentication.
+ * @hide */
+public class AuthenticationStateListeners implements IBinder.DeathRecipient {
+
+    private static final String TAG = "AuthenticationStateListeners";
+
+    @NonNull
+    private final CopyOnWriteArrayList<AuthenticationStateListener> mAuthenticationStateListeners =
+            new CopyOnWriteArrayList<>();
+
+    /**
+     * Enables clients to register an AuthenticationStateListener for updates about the current
+     * state of biometric authentication.
+     * @param listener listener to register
+     */
+    public void registerAuthenticationStateListener(
+            @NonNull AuthenticationStateListener listener) {
+        mAuthenticationStateListeners.add(listener);
+        try {
+            listener.asBinder().linkToDeath(this, 0 /* flags */);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to link to death", e);
+        }
+    }
+
+    /**
+     * Enables clients to unregister an AuthenticationStateListener.
+     * @param listener listener to register
+     */
+    public void unregisterAuthenticationStateListener(
+            @NonNull AuthenticationStateListener listener) {
+        mAuthenticationStateListeners.remove(listener);
+    }
+
+    /**
+     * Defines behavior in response to authentication starting
+     * @param requestReason reason from [BiometricRequestConstants.RequestReason] for requesting
+     * authentication starting
+     */
+    public void onAuthenticationStarted(int requestReason) {
+        for (AuthenticationStateListener listener: mAuthenticationStateListeners) {
+            try {
+                listener.onAuthenticationStarted(requestReason);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception in notifying listener that authentication "
+                        + "started", e);
+            }
+        }
+    }
+
+    /**
+     * Defines behavior in response to authentication stopping
+     */
+    public void onAuthenticationStopped() {
+        for (AuthenticationStateListener listener: mAuthenticationStateListeners) {
+            try {
+                listener.onAuthenticationStopped();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception in notifying listener that authentication "
+                        + "stopped", e);
+            }
+        }
+    }
+
+    @Override
+    public void binderDied() {
+        // Do nothing, handled below
+    }
+
+    @Override
+    public void binderDied(IBinder who) {
+        Slog.w(TAG, "Callback binder died: " + who);
+        if (mAuthenticationStateListeners.removeIf(listener -> listener.asBinder().equals(who))) {
+            Slog.w(TAG, "Removed dead listener for " + who);
+        } else {
+            Slog.w(TAG, "No dead listeners found");
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
index 483ce75eae98df91f002fed6c931427ac0c6d31a..2c4d30b622addafe49d7e8c99bd8420b190ef772 100644
--- a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
@@ -19,7 +19,7 @@ package com.android.server.biometrics.sensors;
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
-import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.biometrics.BiometricRequestConstants;
 import android.hardware.fingerprint.FingerprintManager;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -135,14 +135,14 @@ public abstract class EnrollClient<T> extends AcquisitionClient<T> implements En
         return true;
     }
 
-    protected int getOverlayReasonFromEnrollReason(@FingerprintManager.EnrollReason int reason) {
+    protected int getRequestReasonFromEnrollReason(@FingerprintManager.EnrollReason int reason) {
         switch (reason) {
             case FingerprintManager.ENROLL_FIND_SENSOR:
-                return BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR;
+                return BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR;
             case FingerprintManager.ENROLL_ENROLL:
-                return BiometricOverlayConstants.REASON_ENROLL_ENROLLING;
+                return BiometricRequestConstants.REASON_ENROLL_ENROLLING;
             default:
-                return BiometricOverlayConstants.REASON_UNKNOWN;
+                return BiometricRequestConstants.REASON_UNKNOWN;
         }
     }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java b/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
index aeb6b6e2a90737b4485496b0d030dfe0aa71dcc8..3d20efc88c77b850601e11039f78fbe33e4a529e 100644
--- a/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
+++ b/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
@@ -16,9 +16,11 @@
 
 package com.android.server.biometrics.sensors;
 
+import static com.android.systemui.shared.Flags.sidefpsControllerRefactor;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.biometrics.BiometricRequestConstants;
 import android.hardware.fingerprint.ISidefpsController;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback;
@@ -42,6 +44,8 @@ public final class SensorOverlays {
     private static final String TAG = "SensorOverlays";
 
     @NonNull private final Optional<IUdfpsOverlayController> mUdfpsOverlayController;
+
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @NonNull private final Optional<ISidefpsController> mSidefpsController;
 
     /**
@@ -57,6 +61,17 @@ public final class SensorOverlays {
         mSidefpsController = Optional.ofNullable(sidefpsController);
     }
 
+    /**
+     * Create an overlay controller for each modality.
+     *
+     * @param udfpsOverlayController under display fps or null if not present on device
+     */
+    public SensorOverlays(@Nullable IUdfpsOverlayController udfpsOverlayController) {
+        mUdfpsOverlayController = Optional.ofNullable(udfpsOverlayController);
+        // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+        mSidefpsController = Optional.empty();
+    }
+
     /**
      * Show the overlay.
      *
@@ -64,13 +79,15 @@ public final class SensorOverlays {
      * @param reason reason for showing
      * @param client client performing operation
      */
-    public void show(int sensorId, @BiometricOverlayConstants.ShowReason int reason,
+    public void show(int sensorId, @BiometricRequestConstants.RequestReason int reason,
             @NonNull AcquisitionClient<?> client) {
-        if (mSidefpsController.isPresent()) {
-            try {
-                mSidefpsController.get().show(sensorId, reason);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Remote exception when showing the side-fps overlay", e);
+        if (!sidefpsControllerRefactor()) {
+            if (mSidefpsController.isPresent()) {
+                try {
+                    mSidefpsController.get().show(sensorId, reason);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Remote exception when showing the side-fps overlay", e);
+                }
             }
         }
 
@@ -98,11 +115,13 @@ public final class SensorOverlays {
      * @param sensorId sensor id
      */
     public void hide(int sensorId) {
-        if (mSidefpsController.isPresent()) {
-            try {
-                mSidefpsController.get().hide(sensorId);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Remote exception when hiding the side-fps overlay", e);
+        if (!sidefpsControllerRefactor()) {
+            if (mSidefpsController.isPresent()) {
+                try {
+                    mSidefpsController.get().hide(sensorId);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Remote exception when hiding the side-fps overlay", e);
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index 769554315b6e78fd08d556d1a8e7393bcb02fda6..83b306b07c277075aefd2ed995769dcb3594d44b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -33,6 +33,7 @@ import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.hardware.biometrics.AuthenticationStateListener;
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.IBiometricSensorReceiver;
@@ -81,6 +82,7 @@ import com.android.internal.widget.LockPatternUtils;
 import com.android.server.SystemService;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
@@ -128,6 +130,8 @@ public class FingerprintService extends SystemService {
     private final BiometricStateCallback<ServiceProvider, FingerprintSensorPropertiesInternal>
             mBiometricStateCallback;
     @NonNull
+    private final AuthenticationStateListeners mAuthenticationStateListeners;
+    @NonNull
     private final Handler mHandler;
     @NonNull
     private final FingerprintServiceRegistry mRegistry;
@@ -891,6 +895,7 @@ public class FingerprintService extends SystemService {
                 return providers;
             });
         }
+
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void addAuthenticatorsRegisteredCallback(
@@ -900,6 +905,24 @@ public class FingerprintService extends SystemService {
             mRegistry.addAllRegisteredCallback(callback);
         }
 
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override
+        public void registerAuthenticationStateListener(
+                @NonNull AuthenticationStateListener listener) {
+            super.registerAuthenticationStateListener_enforcePermission();
+
+            mAuthenticationStateListeners.registerAuthenticationStateListener(listener);
+        }
+
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override
+        public void unregisterAuthenticationStateListener(
+                @NonNull AuthenticationStateListener listener) {
+            super.unregisterAuthenticationStateListener_enforcePermission();
+
+            mAuthenticationStateListeners.unregisterAuthenticationStateListener(listener);
+        }
+
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) {
@@ -957,6 +980,7 @@ public class FingerprintService extends SystemService {
             }
         }
 
+        // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void setSidefpsController(@NonNull ISidefpsController controller) {
@@ -1014,6 +1038,7 @@ public class FingerprintService extends SystemService {
         mLockoutResetDispatcher = new LockoutResetDispatcher(context);
         mLockPatternUtils = new LockPatternUtils(context);
         mBiometricStateCallback = new BiometricStateCallback<>(UserManager.get(context));
+        mAuthenticationStateListeners = new AuthenticationStateListeners();
         mFingerprintProvider = fingerprintProvider != null ? fingerprintProvider :
                 (name) -> {
                     final String fqName = IFingerprint.DESCRIPTOR + "/" + name;
@@ -1022,9 +1047,9 @@ public class FingerprintService extends SystemService {
                     if (fp != null) {
                         try {
                             return new FingerprintProvider(getContext(),
-                                    mBiometricStateCallback, fp.getSensorProps(), name,
-                                    mLockoutResetDispatcher, mGestureAvailabilityDispatcher,
-                                    mBiometricContext);
+                                    mBiometricStateCallback, mAuthenticationStateListeners,
+                                    fp.getSensorProps(), name, mLockoutResetDispatcher,
+                                    mGestureAvailabilityDispatcher, mBiometricContext);
                         } catch (RemoteException e) {
                             Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
                         }
@@ -1086,13 +1111,13 @@ public class FingerprintService extends SystemService {
                     Fingerprint21UdfpsMock.CONFIG_ENABLE_TEST_UDFPS, 0 /* default */,
                     UserHandle.USER_CURRENT) != 0) {
                 fingerprint21 = Fingerprint21UdfpsMock.newInstance(getContext(),
-                        mBiometricStateCallback, hidlSensor,
-                        mLockoutResetDispatcher, mGestureAvailabilityDispatcher,
+                        mBiometricStateCallback, mAuthenticationStateListeners,
+                        hidlSensor, mLockoutResetDispatcher, mGestureAvailabilityDispatcher,
                         BiometricContext.getInstance(getContext()));
             } else {
                 fingerprint21 = Fingerprint21.newInstance(getContext(),
-                        mBiometricStateCallback, hidlSensor, mHandler,
-                        mLockoutResetDispatcher, mGestureAvailabilityDispatcher);
+                        mBiometricStateCallback, mAuthenticationStateListeners, hidlSensor,
+                        mHandler, mLockoutResetDispatcher, mGestureAvailabilityDispatcher);
             }
             providers.add(fingerprint21);
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
index a15d1a4df39f053a8b6b7d43594a208c11771140..fc37d7020a211979674cd811c41f1e6f293cd390 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
@@ -135,6 +135,7 @@ public interface ServiceProvider extends
 
     void onPowerPressed();
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     /**
      * Sets side-fps controller
      * @param controller side-fps controller
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 337c3c299e54a67692b2a846f3099b1187130419..29c5a3de3a80911bd00f33823ef7c7a1dfde0d79 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
+import static com.android.systemui.shared.Flags.sidefpsControllerRefactor;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.TaskStackListener;
@@ -48,6 +50,7 @@ import com.android.server.biometrics.log.OperationContextExt;
 import com.android.server.biometrics.log.Probe;
 import com.android.server.biometrics.sensors.AuthSessionCoordinator;
 import com.android.server.biometrics.sensors.AuthenticationClient;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
 import com.android.server.biometrics.sensors.BiometricNotificationUtils;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
@@ -84,6 +87,7 @@ public class FingerprintAuthenticationClient
     private final int mSkipWaitForPowerVendorAcquireMessage;
     private final long mFingerUpIgnoresPower = 500;
     private final AuthSessionCoordinator mAuthSessionCoordinator;
+    @NonNull private final AuthenticationStateListeners mAuthenticationStateListeners;
     @Nullable
     private ICancellationSignal mCancellationSignal;
     private boolean mIsPointerDown;
@@ -110,7 +114,9 @@ public class FingerprintAuthenticationClient
             boolean isStrongBiometric,
             @Nullable TaskStackListener taskStackListener,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
+            // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
             @Nullable ISidefpsController sidefpsController,
+            @NonNull AuthenticationStateListeners authenticationStateListeners,
             boolean allowBackgroundAuthentication,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @NonNull Handler handler,
@@ -136,7 +142,12 @@ public class FingerprintAuthenticationClient
                 false /* shouldVibrate */,
                 biometricStrength);
         setRequestId(requestId);
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        if (sidefpsControllerRefactor()) {
+            mSensorOverlays = new SensorOverlays(udfpsOverlayController);
+        } else {
+            mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        }
+        mAuthenticationStateListeners = authenticationStateListeners;
         mSensorProps = sensorProps;
         mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */);
         mHandler = handler;
@@ -216,6 +227,9 @@ public class FingerprintAuthenticationClient
         if (authenticated) {
             mState = STATE_STOPPED;
             mSensorOverlays.hide(getSensorId());
+            if (sidefpsControllerRefactor()) {
+                mAuthenticationStateListeners.onAuthenticationStopped();
+            }
         } else {
             mState = STATE_STARTED_PAUSED_ATTEMPTED;
         }
@@ -241,6 +255,9 @@ public class FingerprintAuthenticationClient
                 // controlled by the HAL, the framework must stop the sensor before finishing the
                 // client.
                 mSensorOverlays.hide(getSensorId());
+                if (sidefpsControllerRefactor()) {
+                    mAuthenticationStateListeners.onAuthenticationStopped();
+                }
                 onErrorInternal(errorCode, 0 /* vendorCode */, false /* finish */);
                 cancel();
             }
@@ -266,11 +283,17 @@ public class FingerprintAuthenticationClient
         }
 
         mSensorOverlays.hide(getSensorId());
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStopped();
+        }
     }
 
     @Override
     protected void startHalOperation() {
-        mSensorOverlays.show(getSensorId(), getShowOverlayReason(), this);
+        mSensorOverlays.show(getSensorId(), getRequestReason(), this);
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStarted(getRequestReason());
+        }
 
         try {
             mCancellationSignal = doAuthenticate();
@@ -280,6 +303,9 @@ public class FingerprintAuthenticationClient
                     BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
                     0 /* vendorCode */);
             mSensorOverlays.hide(getSensorId());
+            if (sidefpsControllerRefactor()) {
+                mAuthenticationStateListeners.onAuthenticationStopped();
+            }
             mCallback.onClientFinished(this, false /* success */);
         }
     }
@@ -323,6 +349,9 @@ public class FingerprintAuthenticationClient
     @Override
     protected void stopHalOperation() {
         mSensorOverlays.hide(getSensorId());
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStopped();
+        }
         unsubscribeBiometricContext();
 
         if (mCancellationSignal != null) {
@@ -423,6 +452,9 @@ public class FingerprintAuthenticationClient
         }
 
         mSensorOverlays.hide(getSensorId());
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStopped();
+        }
         mCallback.onClientFinished(this, false /* success */);
     }
 
@@ -450,6 +482,9 @@ public class FingerprintAuthenticationClient
         }
 
         mSensorOverlays.hide(getSensorId());
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStopped();
+        }
         mCallback.onClientFinished(this, false /* success */);
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
index e2413ee1c016241447611429922538e6981fe627..e58e5ae117b57370fe3e4cdf6fb95a736c9b5ef5 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
@@ -16,10 +16,12 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
+import static com.android.systemui.shared.Flags.sidefpsControllerRefactor;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.biometrics.BiometricRequestConstants;
 import android.hardware.biometrics.common.ICancellationSignal;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
 import android.hardware.fingerprint.IUdfpsOverlayController;
@@ -66,7 +68,12 @@ public class FingerprintDetectClient extends AcquisitionClient<AidlSession>
                 true /* shouldVibrate */, biometricLogger, biometricContext);
         setRequestId(requestId);
         mIsStrongBiometric = isStrongBiometric;
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, null /* sideFpsController*/);
+        if (sidefpsControllerRefactor()) {
+            mSensorOverlays = new SensorOverlays(udfpsOverlayController);
+        } else {
+            mSensorOverlays = new SensorOverlays(
+                    udfpsOverlayController, null /* sideFpsController */);
+        }
         mOptions = options;
     }
 
@@ -93,7 +100,7 @@ public class FingerprintDetectClient extends AcquisitionClient<AidlSession>
 
     @Override
     protected void startHalOperation() {
-        mSensorOverlays.show(getSensorId(), BiometricOverlayConstants.REASON_AUTH_KEYGUARD,
+        mSensorOverlays.show(getSensorId(), BiometricRequestConstants.REASON_AUTH_KEYGUARD,
                 this);
 
         try {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index 06550d8b4fce756e890ea6a576cb527aab76bd41..c0761ed8f32b76f4b710fbfc3586483f4345056a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
+import static com.android.systemui.shared.Flags.sidefpsControllerRefactor;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -42,6 +44,7 @@ import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.log.CallbackWithProbe;
 import com.android.server.biometrics.log.OperationContextExt;
 import com.android.server.biometrics.log.Probe;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
 import com.android.server.biometrics.sensors.BiometricNotificationUtils;
 import com.android.server.biometrics.sensors.BiometricUtils;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
@@ -65,6 +68,7 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement
     @NonNull private final CallbackWithProbe<Probe> mALSProbeCallback;
 
     private final @FingerprintManager.EnrollReason int mEnrollReason;
+    @NonNull private final AuthenticationStateListeners mAuthenticationStateListeners;
     @Nullable private ICancellationSignal mCancellationSignal;
     private final int mMaxTemplatesPerUser;
     private boolean mIsPointerDown;
@@ -80,15 +84,18 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement
         }
     }
 
-    public FingerprintEnrollClient(@NonNull Context context,
-            @NonNull Supplier<AidlSession> lazyDaemon, @NonNull IBinder token, long requestId,
+    public FingerprintEnrollClient(
+            @NonNull Context context, @NonNull Supplier<AidlSession> lazyDaemon,
+            @NonNull IBinder token, long requestId,
             @NonNull ClientMonitorCallbackConverter listener, int userId,
             @NonNull byte[] hardwareAuthToken, @NonNull String owner,
             @NonNull BiometricUtils<Fingerprint> utils, int sensorId,
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
+            // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
             @Nullable ISidefpsController sidefpsController,
+            @NonNull AuthenticationStateListeners authenticationStateListeners,
             int maxTemplatesPerUser, @FingerprintManager.EnrollReason int enrollReason) {
         // UDFPS haptics occur when an image is acquired (instead of when the result is known)
         super(context, lazyDaemon, token, listener, userId, hardwareAuthToken, owner, utils,
@@ -96,7 +103,13 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement
                 logger, biometricContext);
         setRequestId(requestId);
         mSensorProps = sensorProps;
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        if (sidefpsControllerRefactor()) {
+            mSensorOverlays = new SensorOverlays(udfpsOverlayController);
+        } else {
+            mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        }
+        mAuthenticationStateListeners = authenticationStateListeners;
+
         mMaxTemplatesPerUser = maxTemplatesPerUser;
 
         mALSProbeCallback = getLogger().getAmbientLightProbe(true /* startWithClient */);
@@ -130,7 +143,11 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement
 
         if (remaining == 0) {
             mSensorOverlays.hide(getSensorId());
+            if (sidefpsControllerRefactor()) {
+                mAuthenticationStateListeners.onAuthenticationStopped();
+            }
         }
+
     }
 
     @Override
@@ -159,8 +176,10 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement
     @Override
     public void onError(int errorCode, int vendorCode) {
         super.onError(errorCode, vendorCode);
-
         mSensorOverlays.hide(getSensorId());
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStopped();
+        }
     }
 
     @Override
@@ -171,8 +190,12 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement
 
     @Override
     protected void startHalOperation() {
-        mSensorOverlays.show(getSensorId(), getOverlayReasonFromEnrollReason(mEnrollReason),
+        mSensorOverlays.show(getSensorId(), getRequestReasonFromEnrollReason(mEnrollReason),
                 this);
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStarted(
+                    getRequestReasonFromEnrollReason(mEnrollReason));
+        }
 
         BiometricNotificationUtils.cancelBadCalibrationNotification(getContext());
         try {
@@ -210,6 +233,10 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement
     @Override
     protected void stopHalOperation() {
         mSensorOverlays.hide(getSensorId());
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStopped();
+        }
+
         unsubscribeBiometricContext();
 
         if (mCancellationSignal != null) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index 9985b06833c947c70d2a5e279e18dc5cb5eb648b..032ab87196f80e71916b550117f02b713ba2c88b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -64,6 +64,7 @@ import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.AuthSessionCoordinator;
 import com.android.server.biometrics.sensors.AuthenticationClient;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.BiometricStateCallback;
@@ -108,6 +109,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
     @NonNull
     private final BiometricStateCallback mBiometricStateCallback;
     @NonNull
+    private final AuthenticationStateListeners mAuthenticationStateListeners;
+    @NonNull
     private final String mHalInstanceName;
     @NonNull
     private final Handler mHandler;
@@ -122,6 +125,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
     @NonNull private final BiometricContext mBiometricContext;
     @Nullable private IFingerprint mDaemon;
     @Nullable private IUdfpsOverlayController mUdfpsOverlayController;
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Nullable private ISidefpsController mSidefpsController;
     private AuthSessionCoordinator mAuthSessionCoordinator;
     @Nullable private AuthenticationStatsCollector mAuthenticationStatsCollector;
@@ -157,16 +161,19 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
 
     public FingerprintProvider(@NonNull Context context,
             @NonNull BiometricStateCallback biometricStateCallback,
+            @NonNull AuthenticationStateListeners authenticationStateListeners,
             @NonNull SensorProps[] props, @NonNull String halInstanceName,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
             @NonNull BiometricContext biometricContext) {
-        this(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
-                gestureAvailabilityDispatcher, biometricContext, null /* daemon */);
+        this(context, biometricStateCallback, authenticationStateListeners, props, halInstanceName,
+                lockoutResetDispatcher, gestureAvailabilityDispatcher, biometricContext,
+                null /* daemon */);
     }
 
     @VisibleForTesting FingerprintProvider(@NonNull Context context,
             @NonNull BiometricStateCallback biometricStateCallback,
+            @NonNull AuthenticationStateListeners authenticationStateListeners,
             @NonNull SensorProps[] props, @NonNull String halInstanceName,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
@@ -174,6 +181,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
             IFingerprint daemon) {
         mContext = context;
         mBiometricStateCallback = biometricStateCallback;
+        mAuthenticationStateListeners = authenticationStateListeners;
         mHalInstanceName = halInstanceName;
         mFingerprintSensors = new SensorList<>(ActivityManager.getService());
         mHandler = new Handler(Looper.getMainLooper());
@@ -434,7 +442,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
                     mBiometricContext,
                     mFingerprintSensors.get(sensorId).getSensorProperties(),
                     mUdfpsOverlayController, mSidefpsController,
-                    maxTemplatesPerUser, enrollReason);
+                    mAuthenticationStateListeners, maxTemplatesPerUser, enrollReason);
             scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
                     mBiometricStateCallback, new ClientMonitorCallback() {
                 @Override
@@ -498,7 +506,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
                     mBiometricContext, isStrongBiometric,
                     mTaskStackListener,
                     mUdfpsOverlayController, mSidefpsController,
-                    allowBackgroundAuthentication,
+                    mAuthenticationStateListeners, allowBackgroundAuthentication,
                     mFingerprintSensors.get(sensorId).getSensorProperties(), mHandler,
                     Utils.getCurrentStrength(sensorId),
                     SystemClock.elapsedRealtimeClock(),
@@ -732,6 +740,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
         }
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Override
     public void setSidefpsController(@NonNull ISidefpsController controller) {
         mSidefpsController = controller;
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
index 8bfa560b80317c42cfedbe6538cff73c0ff8874f..d3cecd0e34c7387fc7145e82ae63f8055b0fc623 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
@@ -68,6 +68,7 @@ import com.android.server.biometrics.sensors.AcquisitionClient;
 import com.android.server.biometrics.sensors.AuthSessionCoordinator;
 import com.android.server.biometrics.sensors.AuthenticationClient;
 import com.android.server.biometrics.sensors.AuthenticationConsumer;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.BiometricStateCallback;
@@ -115,6 +116,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
 
     final Context mContext;
     @NonNull private final BiometricStateCallback mBiometricStateCallback;
+    @NonNull private final AuthenticationStateListeners mAuthenticationStateListeners;
     private final ActivityTaskManager mActivityTaskManager;
     @NonNull private final FingerprintSensorPropertiesInternal mSensorProperties;
     private final BiometricScheduler mScheduler;
@@ -128,6 +130,8 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
     @Nullable private IBiometricsFingerprint mDaemon;
     @NonNull private final HalResultController mHalResultController;
     @Nullable private IUdfpsOverlayController mUdfpsOverlayController;
+
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Nullable private ISidefpsController mSidefpsController;
     @NonNull private final BiometricContext mBiometricContext;
     @Nullable private AuthenticationStatsCollector mAuthenticationStatsCollector;
@@ -330,6 +334,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
     @VisibleForTesting
     Fingerprint21(@NonNull Context context,
             @NonNull BiometricStateCallback biometricStateCallback,
+            @NonNull AuthenticationStateListeners authenticationStateListeners,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @NonNull BiometricScheduler scheduler,
             @NonNull Handler handler,
@@ -338,6 +343,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
             @NonNull BiometricContext biometricContext) {
         mContext = context;
         mBiometricStateCallback = biometricStateCallback;
+        mAuthenticationStateListeners = authenticationStateListeners;
         mBiometricContext = biometricContext;
 
         mSensorProperties = sensorProps;
@@ -378,6 +384,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
 
     public static Fingerprint21 newInstance(@NonNull Context context,
             @NonNull BiometricStateCallback biometricStateCallback,
+            @NonNull AuthenticationStateListeners authenticationStateListeners,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @NonNull Handler handler,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
@@ -388,8 +395,9 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
                         gestureAvailabilityDispatcher);
         final HalResultController controller = new HalResultController(sensorProps.sensorId,
                 context, handler, scheduler);
-        return new Fingerprint21(context, biometricStateCallback, sensorProps, scheduler, handler,
-                lockoutResetDispatcher, controller, BiometricContext.getInstance(context));
+        return new Fingerprint21(context, biometricStateCallback, authenticationStateListeners,
+                sensorProps, scheduler, handler, lockoutResetDispatcher, controller,
+                BiometricContext.getInstance(context));
     }
 
     @Override
@@ -719,7 +727,10 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
                 mSensorProperties.sensorId,
                 createLogger(BiometricsProtoEnums.ACTION_ENROLL,
                         BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector),
-                mBiometricContext, mUdfpsOverlayController, mSidefpsController, enrollReason);
+                mBiometricContext, mUdfpsOverlayController,
+                // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+                mSidefpsController,
+                mAuthenticationStateListeners, enrollReason);
         mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
             @Override
             public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
@@ -761,10 +772,14 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
                                 BiometricsProtoEnums.CLIENT_UNKNOWN,
                                 mAuthenticationStatsCollector),
                         mBiometricContext, null /* sensorProps */,
-                        mUdfpsOverlayController, mSidefpsController,
+                        mUdfpsOverlayController,
+                        // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+                        mSidefpsController,
+                        mAuthenticationStateListeners,
                         mContext.getResources().getInteger(
                                 com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser),
                         enrollReason);
+
         mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
             @Override
             public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
@@ -880,6 +895,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
                         mBiometricContext, isStrongBiometric,
                         mTaskStackListener,
                         mUdfpsOverlayController, mSidefpsController,
+                        mAuthenticationStateListeners,
                         allowBackgroundAuthentication, mSensorProperties, mHandler,
                         Utils.getCurrentStrength(mSensorId), null /* clock */, mLockoutTracker);
         mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
@@ -898,6 +914,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
                 mBiometricContext, isStrongBiometric,
                 mTaskStackListener, mLockoutTracker,
                 mUdfpsOverlayController, mSidefpsController,
+                mAuthenticationStateListeners,
                 allowBackgroundAuthentication, mSensorProperties,
                 Utils.getCurrentStrength(mSensorId));
         mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
@@ -1123,6 +1140,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider
         mUdfpsOverlayController = controller;
     }
 
+    // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
     @Override
     public void setSidefpsController(@NonNull ISidefpsController controller) {
         mSidefpsController = controller;
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
index c1a9370b542325c59d18cb67e09cd11cd8aceb26..88dae6fcc453775fbb8647fa08820cb567c44bfa 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
@@ -41,6 +41,7 @@ import android.util.SparseBooleanArray;
 import com.android.internal.R;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.AuthenticationConsumer;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.BiometricStateCallback;
@@ -248,6 +249,7 @@ public class Fingerprint21UdfpsMock extends Fingerprint21 implements TrustManage
 
     public static Fingerprint21UdfpsMock newInstance(@NonNull Context context,
             @NonNull BiometricStateCallback biometricStateCallback,
+            @NonNull AuthenticationStateListeners authenticationStateListeners,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
@@ -259,8 +261,9 @@ public class Fingerprint21UdfpsMock extends Fingerprint21 implements TrustManage
                 new TestableBiometricScheduler(TAG, handler, gestureAvailabilityDispatcher);
         final MockHalResultController controller =
                 new MockHalResultController(sensorProps.sensorId, context, handler, scheduler);
-        return new Fingerprint21UdfpsMock(context, biometricStateCallback, sensorProps, scheduler,
-                handler, lockoutResetDispatcher, controller, biometricContext);
+        return new Fingerprint21UdfpsMock(context, biometricStateCallback,
+                authenticationStateListeners, sensorProps, scheduler, handler,
+                lockoutResetDispatcher, controller, biometricContext);
     }
 
     private static abstract class FakeFingerRunnable implements Runnable {
@@ -388,14 +391,15 @@ public class Fingerprint21UdfpsMock extends Fingerprint21 implements TrustManage
 
     private Fingerprint21UdfpsMock(@NonNull Context context,
             @NonNull BiometricStateCallback biometricStateCallback,
+            @NonNull AuthenticationStateListeners authenticationStateListeners,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @NonNull TestableBiometricScheduler scheduler,
             @NonNull Handler handler,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull MockHalResultController controller,
             @NonNull BiometricContext biometricContext) {
-        super(context, biometricStateCallback, sensorProps, scheduler, handler,
-                lockoutResetDispatcher, controller, biometricContext);
+        super(context, biometricStateCallback, authenticationStateListeners, sensorProps, scheduler,
+                handler, lockoutResetDispatcher, controller, biometricContext);
         mScheduler = scheduler;
         mScheduler.init(this);
         mHandler = handler;
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
index 9966e910d502ba4c7c6eaf4581ef30e11ab13e99..4c1d4d6d6d124a5527d51cc50764b04b76c2e221 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.sensors.fingerprint.hidl;
 
+import static com.android.systemui.shared.Flags.sidefpsControllerRefactor;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.TaskStackListener;
@@ -40,6 +42,7 @@ import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.log.CallbackWithProbe;
 import com.android.server.biometrics.log.Probe;
 import com.android.server.biometrics.sensors.AuthenticationClient;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
 import com.android.server.biometrics.sensors.BiometricNotificationUtils;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
@@ -68,6 +71,7 @@ class FingerprintAuthenticationClient
     @NonNull private final SensorOverlays mSensorOverlays;
     @NonNull private final FingerprintSensorPropertiesInternal mSensorProps;
     @NonNull private final CallbackWithProbe<Probe> mALSProbeCallback;
+    @NonNull private final AuthenticationStateListeners mAuthenticationStateListeners;
 
     private boolean mIsPointerDown;
 
@@ -81,7 +85,9 @@ class FingerprintAuthenticationClient
             @NonNull TaskStackListener taskStackListener,
             @NonNull LockoutFrameworkImpl lockoutTracker,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
+            // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
             @Nullable ISidefpsController sidefpsController,
+            @NonNull AuthenticationStateListeners authenticationStateListeners,
             boolean allowBackgroundAuthentication,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @Authenticators.Types int sensorStrength) {
@@ -91,7 +97,12 @@ class FingerprintAuthenticationClient
                 false /* shouldVibrate */, sensorStrength);
         setRequestId(requestId);
         mLockoutFrameworkImpl = lockoutTracker;
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        if (sidefpsControllerRefactor()) {
+            mSensorOverlays = new SensorOverlays(udfpsOverlayController);
+        } else {
+            mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        }
+        mAuthenticationStateListeners = authenticationStateListeners;
         mSensorProps = sensorProps;
         mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */);
     }
@@ -128,6 +139,9 @@ class FingerprintAuthenticationClient
             mState = STATE_STOPPED;
             resetFailedAttempts(getTargetUserId());
             mSensorOverlays.hide(getSensorId());
+            if (sidefpsControllerRefactor()) {
+                mAuthenticationStateListeners.onAuthenticationStopped();
+            }
         } else {
             mState = STATE_STARTED_PAUSED_ATTEMPTED;
             final @LockoutTracker.LockoutMode int lockoutMode =
@@ -141,6 +155,9 @@ class FingerprintAuthenticationClient
                 // controlled by the HAL, the framework must stop the sensor before finishing the
                 // client.
                 mSensorOverlays.hide(getSensorId());
+                if (sidefpsControllerRefactor()) {
+                    mAuthenticationStateListeners.onAuthenticationStopped();
+                }
                 onErrorInternal(errorCode, 0 /* vendorCode */, false /* finish */);
                 cancel();
             }
@@ -156,6 +173,9 @@ class FingerprintAuthenticationClient
         }
 
         mSensorOverlays.hide(getSensorId());
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStopped();
+        }
     }
 
     private void resetFailedAttempts(int userId) {
@@ -205,7 +225,10 @@ class FingerprintAuthenticationClient
 
     @Override
     protected void startHalOperation() {
-        mSensorOverlays.show(getSensorId(), getShowOverlayReason(), this);
+        mSensorOverlays.show(getSensorId(), getRequestReason(), this);
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStarted(getRequestReason());
+        }
 
         try {
             // GroupId was never used. In fact, groupId is always the same as userId.
@@ -215,6 +238,9 @@ class FingerprintAuthenticationClient
             onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
                     0 /* vendorCode */);
             mSensorOverlays.hide(getSensorId());
+            if (sidefpsControllerRefactor()) {
+                mAuthenticationStateListeners.onAuthenticationStopped();
+            }
             mCallback.onClientFinished(this, false /* success */);
         }
     }
@@ -222,6 +248,9 @@ class FingerprintAuthenticationClient
     @Override
     protected void stopHalOperation() {
         mSensorOverlays.hide(getSensorId());
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStopped();
+        }
 
         try {
             getFreshDaemon().cancel();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
index 0d7f9f23e0e110f077d9c1ad2e0bf97cc3ec23a6..6e029d2268e20e6b12ec1063857e5d2838c30d98 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
@@ -16,12 +16,14 @@
 
 package com.android.server.biometrics.sensors.fingerprint.hidl;
 
+import static com.android.systemui.shared.Flags.sidefpsControllerRefactor;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricFingerprintConstants;
-import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.biometrics.BiometricRequestConstants;
 import android.hardware.biometrics.fingerprint.PointerContext;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
@@ -71,7 +73,12 @@ class FingerprintDetectClient extends AcquisitionClient<IBiometricsFingerprint>
                 options.getOpPackageName(), 0 /* cookie */, options.getSensorId(),
                 true /* shouldVibrate */, biometricLogger, biometricContext);
         setRequestId(requestId);
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, null /* sideFpsController */);
+        if (sidefpsControllerRefactor()) {
+            mSensorOverlays = new SensorOverlays(udfpsOverlayController);
+        } else {
+            mSensorOverlays = new SensorOverlays(
+                    udfpsOverlayController, null /* sideFpsController */);
+        }
         mIsStrongBiometric = isStrongBiometric;
     }
 
@@ -97,7 +104,7 @@ class FingerprintDetectClient extends AcquisitionClient<IBiometricsFingerprint>
 
     @Override
     protected void startHalOperation() {
-        mSensorOverlays.show(getSensorId(), BiometricOverlayConstants.REASON_AUTH_KEYGUARD,
+        mSensorOverlays.show(getSensorId(), BiometricRequestConstants.REASON_AUTH_KEYGUARD,
                 this);
 
         try {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
index 382e7e2121f49206e3d36f5f0bfe657627e41ef4..26332ff6893c3bd6139535dcfbf05eb6a1b8ecd5 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.sensors.fingerprint.hidl;
 
+import static com.android.systemui.shared.Flags.sidefpsControllerRefactor;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -34,6 +36,7 @@ import android.util.Slog;
 
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
 import com.android.server.biometrics.sensors.BiometricNotificationUtils;
 import com.android.server.biometrics.sensors.BiometricUtils;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
@@ -58,22 +61,31 @@ public class FingerprintEnrollClient extends EnrollClient<IBiometricsFingerprint
 
     @NonNull private final SensorOverlays mSensorOverlays;
     private final @FingerprintManager.EnrollReason int mEnrollReason;
+    @NonNull private final AuthenticationStateListeners mAuthenticationStateListeners;
     private boolean mIsPointerDown;
 
-    FingerprintEnrollClient(@NonNull Context context,
-            @NonNull Supplier<IBiometricsFingerprint> lazyDaemon, @NonNull IBinder token,
-            long requestId, @NonNull ClientMonitorCallbackConverter listener, int userId,
+    FingerprintEnrollClient(
+            @NonNull Context context, @NonNull Supplier<IBiometricsFingerprint> lazyDaemon,
+            @NonNull IBinder token, long requestId,
+            @NonNull ClientMonitorCallbackConverter listener, int userId,
             @NonNull byte[] hardwareAuthToken, @NonNull String owner,
             @NonNull BiometricUtils<Fingerprint> utils, int timeoutSec, int sensorId,
             @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
+            // TODO(b/288175061): remove with Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
             @Nullable ISidefpsController sidefpsController,
+            @NonNull AuthenticationStateListeners authenticationStateListeners,
             @FingerprintManager.EnrollReason int enrollReason) {
         super(context, lazyDaemon, token, listener, userId, hardwareAuthToken, owner, utils,
                 timeoutSec, sensorId, true /* shouldVibrate */, biometricLogger,
                 biometricContext);
         setRequestId(requestId);
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        if (sidefpsControllerRefactor()) {
+            mSensorOverlays = new SensorOverlays(udfpsOverlayController);
+        } else {
+            mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        }
+        mAuthenticationStateListeners = authenticationStateListeners;
 
         mEnrollReason = enrollReason;
         if (enrollReason == FingerprintManager.ENROLL_FIND_SENSOR) {
@@ -110,8 +122,12 @@ public class FingerprintEnrollClient extends EnrollClient<IBiometricsFingerprint
 
     @Override
     protected void startHalOperation() {
-        mSensorOverlays.show(getSensorId(), getOverlayReasonFromEnrollReason(mEnrollReason),
+        mSensorOverlays.show(getSensorId(), getRequestReasonFromEnrollReason(mEnrollReason),
                 this);
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStarted(
+                    getRequestReasonFromEnrollReason(mEnrollReason));
+        }
 
         BiometricNotificationUtils.cancelBadCalibrationNotification(getContext());
         try {
@@ -122,6 +138,9 @@ public class FingerprintEnrollClient extends EnrollClient<IBiometricsFingerprint
             onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE,
                     0 /* vendorCode */);
             mSensorOverlays.hide(getSensorId());
+            if (sidefpsControllerRefactor()) {
+                mAuthenticationStateListeners.onAuthenticationStopped();
+            }
             mCallback.onClientFinished(this, false /* success */);
         }
     }
@@ -129,6 +148,9 @@ public class FingerprintEnrollClient extends EnrollClient<IBiometricsFingerprint
     @Override
     protected void stopHalOperation() {
         mSensorOverlays.hide(getSensorId());
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStopped();
+        }
 
         try {
             getFreshDaemon().cancel();
@@ -149,6 +171,9 @@ public class FingerprintEnrollClient extends EnrollClient<IBiometricsFingerprint
 
         if (remaining == 0) {
             mSensorOverlays.hide(getSensorId());
+            if (sidefpsControllerRefactor()) {
+                mAuthenticationStateListeners.onAuthenticationStopped();
+            }
         }
     }
 
@@ -170,6 +195,9 @@ public class FingerprintEnrollClient extends EnrollClient<IBiometricsFingerprint
         super.onError(errorCode, vendorCode);
 
         mSensorOverlays.hide(getSensorId());
+        if (sidefpsControllerRefactor()) {
+            mAuthenticationStateListeners.onAuthenticationStopped();
+        }
     }
 
     @Override
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
index a78f2dcf2ab272c8c42c7cea2791bd394520f8f2..3b5cae328b3ca5ae15d21abf69443094ea1c7093 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
@@ -23,6 +23,8 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
 import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;
 import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_SUCCESS;
 
+import static com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR;
+
 import static junit.framework.Assert.assertEquals;
 
 import static org.junit.Assert.assertThrows;
@@ -41,6 +43,7 @@ import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
+import android.hardware.biometrics.AuthenticationStateListener;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.Flags;
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
@@ -108,6 +111,7 @@ public class AuthServiceTest {
     @Mock
     IFaceService mFaceService;
     @Mock
+
     AppOpsManager mAppOpsManager;
     @Mock
     private VirtualDeviceManagerInternal mVdmInternal;
@@ -404,6 +408,23 @@ public class AuthServiceTest {
                 eq(TEST_OP_PACKAGE_NAME));
     }
 
+    @Test
+    public void testRegisterAuthenticationStateListener_callsFingerprintService()
+            throws Exception {
+        mSetFlagsRule.enableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR);
+        setInternalAndTestBiometricPermissions(mContext, true /* hasPermission */);
+
+        mAuthService = new AuthService(mContext, mInjector);
+        mAuthService.onStart();
+
+        final AuthenticationStateListener listener = mock(AuthenticationStateListener.class);
+
+        mAuthService.mImpl.registerAuthenticationStateListener(listener);
+
+        waitForIdle();
+        verify(mFingerprintService).registerAuthenticationStateListener(
+                eq(listener));
+    }
 
     @Test
     public void testRegisterKeyguardCallback_callsBiometricServiceRegisterKeyguardCallback()
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
index 5012335b533fa77b90710f3eb41fe927fa2df30c..94cb860ae7107805a1a7f751419006cacc2803bb 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.sensors;
 
+import static com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -23,10 +25,11 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.biometrics.BiometricRequestConstants;
 import android.hardware.fingerprint.ISidefpsController;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
 
 import androidx.test.filters.SmallTest;
 
@@ -40,6 +43,7 @@ import org.mockito.junit.MockitoRule;
 import java.util.ArrayList;
 import java.util.List;
 
+@RequiresFlagsDisabled(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
 @Presubmit
 @SmallTest
 public class SensorOverlaysTest {
@@ -97,7 +101,7 @@ public class SensorOverlaysTest {
     private void testShow(IUdfpsOverlayController udfps, ISidefpsController sidefps)
             throws Exception {
         final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps);
-        final int reason = BiometricOverlayConstants.REASON_UNKNOWN;
+        final int reason = BiometricRequestConstants.REASON_UNKNOWN;
         sensorOverlays.show(SENSOR_ID, reason, mAcquisitionClient);
 
         if (udfps != null) {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index 79a528c59f497bd04b1dea4a09a5d615ee1f71a1..c24227fcd1f76e816464014e94e46d8b97288846 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -18,6 +18,8 @@ package com.android.server.biometrics.sensors.fingerprint.aidl;
 
 import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;
 
+import static com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -56,6 +58,7 @@ import android.platform.test.annotations.Presubmit;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.TestableContext;
 
 import androidx.test.filters.SmallTest;
@@ -68,6 +71,7 @@ import com.android.server.biometrics.log.CallbackWithProbe;
 import com.android.server.biometrics.log.OperationContextExt;
 import com.android.server.biometrics.log.Probe;
 import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.LockoutTracker;
@@ -91,6 +95,8 @@ import java.util.function.Consumer;
 @SmallTest
 public class FingerprintAuthenticationClientTest {
 
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private static final int SENSOR_ID = 4;
     private static final int USER_ID = 8;
     private static final long OP_ID = 7;
@@ -128,6 +134,8 @@ public class FingerprintAuthenticationClientTest {
     @Mock
     private ISidefpsController mSideFpsController;
     @Mock
+    private AuthenticationStateListeners mAuthenticationStateListeners;
+    @Mock
     private FingerprintSensorPropertiesInternal mSensorProps;
     @Mock
     private ClientMonitorCallback mCallback;
@@ -384,6 +392,7 @@ public class FingerprintAuthenticationClientTest {
 
     private void showHideOverlay(Consumer<FingerprintAuthenticationClient> block)
             throws RemoteException {
+        mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR);
         final FingerprintAuthenticationClient client = createClient();
 
         client.start(mCallback);
@@ -397,6 +406,49 @@ public class FingerprintAuthenticationClientTest {
         verify(mSideFpsController).hide(anyInt());
     }
 
+    @Test
+    public void showHideOverlay_cancel_sidefpsControllerRemovalRefactor() throws RemoteException {
+        showHideOverlay_sidefpsControllerRemovalRefactor(c -> c.cancel());
+    }
+
+    @Test
+    public void showHideOverlay_stop_sidefpsControllerRemovalRefactor() throws RemoteException {
+        showHideOverlay_sidefpsControllerRemovalRefactor(c -> c.stopHalOperation());
+    }
+
+    @Test
+    public void showHideOverlay_error_sidefpsControllerRemovalRefactor() throws RemoteException {
+        showHideOverlay_sidefpsControllerRemovalRefactor(c -> c.onError(0, 0));
+        verify(mCallback).onClientFinished(any(), eq(false));
+    }
+
+    @Test
+    public void showHideOverlay_lockout_sidefpsControllerRemovalRefactor() throws RemoteException {
+        showHideOverlay_sidefpsControllerRemovalRefactor(c -> c.onLockoutTimed(5000));
+    }
+
+    @Test
+    public void showHideOverlay_lockoutPerm_sidefpsControllerRemovalRefactor()
+            throws RemoteException {
+        showHideOverlay_sidefpsControllerRemovalRefactor(c -> c.onLockoutPermanent());
+    }
+
+    private void showHideOverlay_sidefpsControllerRemovalRefactor(
+            Consumer<FingerprintAuthenticationClient> block) throws RemoteException {
+        mSetFlagsRule.enableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR);
+        final FingerprintAuthenticationClient client = createClient();
+
+        client.start(mCallback);
+
+        verify(mUdfpsOverlayController).showUdfpsOverlay(eq(REQUEST_ID), anyInt(), anyInt(), any());
+        verify(mAuthenticationStateListeners).onAuthenticationStarted(anyInt());
+
+        block.accept(client);
+
+        verify(mUdfpsOverlayController).hideUdfpsOverlay(anyInt());
+        verify(mAuthenticationStateListeners).onAuthenticationStopped();
+    }
+
     @Test
     public void cancelsAuthWhenNotInForeground() throws Exception {
         final ActivityManager.RunningTaskInfo topTask = new ActivityManager.RunningTaskInfo();
@@ -502,7 +554,8 @@ public class FingerprintAuthenticationClientTest {
                 mBiometricLogger, mBiometricContext,
                 true /* isStrongBiometric */,
                 null /* taskStackListener */,
-                mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication,
+                mUdfpsOverlayController, mSideFpsController, mAuthenticationStateListeners,
+                allowBackgroundAuthentication,
                 mSensorProps,
                 new Handler(mLooper.getLooper()), 0 /* biometricStrength */, mClock,
                 lockoutTracker) {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
index c7eb1db3e74bb9c9f46687c104329a9d4e44d10e..e7d4a2e463f7dc325cd2bc5f3dfa4511289ebdce 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
+import static com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR;
+
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -38,6 +40,7 @@ import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.TestableContext;
 
 import androidx.test.filters.SmallTest;
@@ -48,6 +51,7 @@ import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.log.CallbackWithProbe;
 import com.android.server.biometrics.log.OperationContextExt;
 import com.android.server.biometrics.log.Probe;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
 import com.android.server.biometrics.sensors.BiometricUtils;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
@@ -68,6 +72,8 @@ import java.util.function.Consumer;
 @SmallTest
 public class FingerprintEnrollClientTest {
 
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private static final byte[] HAT = new byte[69];
     private static final int USER_ID = 8;
     private static final long REQUEST_ID = 9;
@@ -98,6 +104,8 @@ public class FingerprintEnrollClientTest {
     @Mock
     private ISidefpsController mSideFpsController;
     @Mock
+    private AuthenticationStateListeners mAuthenticationStateListeners;
+    @Mock
     private FingerprintSensorPropertiesInternal mSensorProps;
     @Mock
     private ClientMonitorCallback mCallback;
@@ -271,6 +279,7 @@ public class FingerprintEnrollClientTest {
 
     private void showHideOverlay(Consumer<FingerprintEnrollClient> block)
             throws RemoteException {
+        mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR);
         final FingerprintEnrollClient client = createClient();
 
         client.start(mCallback);
@@ -284,6 +293,44 @@ public class FingerprintEnrollClientTest {
         verify(mSideFpsController).hide(anyInt());
     }
 
+    @Test
+    public void showHideOverlay_cancel_sidefpsControllerRemovalRefactor() throws RemoteException {
+        showHideOverlay_sidefpsControllerRemovalRefactor(c -> c.cancel());
+    }
+
+    @Test
+    public void showHideOverlay_stop_sidefpsControllerRemovalRefactor() throws RemoteException {
+        showHideOverlay_sidefpsControllerRemovalRefactor(c -> c.stopHalOperation());
+    }
+
+    @Test
+    public void showHideOverlay_error_sidefpsControllerRemovalRefactor() throws RemoteException {
+        showHideOverlay_sidefpsControllerRemovalRefactor(c -> c.onError(0, 0));
+        verify(mCallback).onClientFinished(any(), eq(false));
+    }
+
+    @Test
+    public void showHideOverlay_result_sidefpsControllerRemovalRefactor() throws RemoteException {
+        showHideOverlay_sidefpsControllerRemovalRefactor(
+                c -> c.onEnrollResult(new Fingerprint("", 1, 1), 0));
+    }
+
+    private void showHideOverlay_sidefpsControllerRemovalRefactor(
+            Consumer<FingerprintEnrollClient> block) throws RemoteException {
+        mSetFlagsRule.enableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR);
+        final FingerprintEnrollClient client = createClient();
+
+        client.start(mCallback);
+
+        verify(mUdfpsOverlayController).showUdfpsOverlay(eq(REQUEST_ID), anyInt(), anyInt(), any());
+        verify(mAuthenticationStateListeners).onAuthenticationStarted(anyInt());
+
+        block.accept(client);
+
+        verify(mUdfpsOverlayController).hideUdfpsOverlay(anyInt());
+        verify(mAuthenticationStateListeners).onAuthenticationStopped();
+    }
+
     private FingerprintEnrollClient createClient() throws RemoteException {
         return createClient(500);
     }
@@ -296,6 +343,7 @@ public class FingerprintEnrollClientTest {
         mClientMonitorCallbackConverter, 0 /* userId */,
         HAT, "owner", mBiometricUtils, 8 /* sensorId */,
         mBiometricLogger, mBiometricContext, mSensorProps, mUdfpsOverlayController,
-        mSideFpsController, 6 /* maxTemplatesPerUser */, FingerprintManager.ENROLL_ENROLL);
+        mSideFpsController, mAuthenticationStateListeners, 6 /* maxTemplatesPerUser */,
+        FingerprintManager.ENROLL_ENROLL);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
index 8f6efffcbff8a30d79bb8b7e721b4808b9a8a3f1..4cfb83fa1c69fc051c962a4f7799962a3c9cceaa 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
@@ -43,6 +43,7 @@ import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
 import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.BiometricStateCallback;
@@ -74,6 +75,8 @@ public class FingerprintProviderTest {
     @Mock
     private GestureAvailabilityDispatcher mGestureAvailabilityDispatcher;
     @Mock
+    private AuthenticationStateListeners mAuthenticationStateListeners;
+    @Mock
     private BiometricStateCallback mBiometricStateCallback;
     @Mock
     private BiometricContext mBiometricContext;
@@ -110,8 +113,9 @@ public class FingerprintProviderTest {
         mLockoutResetDispatcher = new LockoutResetDispatcher(mContext);
 
         mFingerprintProvider = new FingerprintProvider(mContext,
-                mBiometricStateCallback, mSensorProps, TAG, mLockoutResetDispatcher,
-                mGestureAvailabilityDispatcher, mBiometricContext, mDaemon);
+                mBiometricStateCallback, mAuthenticationStateListeners, mSensorProps, TAG,
+                mLockoutResetDispatcher, mGestureAvailabilityDispatcher, mBiometricContext,
+                mDaemon);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21Test.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21Test.java
index b32b89aaefb03c310519d5bac87df9737a99f8fa..0d3f1921c9475ea63b7d31797a1930a5360a9e2d 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21Test.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21Test.java
@@ -40,6 +40,7 @@ import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
 import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -70,6 +71,8 @@ public class Fingerprint21Test {
     @Mock
     private BiometricScheduler mScheduler;
     @Mock
+    private AuthenticationStateListeners mAuthenticationStateListeners;
+    @Mock
     private BiometricStateCallback mBiometricStateCallback;
     @Mock
     private BiometricContext mBiometricContext;
@@ -102,9 +105,10 @@ public class Fingerprint21Test {
                         componentInfo, FingerprintSensorProperties.TYPE_UNKNOWN,
                         resetLockoutRequiresHardwareAuthToken);
 
-        mFingerprint21 = new TestableFingerprint21(mContext, mBiometricStateCallback, sensorProps,
-                mScheduler, new Handler(Looper.getMainLooper()), mLockoutResetDispatcher,
-                mHalResultController, mBiometricContext);
+        mFingerprint21 = new TestableFingerprint21(mContext, mBiometricStateCallback,
+                mAuthenticationStateListeners, sensorProps, mScheduler,
+                new Handler(Looper.getMainLooper()), mLockoutResetDispatcher, mHalResultController,
+                mBiometricContext);
     }
 
     @Test
@@ -126,13 +130,14 @@ public class Fingerprint21Test {
 
         TestableFingerprint21(@NonNull Context context,
                 @NonNull BiometricStateCallback biometricStateCallback,
+                @NonNull AuthenticationStateListeners authenticationStateListeners,
                 @NonNull FingerprintSensorPropertiesInternal sensorProps,
                 @NonNull BiometricScheduler scheduler, @NonNull Handler handler,
                 @NonNull LockoutResetDispatcher lockoutResetDispatcher,
                 @NonNull HalResultController controller,
                 @NonNull BiometricContext biometricContext) {
-            super(context, biometricStateCallback, sensorProps, scheduler, handler,
-                    lockoutResetDispatcher, controller, biometricContext);
+            super(context, biometricStateCallback, authenticationStateListeners, sensorProps,
+                    scheduler, handler, lockoutResetDispatcher, controller, biometricContext);
         }
 
         @Override