UDFPS Enrollment Refactor (4/N)

Accessibility + text/dpi change + rotation should be properly handled.
Debug repos were added to make UI developemnt for UDFPS much easier(not
requiring calls to fingerprint manager).

Change-Id: I89900cea0d9e953124781cdf308fb38858de5d16
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f629a36..a438fdd 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2788,6 +2788,7 @@
         <activity android:name=".biometrics.fingerprint2.ui.enrollment.activity.FingerprintEnrollmentV2Activity"
             android:exported="true"
             android:permission="android.permission.MANAGE_FINGERPRINT"
+            android:configChanges="density"
             android:theme="@style/GlifTheme.Light">
             <intent-filter>
                 <action android:name="android.settings.FINGERPRINT_SETUP" />
diff --git a/res/layout-land/fingerprint_v2_udfps_enroll_enrolling.xml b/res/layout-land/fingerprint_v2_udfps_enroll_enrolling.xml
new file mode 100644
index 0000000..86768d6
--- /dev/null
+++ b/res/layout-land/fingerprint_v2_udfps_enroll_enrolling.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/udfps_layout"
+    style="?attr/fingerprint_layout_theme"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal">
+
+    <!-- This is used to grab style attributes and apply them
+    to this layout -->
+    <com.google.android.setupdesign.GlifLayout
+        android:id="@+id/dummy_glif_layout"
+        style="?attr/fingerprint_layout_theme"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:visibility="gone"
+        />
+
+    <LinearLayout
+        android:layout_width="300dp"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@+id/sud_layout_icon"
+            style="@style/SudGlifIcon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:scaleType="fitStart"
+            android:src="@drawable/ic_lock" />
+
+        <TextView
+            android:id="@+id/title"
+            style="@style/SudGlifHeaderTitle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:lines="2"
+            />
+
+        <TextView
+            android:id="@+id/description"
+            style="@style/SudDescription.Glif"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:lines="3"
+            android:paddingLeft="10dp"
+            android:paddingRight="10dp"
+            />
+
+
+        <com.airbnb.lottie.LottieAnimationView
+            android:id="@+id/illustration_lottie"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:paddingLeft="10dp"
+            android:paddingRight="10dp"
+            android:scaleType="centerInside"
+            android:visibility="gone"
+            app:lottie_autoPlay="true"
+            app:lottie_loop="true"
+            app:lottie_speed=".85"
+            />
+    </LinearLayout>
+
+    <FrameLayout
+        android:id="@+id/layout_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center_horizontal|bottom"
+        android:clipToPadding="false"
+        >
+
+        <include layout="@layout/fingerprint_v2_udfps_enroll_view" />
+    </FrameLayout>
+
+
+</LinearLayout>
+
diff --git a/res/layout/fingerprint_v2_udfps_enroll_enrolling.xml b/res/layout/fingerprint_v2_udfps_enroll_enrolling.xml
index ddd2c30..ab8fb2c 100644
--- a/res/layout/fingerprint_v2_udfps_enroll_enrolling.xml
+++ b/res/layout/fingerprint_v2_udfps_enroll_enrolling.xml
@@ -18,7 +18,7 @@
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/setup_wizard_layout"
+    android:id="@+id/udfps_layout"
     style="?attr/fingerprint_layout_theme"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
diff --git a/src/com/android/settings/biometrics/fingerprint2/data/repository/DebuggingRepository.kt b/src/com/android/settings/biometrics/fingerprint2/data/repository/DebuggingRepository.kt
new file mode 100644
index 0000000..e99f85b
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/data/repository/DebuggingRepository.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.data.repository
+
+import android.os.Build
+
+/** Indicates if the developer has debugging features enabled. */
+interface DebuggingRepository {
+
+  /** A function that will return if a build is debuggable */
+  fun isDebuggingEnabled(): Boolean
+  /** A function that will return if udfps enrollment should be swapped with debug repos */
+  fun isUdfpsEnrollmentDebuggingEnabled(): Boolean
+}
+
+class DebuggingRepositoryImpl : DebuggingRepository {
+  /**
+   * This flag can be flipped by the engineer which should allow for certain debugging features to
+   * be enabled.
+   */
+  private val isBuildDebuggable = Build.IS_DEBUGGABLE
+  /** This flag indicates if udfps should use debug repos to supply data to its various views. */
+  private val udfpsEnrollmentDebugEnabled = true
+
+  override fun isDebuggingEnabled(): Boolean {
+    return isBuildDebuggable
+  }
+
+  override fun isUdfpsEnrollmentDebuggingEnabled(): Boolean {
+    return isDebuggingEnabled() && udfpsEnrollmentDebugEnabled
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/data/repository/SimulatedTouchEventsRepository.kt b/src/com/android/settings/biometrics/fingerprint2/data/repository/SimulatedTouchEventsRepository.kt
new file mode 100644
index 0000000..a3bcb12
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/data/repository/SimulatedTouchEventsRepository.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.data.repository
+
+import android.graphics.Point
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * This repository simulates touch events. This is mainly used to debug accessibility and ensure
+ * that talkback is correct.
+ */
+interface SimulatedTouchEventsRepository {
+  /**
+   * A flow simulating user touches.
+   */
+  val touchExplorationDebug: Flow<Point>
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/data/repository/UdfpsEnrollDebugRepository.kt b/src/com/android/settings/biometrics/fingerprint2/data/repository/UdfpsEnrollDebugRepository.kt
new file mode 100644
index 0000000..9b74813
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/data/repository/UdfpsEnrollDebugRepository.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.data.repository
+
+import android.graphics.Point
+import android.graphics.Rect
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintEnrollInteractor
+import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason
+import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
+import com.android.systemui.biometrics.shared.model.FingerprintSensor
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
+
+/**
+ * This class is used to simulate enroll data. This has two major use cases. 1). Ease of Development
+ * 2). Bug Fixes
+ */
+class UdfpsEnrollDebugRepositoryImpl :
+  FingerprintEnrollInteractor, FingerprintSensorRepository, SimulatedTouchEventsRepository {
+
+  override suspend fun enroll(hardwareAuthToken: ByteArray?, enrollReason: EnrollReason) = flow {
+    emit(FingerEnrollState.OverlayShown)
+    delay(200)
+    emit(FingerEnrollState.EnrollHelp(helpMsgId, "Hello world"))
+    delay(200)
+    emit(FingerEnrollState.EnrollProgress(15, 16))
+    delay(300)
+    emit(FingerEnrollState.EnrollHelp(helpMsgId, "Hello world"))
+    delay(1000)
+    emit(FingerEnrollState.EnrollProgress(14, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(13, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(12, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(11, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(10, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(9, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(8, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(7, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(6, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(5, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(4, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(3, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(2, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(1, 16))
+    delay(500)
+    emit(FingerEnrollState.EnrollProgress(0, 16))
+  }
+
+  /** Provides touch events to the UdfpsEnrollFragment */
+  override val touchExplorationDebug: Flow<Point> = flow {
+    delay(2000)
+    emit(pointToLeftOfSensor(sensorRect))
+    delay(2000)
+    emit(pointBelowSensor(sensorRect))
+    delay(2000)
+    emit(pointToRightOfSensor(sensorRect))
+    delay(2000)
+    emit(pointAboveSensor(sensorRect))
+  }
+
+  override val fingerprintSensor: Flow<FingerprintSensor> = flowOf(sensorProps)
+
+  private fun pointToLeftOfSensor(sensorLocation: Rect) =
+    Point(sensorLocation.right + 5, sensorLocation.centerY())
+
+  private fun pointToRightOfSensor(sensorLocation: Rect) =
+    Point(sensorLocation.left - 5, sensorLocation.centerY())
+
+  private fun pointBelowSensor(sensorLocation: Rect) =
+    Point(sensorLocation.centerX(), sensorLocation.bottom + 5)
+
+  private fun pointAboveSensor(sensorLocation: Rect) =
+    Point(sensorLocation.centerX(), sensorLocation.top - 5)
+
+  companion object {
+
+    private val helpMsgId: Int = 1
+    private val sensorLocationInternal = Pair(540, 1713)
+    private val sensorRadius = 100
+    private val sensorRect =
+      Rect(
+        this.sensorLocationInternal.first - sensorRadius,
+        this.sensorLocationInternal.second - sensorRadius,
+        this.sensorLocationInternal.first + sensorRadius,
+        this.sensorLocationInternal.second + sensorRadius,
+      )
+    val sensorProps =
+      FingerprintSensor(
+        1,
+        SensorStrength.STRONG,
+        5,
+        FingerprintSensorType.UDFPS_OPTICAL,
+        sensorRect,
+        sensorRadius,
+      )
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/DebuggingInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/DebuggingInteractor.kt
new file mode 100644
index 0000000..3edca96
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/DebuggingInteractor.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.domain.interactor
+
+import com.android.settings.biometrics.fingerprint2.data.repository.DebuggingRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+/** Interactor indicating if certain debug flows are enabled. */
+interface DebuggingInteractor {
+  /** This indicates that certain debug flows are enabled. */
+  val debuggingEnabled: Flow<Boolean>
+  /** This indicates if udfps should instead use debug repos to supply data to its various views. */
+  val udfpsEnrollmentDebuggingEnabled: Flow<Boolean>
+}
+
+/**
+ * This interactor essentially forwards the [DebuggingRepository]
+ */
+class DebuggingInteractorImpl(val debuggingRepository: DebuggingRepository) : DebuggingInteractor {
+  override val debuggingEnabled: Flow<Boolean> = flow {
+    emit(debuggingRepository.isDebuggingEnabled())
+  }
+  override val udfpsEnrollmentDebuggingEnabled: Flow<Boolean> = flow {
+    emit(debuggingRepository.isUdfpsEnrollmentDebuggingEnabled())
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/DisplayDensityInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/DisplayDensityInteractor.kt
new file mode 100644
index 0000000..67c0001
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/DisplayDensityInteractor.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.domain.interactor
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.update
+
+/**
+ * This class is responsible for handling updates to fontScale and displayDensity and forwarding
+ * these events to classes that need them
+ */
+interface DisplayDensityInteractor {
+  /** Indicates the display density has been updated. */
+  fun updateDisplayDensity(density: Int)
+
+  /** Indicates the font scale has been updates. */
+  fun updateFontScale(fontScale: Float)
+
+  /** A flow that propagates fontscale. */
+  val fontScale: Flow<Float>
+
+  /** A flow that propagates displayDensity. */
+  val displayDensity: Flow<Int>
+
+  /** A flow that propagates the default display density. */
+  val defaultDisplayDensity: Flow<Int>
+}
+
+/**
+ * Implementation of the [DisplayDensityInteractor]. This interactor is used to forward activity
+ * information to the rest of the application.
+ */
+class DisplayDensityInteractorImpl(
+  currentFontScale: Float,
+  currentDisplayDensity: Int,
+  defaultDisplayDensity: Int,
+  scope: CoroutineScope,
+) : DisplayDensityInteractor {
+  override fun updateDisplayDensity(density: Int) {
+    _displayDensity.update { density }
+  }
+
+  override fun updateFontScale(fontScale: Float) {
+    _fontScale.update { fontScale }
+  }
+
+  private val _fontScale = MutableStateFlow(currentFontScale)
+  private val _displayDensity = MutableStateFlow(currentDisplayDensity)
+
+  override val fontScale: Flow<Float> = _fontScale.asStateFlow()
+
+  override val displayDensity: Flow<Int> = _displayDensity.asStateFlow()
+
+  override val defaultDisplayDensity: Flow<Int> =
+    flowOf(defaultDisplayDensity).shareIn(scope, SharingStarted.Eagerly, 1)
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt
new file mode 100644
index 0000000..2d4cb40
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.domain.interactor
+
+import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+typealias EnrollStageThresholds = Map<Float, StageViewModel>
+
+/** Interactor that provides enroll stages for enrollment. */
+interface EnrollStageInteractor {
+
+  /** Provides enroll stages for enrollment. */
+  val enrollStageThresholds: Flow<EnrollStageThresholds>
+}
+
+class EnrollStageInteractorImpl() : EnrollStageInteractor {
+  override val enrollStageThresholds: Flow<EnrollStageThresholds> =
+    flowOf(
+      mapOf(
+        0.0f to StageViewModel.Center,
+        0.25f to StageViewModel.Guided,
+        0.5f to StageViewModel.Fingertip,
+        0.75f to StageViewModel.LeftEdge,
+        0.875f to StageViewModel.RightEdge,
+      )
+    )
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintEnrollInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintEnrollInteractor.kt
new file mode 100644
index 0000000..e4180d3
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintEnrollInteractor.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.domain.interactor
+
+import android.content.Context
+import android.hardware.fingerprint.FingerprintEnrollOptions
+import android.hardware.fingerprint.FingerprintManager
+import android.os.CancellationSignal
+import android.util.Log
+import com.android.settings.biometrics.fingerprint2.conversion.Util.toEnrollError
+import com.android.settings.biometrics.fingerprint2.conversion.Util.toOriginalReason
+import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason
+import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
+import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintFlow
+import com.android.settings.biometrics.fingerprint2.lib.model.SetupWizard
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.channels.onFailure
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.update
+
+/** This repository is responsible for collecting all state related to the enroll API. */
+interface FingerprintEnrollInteractor {
+
+  /**
+   * By calling this function, [fingerEnrollState] will begin to be populated with data on success.
+   */
+  suspend fun enroll(
+    hardwareAuthToken: ByteArray?,
+    enrollReason: EnrollReason,
+  ): Flow<FingerEnrollState>
+}
+
+class FingerprintEnrollInteractorImpl(
+  private val applicationContext: Context,
+  private val fingerprintEnrollOptions: FingerprintEnrollOptions,
+  private val fingerprintManager: FingerprintManager,
+  private val fingerprintFlow: FingerprintFlow,
+) : FingerprintEnrollInteractor {
+  private val enrollRequestOutstanding = MutableStateFlow(false)
+
+  override suspend fun enroll(
+    hardwareAuthToken: ByteArray?,
+    enrollReason: EnrollReason,
+  ): Flow<FingerEnrollState> = callbackFlow {
+    // TODO (b/308456120) Improve this logic
+    if (enrollRequestOutstanding.value) {
+      Log.d(TAG, "Outstanding enroll request, waiting 150ms")
+      delay(150)
+      if (enrollRequestOutstanding.value) {
+        Log.e(TAG, "Request still present, continuing")
+      }
+    }
+
+    enrollRequestOutstanding.update { true }
+
+    var streamEnded = false
+    var totalSteps: Int? = null
+    val enrollmentCallback =
+      object : FingerprintManager.EnrollmentCallback() {
+        override fun onEnrollmentProgress(remaining: Int) {
+          // This is sort of an implementation detail, but unfortunately the API isn't
+          // very expressive. If anything we should look at changing the FingerprintManager API.
+          if (totalSteps == null) {
+            totalSteps = remaining + 1
+          }
+
+          trySend(FingerEnrollState.EnrollProgress(remaining, totalSteps!!)).onFailure { error ->
+            Log.d(TAG, "onEnrollmentProgress($remaining) failed to send, due to $error")
+          }
+
+          if (remaining == 0) {
+            streamEnded = true
+            enrollRequestOutstanding.update { false }
+          }
+        }
+
+        override fun onEnrollmentHelp(helpMsgId: Int, helpString: CharSequence?) {
+          trySend(FingerEnrollState.EnrollHelp(helpMsgId, helpString.toString())).onFailure { error
+            ->
+            Log.d(TAG, "onEnrollmentHelp failed to send, due to $error")
+          }
+        }
+
+        override fun onEnrollmentError(errMsgId: Int, errString: CharSequence?) {
+          trySend(errMsgId.toEnrollError(fingerprintFlow == SetupWizard)).onFailure { error ->
+            Log.d(TAG, "onEnrollmentError failed to send, due to $error")
+          }
+          Log.d(TAG, "onEnrollmentError($errMsgId)")
+          streamEnded = true
+          enrollRequestOutstanding.update { false }
+        }
+      }
+
+    val cancellationSignal = CancellationSignal()
+
+    fingerprintManager.enroll(
+      hardwareAuthToken,
+      cancellationSignal,
+      applicationContext.userId,
+      enrollmentCallback,
+      enrollReason.toOriginalReason(),
+      fingerprintEnrollOptions,
+    )
+    awaitClose {
+      // If the stream has not been ended, and the user has stopped collecting the flow
+      // before it was over, send cancel.
+      if (!streamEnded) {
+        Log.e(TAG, "Cancel is sent from settings for enroll()")
+        cancellationSignal.cancel()
+      }
+    }
+  }
+
+  companion object {
+    private const val TAG = "FingerprintEnrollStateRepository"
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt
index 652bc0c..ca3665c 100644
--- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt
@@ -18,43 +18,25 @@
 
 import android.content.Context
 import android.content.Intent
-import android.hardware.biometrics.BiometricConstants;
-import android.hardware.biometrics.BiometricFingerprintConstants
-import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.fingerprint.FingerprintEnrollOptions;
 import android.hardware.fingerprint.FingerprintManager
 import android.hardware.fingerprint.FingerprintManager.GenerateChallengeCallback
 import android.hardware.fingerprint.FingerprintManager.RemovalCallback
-import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
 import android.os.CancellationSignal
 import android.util.Log
 import com.android.settings.biometrics.GatekeeperPasswordProvider
-import com.android.settings.biometrics.BiometricUtils
-import com.android.settings.biometrics.fingerprint2.conversion.Util.toEnrollError
-import com.android.settings.biometrics.fingerprint2.conversion.Util.toOriginalReason
 import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository
 import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor
 import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason
 import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
 import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel
 import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData
-import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintFlow
-import com.android.settings.biometrics.fingerprint2.lib.model.SetupWizard
 import com.android.settings.password.ChooseLockSettingsHelper
-import com.android.systemui.biometrics.shared.model.toFingerprintSensor
-import com.google.android.setupcompat.util.WizardManagerHelper
 import kotlin.coroutines.resume
 import kotlin.coroutines.suspendCoroutine
 import kotlinx.coroutines.CancellableContinuation
 import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.channels.onFailure
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 
@@ -66,9 +48,7 @@
   private val fingerprintManager: FingerprintManager,
   fingerprintSensorRepository: FingerprintSensorRepository,
   private val gatekeeperPasswordProvider: GatekeeperPasswordProvider,
-  private val pressToAuthInteractor: PressToAuthInteractor,
-  private val fingerprintFlow: FingerprintFlow,
-  private val intent: Intent,
+  private val fingerprintEnrollStateRepository: FingerprintEnrollInteractor,
 ) : FingerprintManagerInteractor {
 
   private val maxFingerprints =
@@ -77,7 +57,6 @@
     )
   private val applicationContext = applicationContext.applicationContext
 
-  private val enrollRequestOutstanding = MutableStateFlow(false)
 
   override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> =
     suspendCoroutine {
@@ -113,85 +92,8 @@
 
   override val maxEnrollableFingerprints = flow { emit(maxFingerprints) }
 
-  override suspend fun enroll(
-    hardwareAuthToken: ByteArray?,
-    enrollReason: EnrollReason,
-  ): Flow<FingerEnrollState> = callbackFlow {
-    // TODO (b/308456120) Improve this logic
-    if (enrollRequestOutstanding.value) {
-      Log.d(TAG, "Outstanding enroll request, waiting 150ms")
-      delay(150)
-      if (enrollRequestOutstanding.value) {
-        Log.e(TAG, "Request still present, continuing")
-      }
-    }
-
-    enrollRequestOutstanding.update { true }
-
-    var streamEnded = false
-    var totalSteps: Int? = null
-    val enrollmentCallback =
-      object : FingerprintManager.EnrollmentCallback() {
-        override fun onEnrollmentProgress(remaining: Int) {
-          // This is sort of an implementation detail, but unfortunately the API isn't
-          // very expressive. If anything we should look at changing the FingerprintManager API.
-          if (totalSteps == null) {
-            totalSteps = remaining + 1
-          }
-
-          trySend(FingerEnrollState.EnrollProgress(remaining, totalSteps!!)).onFailure { error ->
-            Log.d(TAG, "onEnrollmentProgress($remaining) failed to send, due to $error")
-          }
-
-          if (remaining == 0) {
-            streamEnded = true
-            enrollRequestOutstanding.update { false }
-          }
-        }
-
-        override fun onEnrollmentHelp(helpMsgId: Int, helpString: CharSequence?) {
-          trySend(FingerEnrollState.EnrollHelp(helpMsgId, helpString.toString())).onFailure { error
-            ->
-            Log.d(TAG, "onEnrollmentHelp failed to send, due to $error")
-          }
-        }
-
-        override fun onEnrollmentError(errMsgId: Int, errString: CharSequence?) {
-          trySend(errMsgId.toEnrollError(fingerprintFlow == SetupWizard)).onFailure { error ->
-            Log.d(TAG, "onEnrollmentError failed to send, due to $error")
-          }
-          Log.d(TAG, "onEnrollmentError($errMsgId)")
-          streamEnded = true
-          enrollRequestOutstanding.update { false }
-        }
-      }
-
-    val cancellationSignal = CancellationSignal()
-
-    if (intent.getIntExtra(BiometricUtils.EXTRA_ENROLL_REASON, -1) === -1) {
-      val isSuw: Boolean = WizardManagerHelper.isAnySetupWizard(intent)
-      intent.putExtra(BiometricUtils.EXTRA_ENROLL_REASON,
-      if (isSuw) FingerprintEnrollOptions.ENROLL_REASON_SUW else
-        FingerprintEnrollOptions.ENROLL_REASON_SETTINGS)
-    }
-
-    fingerprintManager.enroll(
-      hardwareAuthToken,
-      cancellationSignal,
-      applicationContext.userId,
-      enrollmentCallback,
-      enrollReason.toOriginalReason(),
-      toFingerprintEnrollOptions(intent)
-    )
-    awaitClose {
-      // If the stream has not been ended, and the user has stopped collecting the flow
-      // before it was over, send cancel.
-      if (!streamEnded) {
-        Log.e(TAG, "Cancel is sent from settings for enroll()")
-        cancellationSignal.cancel()
-      }
-    }
-  }
+  override suspend fun enroll(hardwareAuthToken: ByteArray?, enrollReason: EnrollReason): Flow<FingerEnrollState> =
+    fingerprintEnrollStateRepository.enroll(hardwareAuthToken, enrollReason)
 
   override suspend fun removeFingerprint(fp: FingerprintData): Boolean = suspendCoroutine {
     val callback =
@@ -263,14 +165,4 @@
       )
     }
 
-  private fun toFingerprintEnrollOptions(intent: Intent): FingerprintEnrollOptions {
-    val reason: Int =
-      intent.getIntExtra(BiometricUtils.EXTRA_ENROLL_REASON, -1)
-    val builder: FingerprintEnrollOptions.Builder = FingerprintEnrollOptions.Builder()
-    builder.setEnrollReason(FingerprintEnrollOptions.ENROLL_REASON_UNKNOWN)
-    if (reason != -1) {
-      builder.setEnrollReason(reason)
-    }
-    return builder.build()
-  }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt
index 968203f..5d1d8c8 100644
--- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt
@@ -17,6 +17,7 @@
 package com.android.settings.biometrics.fingerprint2.domain.interactor
 
 import android.content.Context
+import android.util.Log
 import android.view.OrientationEventListener
 import com.android.internal.R
 import kotlinx.coroutines.CoroutineScope
@@ -24,17 +25,24 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transform
 
-/**
- * Interactor which provides information about orientation
- */
+/** Interactor which provides information about orientation */
 interface OrientationInteractor {
   /** A flow that contains the information about the orientation changing */
   val orientation: Flow<Int>
-  /** A flow that contains the rotation info */
+  /**
+   * A flow that contains the rotation info
+   */
   val rotation: Flow<Int>
   /**
+   * A flow that contains the rotation info matched against the def [config_reverseDefaultRotation]
+   */
+  val rotationFromDefault: Flow<Int>
+  /**
    * A Helper function that computes rotation if device is in
    * [R.bool.config_reverseDefaultConfigRotation]
    */
@@ -53,24 +61,11 @@
       }
     orientationEventListener.enable()
     awaitClose { orientationEventListener.disable() }
-  }
+  }.shareIn(activityScope, SharingStarted.Eagerly, replay = 1)
 
-  override val rotation: Flow<Int> =
-    callbackFlow {
-      val orientationEventListener =
-        object : OrientationEventListener(context) {
-          override fun onOrientationChanged(orientation: Int) {
-            trySend(getRotationFromDefault(context.display!!.rotation))
-          }
-        }
-      orientationEventListener.enable()
-      awaitClose { orientationEventListener.disable() }
-    }
-      .stateIn(
-        activityScope, // This is tied to the activity scope
-        SharingStarted.WhileSubscribed(), // When no longer subscribed, we removeTheListener
-        context.display!!.rotation,
-      )
+  override val rotation: Flow<Int> = orientation.transform { emit(context.display!!.rotation) }
+
+  override val rotationFromDefault: Flow<Int> = rotation.map { getRotationFromDefault(it) }
 
   override fun getRotationFromDefault(rotation: Int): Int {
     val isReverseDefaultRotation =
@@ -81,4 +76,4 @@
       rotation
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/VibrationInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/VibrationInteractor.kt
new file mode 100644
index 0000000..0f107cc
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/VibrationInteractor.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.domain.interactor
+
+import android.content.Context
+import android.os.Process
+import android.os.VibrationAttributes
+import android.os.VibrationEffect
+import android.os.Vibrator
+
+/** Indicates the possible vibration effects for fingerprint enrollment */
+sealed class FingerprintVibrationEffects {
+  /** A vibration indicating an error */
+  data object UdfpsError : FingerprintVibrationEffects()
+
+  /**
+   * A vibration indicating success, this usually occurs when progress on the UDFPS enrollment has
+   * been made
+   */
+  data object UdfpsSuccess : FingerprintVibrationEffects()
+
+  /** This vibration typically occurs when a help message is shown during UDFPS enrollment */
+  data object UdfpsHelp : FingerprintVibrationEffects()
+}
+/** Interface for sending haptic feedback */
+interface VibrationInteractor {
+  /** This will send a haptic vibration */
+  fun vibrate(effect: FingerprintVibrationEffects, caller: String)
+}
+
+/** Implementation of the VibrationInteractor interface */
+class VibrationInteractorImpl(val vibrator: Vibrator, val applicationContext: Context) :
+  VibrationInteractor {
+  override fun vibrate(effect: FingerprintVibrationEffects, caller: String) {
+    val callerString = "$caller::$effect"
+    val res =
+      when (effect) {
+        FingerprintVibrationEffects.UdfpsHelp,
+        FingerprintVibrationEffects.UdfpsError ->
+          Pair(VIBRATE_EFFECT_ERROR, FINGERPRINT_ENROLLING_SONIFICATION_ATTRIBUTES)
+        FingerprintVibrationEffects.UdfpsSuccess ->
+          Pair(VIBRATE_EFFECT_SUCCESS, HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES)
+      }
+    vibrator.vibrate(
+      Process.myUid(),
+      applicationContext.opPackageName,
+      res.first,
+      callerString,
+      res.second,
+    )
+  }
+
+  companion object {
+    private val VIBRATE_EFFECT_ERROR = VibrationEffect.createWaveform(longArrayOf(0, 5, 55, 60), -1)
+    private val FINGERPRINT_ENROLLING_SONIFICATION_ATTRIBUTES =
+      VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY)
+    private val HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
+      VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK)
+    private val VIBRATE_EFFECT_SUCCESS = VibrationEffect.get(VibrationEffect.EFFECT_CLICK)
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/FingerprintManagerInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/FingerprintManagerInteractor.kt
index 105929d..d2f9f0a 100644
--- a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/FingerprintManagerInteractor.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/FingerprintManagerInteractor.kt
@@ -57,8 +57,7 @@
 
   /**
    * Runs [FingerprintManager.enroll] with the [hardwareAuthToken] and [EnrollReason] for this
-   * enrollment. Returning the [FingerEnrollState] that represents this fingerprint enrollment
-   * state.
+   * enrollment. If successful data in the [fingerprintEnrollState] should be populated.
    */
   suspend fun enroll(
     hardwareAuthToken: ByteArray?,
diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/model/FingerEnrollState.kt b/src/com/android/settings/biometrics/fingerprint2/lib/model/FingerEnrollState.kt
index 683397f..24a9a86 100644
--- a/src/com/android/settings/biometrics/fingerprint2/lib/model/FingerEnrollState.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/lib/model/FingerEnrollState.kt
@@ -42,4 +42,16 @@
     val shouldRetryEnrollment: Boolean,
     val isCancelled: Boolean,
   ) : FingerEnrollState()
+
+  /** Indicates an acquired event has occurred */
+  data class Acquired(val acquiredGood: Boolean) : FingerEnrollState()
+
+  /** Indicates a pointer down event has occurred */
+  data object PointerDown : FingerEnrollState()
+
+  /** Indicates a pointer up event has occurred */
+  data object PointerUp : FingerEnrollState()
+
+  /** Indicates the overlay has shown */
+  data object OverlayShown : FingerEnrollState()
 }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/StageViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/lib/model/StageViewModel.kt
similarity index 94%
rename from src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/StageViewModel.kt
rename to src/com/android/settings/biometrics/fingerprint2/lib/model/StageViewModel.kt
index 75eaec7..81bba15 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/StageViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/lib/model/StageViewModel.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
+package com.android.settings.biometrics.fingerprint2.lib.model
 
 /**
  * A view model that describes the various stages of UDFPS Enrollment. This stages typically update
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt
index 70d58ea..c8e9ca3 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt
@@ -19,8 +19,10 @@
 import android.app.Activity
 import android.content.Intent
 import android.content.res.Configuration
+import android.hardware.fingerprint.FingerprintEnrollOptions
 import android.hardware.fingerprint.FingerprintManager
 import android.os.Bundle
+import android.os.Vibrator
 import android.util.Log
 import android.view.accessibility.AccessibilityManager
 import androidx.activity.result.contract.ActivityResultContracts
@@ -35,21 +37,35 @@
 import com.android.settings.biometrics.BiometricEnrollBase
 import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST
 import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED
+import com.android.settings.biometrics.BiometricUtils
 import com.android.settings.biometrics.GatekeeperPasswordProvider
+import com.android.settings.biometrics.fingerprint2.data.repository.DebuggingRepositoryImpl
 import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepositoryImpl
+import com.android.settings.biometrics.fingerprint2.data.repository.UdfpsEnrollDebugRepositoryImpl
 import com.android.settings.biometrics.fingerprint2.domain.interactor.AccessibilityInteractorImpl
+import com.android.settings.biometrics.fingerprint2.domain.interactor.DebuggingInteractorImpl
+import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractor
+import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractorImpl
+import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractor
+import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractorImpl
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintEnrollInteractorImpl
 import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
 import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateInteractor
 import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateInteractorImpl
 import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor
 import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractorImpl
-import com.android.settings.biometrics.fingerprint2.domain.interactor.PressToAuthInteractorImpl
+import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor
+import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractorImpl
 import com.android.settings.biometrics.fingerprint2.lib.model.Default
+import com.android.settings.biometrics.fingerprint2.lib.model.Settings
 import com.android.settings.biometrics.fingerprint2.lib.model.SetupWizard
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollConfirmationV2Fragment
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollEnrollingV2Fragment
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollFindSensorV2Fragment
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollIntroV2Fragment
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.education.RfpsEnrollFindSensorFragment
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.education.SfpsEnrollFindSensorFragment
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.education.UdfpsEnrollFindSensorFragment
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.util.toFingerprintEnrollOptions
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.fragment.RFPSEnrollFragment
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSViewModel
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.fragment.UdfpsEnrollFragment
@@ -77,6 +93,7 @@
 import com.android.settings.password.ChooseLockGeneric
 import com.android.settings.password.ChooseLockSettingsHelper
 import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE
+import com.android.settingslib.display.DisplayDensityUtils
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.google.android.setupcompat.util.WizardManagerHelper
 import com.google.android.setupdesign.util.ThemeHelper
@@ -95,14 +112,17 @@
   private lateinit var navigationViewModel: FingerprintNavigationViewModel
   private lateinit var gatekeeperViewModel: FingerprintGatekeeperViewModel
   private lateinit var fingerprintEnrollViewModel: FingerprintEnrollViewModel
+  private lateinit var vibrationInteractor: VibrationInteractor
   private lateinit var foldStateInteractor: FoldStateInteractor
   private lateinit var orientationInteractor: OrientationInteractor
+  private lateinit var displayDensityInteractor: DisplayDensityInteractor
   private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel
   private lateinit var backgroundViewModel: BackgroundViewModel
   private lateinit var fingerprintFlowViewModel: FingerprintFlowViewModel
   private lateinit var fingerprintEnrollConfirmationViewModel:
     FingerprintEnrollConfirmationViewModel
   private lateinit var udfpsViewModel: UdfpsViewModel
+  private lateinit var enrollStageInteractor: EnrollStageInteractor
   private val coroutineDispatcher = Dispatchers.Default
 
   /** Result listener for ChooseLock activity flow. */
@@ -135,6 +155,12 @@
   override fun onConfigurationChanged(newConfig: Configuration) {
     super.onConfigurationChanged(newConfig)
     foldStateInteractor.onConfigurationChange(newConfig)
+    val displayDensityUtils = DisplayDensityUtils(applicationContext)
+    val currIndex = displayDensityUtils.currentIndexForDefaultDisplay
+    displayDensityInteractor.updateFontScale(resources.configuration.fontScale)
+    displayDensityInteractor.updateDisplayDensity(
+      displayDensityUtils.defaultDisplayDensityValues[currIndex]
+    )
   }
 
   private fun onConfirmDevice(resultCode: Int, data: Intent?) {
@@ -193,10 +219,43 @@
     fingerprintFlowViewModel =
       ViewModelProvider(this, FingerprintFlowViewModel.FingerprintFlowViewModelFactory(enrollType))[
         FingerprintFlowViewModel::class.java]
+    val displayDensityUtils = DisplayDensityUtils(context)
+    val currIndex = displayDensityUtils.currentIndexForDefaultDisplay
+    val defaultDisplayDensity = displayDensityUtils.defaultDensityForDefaultDisplay
+    displayDensityInteractor =
+      DisplayDensityInteractorImpl(
+        resources.configuration.fontScale,
+        displayDensityUtils.defaultDisplayDensityValues[currIndex],
+        defaultDisplayDensity,
+        lifecycleScope,
+      )
+
+    val debuggingRepo = DebuggingRepositoryImpl()
+    val debuggingInteractor = DebuggingInteractorImpl(debuggingRepo)
+    val udfpsEnrollDebugRepositoryImpl = UdfpsEnrollDebugRepositoryImpl()
 
     val fingerprintSensorRepo =
-      FingerprintSensorRepositoryImpl(fingerprintManager, backgroundDispatcher, lifecycleScope)
-    val pressToAuthInteractor = PressToAuthInteractorImpl(context, backgroundDispatcher)
+      if (debuggingRepo.isUdfpsEnrollmentDebuggingEnabled()) udfpsEnrollDebugRepositoryImpl
+      else FingerprintSensorRepositoryImpl(fingerprintManager, backgroundDispatcher, lifecycleScope)
+
+    if (intent.getIntExtra(BiometricUtils.EXTRA_ENROLL_REASON, -1) === -1) {
+      val isSuw: Boolean = WizardManagerHelper.isAnySetupWizard(intent)
+      intent.putExtra(
+        BiometricUtils.EXTRA_ENROLL_REASON,
+        if (isSuw) FingerprintEnrollOptions.ENROLL_REASON_SUW
+        else FingerprintEnrollOptions.ENROLL_REASON_SETTINGS,
+      )
+    }
+
+    val fingerprintEnrollStateRepository =
+      if (debuggingRepo.isUdfpsEnrollmentDebuggingEnabled()) udfpsEnrollDebugRepositoryImpl
+      else
+        FingerprintEnrollInteractorImpl(
+          context.applicationContext,
+          intent.toFingerprintEnrollOptions(),
+          fingerprintManager,
+          Settings,
+        )
 
     val fingerprintManagerInteractor =
       FingerprintManagerInteractorImpl(
@@ -205,12 +264,10 @@
         fingerprintManager,
         fingerprintSensorRepo,
         GatekeeperPasswordProvider(LockPatternUtils(context)),
-        pressToAuthInteractor,
-        enrollType,
-        getIntent(),
+        fingerprintEnrollStateRepository,
       )
 
-    var challenge: Long? = intent.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
+    var challenge = intent.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
     val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
     val gatekeeperInfo = FingerprintGatekeeperViewModel.toGateKeeperInfo(challenge, token)
 
@@ -256,6 +313,8 @@
     foldStateInteractor.onConfigurationChange(resources.configuration)
 
     orientationInteractor = OrientationInteractorImpl(context, lifecycleScope)
+    vibrationInteractor =
+      VibrationInteractorImpl(context.getSystemService(Vibrator::class.java)!!, context)
 
     // Initialize FingerprintViewModel
     fingerprintEnrollViewModel =
@@ -309,10 +368,23 @@
       ),
     )[RFPSViewModel::class.java]
 
+    enrollStageInteractor = EnrollStageInteractorImpl()
+
     udfpsViewModel =
       ViewModelProvider(
         this,
-        UdfpsViewModel.UdfpsEnrollmentFactory(),
+        UdfpsViewModel.UdfpsEnrollmentFactory(
+          vibrationInteractor,
+          displayDensityInteractor,
+          navigationViewModel,
+          debuggingInteractor,
+          fingerprintEnrollEnrollingViewModel,
+          udfpsEnrollDebugRepositoryImpl,
+          enrollStageInteractor,
+          orientationInteractor,
+          backgroundViewModel,
+          fingerprintSensorRepo,
+        ),
       )[UdfpsViewModel::class.java]
 
     fingerprintEnrollConfirmationViewModel =
@@ -348,7 +420,12 @@
             when (step) {
               Confirmation -> FingerprintEnrollConfirmationV2Fragment()
               is Education -> {
-                FingerprintEnrollFindSensorV2Fragment(step.sensor.sensorType)
+                when (step.sensor.sensorType) {
+                  FingerprintSensorType.REAR -> RfpsEnrollFindSensorFragment()
+                  FingerprintSensorType.UDFPS_OPTICAL,
+                  FingerprintSensorType.UDFPS_ULTRASONIC -> UdfpsEnrollFindSensorFragment()
+                  else -> SfpsEnrollFindSensorFragment()
+                }
               }
               is Enrollment -> {
                 when (step.sensor.sensorType) {
@@ -370,7 +447,7 @@
             supportFragmentManager
               .beginTransaction()
               .setReorderingAllowed(true)
-              .add(R.id.fragment_container_view, theClass, null)
+              .add(R.id.fragment_container_view, theClass::class.java, null)
               .commit()
             navigationViewModel.update(
               FingerprintAction.TRANSITION_FINISHED,
@@ -386,7 +463,7 @@
       navigationViewModel.shouldFinish.filterNotNull().collect {
         Log.d(TAG, "FingerprintSettingsNav.finishing($it)")
         if (it.result != null) {
-          finishActivity(it.result as Int)
+          finishActivity(it.result)
         } else {
           finish()
         }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/education/RfpsEnrollFindSensorFragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/education/RfpsEnrollFindSensorFragment.kt
new file mode 100644
index 0000000..5ef1770
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/education/RfpsEnrollFindSensorFragment.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.education
+
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.VisibleForTesting
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog
+import com.android.settings.biometrics.fingerprint.FingerprintFindSensorAnimation
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel
+import com.google.android.setupcompat.template.FooterBarMixin
+import com.google.android.setupcompat.template.FooterButton
+import com.google.android.setupdesign.GlifLayout
+import kotlinx.coroutines.launch
+
+/**
+ * A fragment that is used to educate the user about the rear fingerprint sensor on this device.
+ *
+ * The main goals of this page are
+ * 1. Inform the user where the fingerprint sensor is on their device
+ * 2. Explain to the user how the enrollment process shown by [FingerprintEnrollEnrollingV2Fragment]
+ *    will work.
+ */
+class RfpsEnrollFindSensorFragment() : Fragment() {
+  /** Used for testing purposes */
+  private var factory: ViewModelProvider.Factory? = null
+
+  @VisibleForTesting
+  constructor(theFactory: ViewModelProvider.Factory) : this() {
+    factory = theFactory
+  }
+
+  private val viewModelProvider: ViewModelProvider by lazy {
+    if (factory != null) {
+      ViewModelProvider(requireActivity(), factory!!)
+    } else {
+      ViewModelProvider(requireActivity())
+    }
+  }
+
+  private var animation: FingerprintFindSensorAnimation? = null
+
+  private val viewModel: FingerprintEnrollFindSensorViewModel by lazy {
+    viewModelProvider[FingerprintEnrollFindSensorViewModel::class.java]
+  }
+
+  override fun onCreateView(
+    inflater: LayoutInflater,
+    container: ViewGroup?,
+    savedInstanceState: Bundle?,
+  ): View? {
+    val view =
+      inflater.inflate(R.layout.fingerprint_v2_enroll_find_sensor, container, false)!! as GlifLayout
+    view.setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title)
+    view.setDescriptionText(R.string.security_settings_fingerprint_enroll_find_sensor_message)
+
+    // Set up footer bar
+    val footerBarMixin = view.getMixin(FooterBarMixin::class.java)
+    setupSecondaryButton(footerBarMixin)
+    lifecycleScope.launch {
+      viewModel.showPrimaryButton.collect { setupPrimaryButton(footerBarMixin) }
+    }
+
+    lifecycleScope.launch {
+      viewModel.showRfpsAnimation.collect {
+        animation = view.findViewById(R.id.fingerprint_sensor_location_animation)
+        animation!!.startAnimation()
+      }
+    }
+
+    lifecycleScope.launch {
+      viewModel.showErrorDialog.collect { (errMsgId, isSetup) ->
+        // TODO: Covert error dialog kotlin as well
+        FingerprintErrorDialog.showErrorDialog(requireActivity(), errMsgId, isSetup)
+      }
+    }
+    return view
+  }
+
+  override fun onDestroy() {
+    animation?.stopAnimation()
+    super.onDestroy()
+  }
+
+  private fun setupSecondaryButton(footerBarMixin: FooterBarMixin) {
+    footerBarMixin.secondaryButton =
+      FooterButton.Builder(requireActivity())
+        .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
+        .setListener { viewModel.secondaryButtonClicked() }
+        .setButtonType(FooterButton.ButtonType.SKIP)
+        .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
+        .build()
+  }
+
+  private fun setupPrimaryButton(footerBarMixin: FooterBarMixin) {
+    footerBarMixin.primaryButton =
+      FooterButton.Builder(requireActivity())
+        .setText(R.string.security_settings_udfps_enroll_find_sensor_start_button)
+        .setListener {
+          Log.d(TAG, "onStartButtonClick")
+          viewModel.proceedToEnrolling()
+        }
+        .setButtonType(FooterButton.ButtonType.NEXT)
+        .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
+        .build()
+  }
+
+  companion object {
+    private const val TAG = "RfpsEnrollFindSensor"
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/education/SfpsEnrollFindSensorFragment.kt
similarity index 64%
copy from src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt
copy to src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/education/SfpsEnrollFindSensorFragment.kt
index 2b1ff9b..584824d 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/education/SfpsEnrollFindSensorFragment.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment
+package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.education
 
 import android.os.Bundle
 import android.util.Log
@@ -29,36 +29,26 @@
 import com.airbnb.lottie.LottieAnimationView
 import com.android.settings.R
 import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog
-import com.android.settings.biometrics.fingerprint.FingerprintFindSensorAnimation
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel
-import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.google.android.setupcompat.template.FooterBarMixin
 import com.google.android.setupcompat.template.FooterButton
 import com.google.android.setupdesign.GlifLayout
 import kotlinx.coroutines.launch
 
-private const val TAG = "FingerprintEnrollFindSensorV2Fragment"
-
 /**
- * A fragment that is used to educate the user about the fingerprint sensor on this device.
- *
- * If the sensor is not a udfps sensor, this fragment listens to fingerprint enrollment for
- * proceeding to the enroll enrolling.
+ * A fragment that is used to educate the user about the side fingerprint sensor on this device.
  *
  * The main goals of this page are
  * 1. Inform the user where the fingerprint sensor is on their device
  * 2. Explain to the user how the enrollment process shown by [FingerprintEnrollEnrollingV2Fragment]
  *    will work.
  */
-class FingerprintEnrollFindSensorV2Fragment(val sensorType: FingerprintSensorType) : Fragment() {
+class SfpsEnrollFindSensorFragment() : Fragment() {
   /** Used for testing purposes */
   private var factory: ViewModelProvider.Factory? = null
 
   @VisibleForTesting
-  constructor(
-    sensorType: FingerprintSensorType,
-    theFactory: ViewModelProvider.Factory,
-  ) : this(sensorType) {
+  constructor(theFactory: ViewModelProvider.Factory) : this() {
     factory = theFactory
   }
 
@@ -70,10 +60,6 @@
     }
   }
 
-  // This is only for non-udfps or non-sfps sensor. For udfps and sfps, we show lottie.
-  private var animation: FingerprintFindSensorAnimation? = null
-
-  private var contentLayoutId: Int = -1
   private val viewModel: FingerprintEnrollFindSensorViewModel by lazy {
     viewModelProvider[FingerprintEnrollFindSensorViewModel::class.java]
   }
@@ -83,43 +69,24 @@
     container: ViewGroup?,
     savedInstanceState: Bundle?,
   ): View? {
-
-    contentLayoutId =
-      when (sensorType) {
-        FingerprintSensorType.UDFPS_OPTICAL,
-        FingerprintSensorType.UDFPS_ULTRASONIC -> R.layout.udfps_enroll_find_sensor_layout
-        FingerprintSensorType.POWER_BUTTON -> R.layout.sfps_enroll_find_sensor_layout
-        else -> R.layout.fingerprint_v2_enroll_find_sensor
-      }
-
-    val view = inflater.inflate(contentLayoutId, container, false)!! as GlifLayout
-    setTexts(sensorType, view)
+    val view =
+      inflater.inflate(R.layout.sfps_enroll_find_sensor_layout, container, false)!! as GlifLayout
+    view.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
+    view.setDescriptionText(R.string.security_settings_sfps_enroll_find_sensor_message)
 
     // Set up footer bar
     val footerBarMixin = view.getMixin(FooterBarMixin::class.java)
     setupSecondaryButton(footerBarMixin)
-    lifecycleScope.launch {
-      viewModel.showPrimaryButton.collect { setupPrimaryButton(footerBarMixin) }
-    }
 
-    // Set up lottie or animation
+    // Set up lottie
     lifecycleScope.launch {
       viewModel.sfpsLottieInfo.collect { (isFolded, rotation) ->
         setupLottie(view, getSfpsIllustrationLottieAnimation(isFolded, rotation))
       }
     }
+
     lifecycleScope.launch {
-      viewModel.udfpsLottieInfo.collect { isAccessibilityEnabled ->
-        val lottieAnimation =
-          if (isAccessibilityEnabled) R.raw.udfps_edu_a11y_lottie else R.raw.udfps_edu_lottie
-        setupLottie(view, lottieAnimation) { viewModel.proceedToEnrolling() }
-      }
-    }
-    lifecycleScope.launch {
-      viewModel.showRfpsAnimation.collect {
-        animation = view.findViewById(R.id.fingerprint_sensor_location_animation)
-        animation!!.startAnimation()
-      }
+      viewModel.showPrimaryButton.collect { setupPrimaryButton(footerBarMixin) }
     }
 
     lifecycleScope.launch {
@@ -131,11 +98,6 @@
     return view
   }
 
-  override fun onDestroy() {
-    animation?.stopAnimation()
-    super.onDestroy()
-  }
-
   private fun setupSecondaryButton(footerBarMixin: FooterBarMixin) {
     footerBarMixin.secondaryButton =
       FooterButton.Builder(requireActivity())
@@ -159,36 +121,6 @@
         .build()
   }
 
-  private fun setupLottie(
-    view: View,
-    lottieAnimation: Int,
-    lottieClickListener: View.OnClickListener? = null,
-  ) {
-    val illustrationLottie: LottieAnimationView? = view.findViewById(R.id.illustration_lottie)
-    illustrationLottie?.setAnimation(lottieAnimation)
-    illustrationLottie?.playAnimation()
-    illustrationLottie?.setOnClickListener(lottieClickListener)
-    illustrationLottie?.visibility = View.VISIBLE
-  }
-
-  private fun setTexts(sensorType: FingerprintSensorType?, view: GlifLayout) {
-    when (sensorType) {
-      FingerprintSensorType.UDFPS_OPTICAL,
-      FingerprintSensorType.UDFPS_ULTRASONIC -> {
-        view.setHeaderText(R.string.security_settings_udfps_enroll_find_sensor_title)
-        view.setDescriptionText(R.string.security_settings_udfps_enroll_find_sensor_message)
-      }
-      FingerprintSensorType.POWER_BUTTON -> {
-        view.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
-        view.setDescriptionText(R.string.security_settings_sfps_enroll_find_sensor_message)
-      }
-      else -> {
-        view.setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title)
-        view.setDescriptionText(R.string.security_settings_fingerprint_enroll_find_sensor_message)
-      }
-    }
-  }
-
   private fun getSfpsIllustrationLottieAnimation(isFolded: Boolean, rotation: Int): Int {
     val animation: Int
     when (rotation) {
@@ -211,4 +143,20 @@
     }
     return animation
   }
+
+  private fun setupLottie(
+    view: View,
+    lottieAnimation: Int,
+    lottieClickListener: View.OnClickListener? = null,
+  ) {
+    val illustrationLottie: LottieAnimationView? = view.findViewById(R.id.illustration_lottie)
+    illustrationLottie?.setAnimation(lottieAnimation)
+    illustrationLottie?.playAnimation()
+    illustrationLottie?.setOnClickListener(lottieClickListener)
+    illustrationLottie?.visibility = View.VISIBLE
+  }
+
+  companion object {
+    private const val TAG = "SfpsEnrollFindSensor"
+  }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/education/UdfpsEnrollFindSensorFragment.kt
similarity index 66%
rename from src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt
rename to src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/education/UdfpsEnrollFindSensorFragment.kt
index 2b1ff9b..923a309 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/education/UdfpsEnrollFindSensorFragment.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment
+package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.education
 
 import android.os.Bundle
 import android.util.Log
@@ -29,36 +29,27 @@
 import com.airbnb.lottie.LottieAnimationView
 import com.android.settings.R
 import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog
-import com.android.settings.biometrics.fingerprint.FingerprintFindSensorAnimation
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel
-import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.google.android.setupcompat.template.FooterBarMixin
 import com.google.android.setupcompat.template.FooterButton
 import com.google.android.setupdesign.GlifLayout
 import kotlinx.coroutines.launch
 
-private const val TAG = "FingerprintEnrollFindSensorV2Fragment"
-
 /**
- * A fragment that is used to educate the user about the fingerprint sensor on this device.
- *
- * If the sensor is not a udfps sensor, this fragment listens to fingerprint enrollment for
- * proceeding to the enroll enrolling.
+ * A fragment that is used to educate the user about the under display fingerprint sensor on this
+ * device.
  *
  * The main goals of this page are
  * 1. Inform the user where the fingerprint sensor is on their device
  * 2. Explain to the user how the enrollment process shown by [FingerprintEnrollEnrollingV2Fragment]
  *    will work.
  */
-class FingerprintEnrollFindSensorV2Fragment(val sensorType: FingerprintSensorType) : Fragment() {
+class UdfpsEnrollFindSensorFragment() : Fragment() {
   /** Used for testing purposes */
   private var factory: ViewModelProvider.Factory? = null
 
   @VisibleForTesting
-  constructor(
-    sensorType: FingerprintSensorType,
-    theFactory: ViewModelProvider.Factory,
-  ) : this(sensorType) {
+  constructor(theFactory: ViewModelProvider.Factory) : this() {
     factory = theFactory
   }
 
@@ -70,10 +61,6 @@
     }
   }
 
-  // This is only for non-udfps or non-sfps sensor. For udfps and sfps, we show lottie.
-  private var animation: FingerprintFindSensorAnimation? = null
-
-  private var contentLayoutId: Int = -1
   private val viewModel: FingerprintEnrollFindSensorViewModel by lazy {
     viewModelProvider[FingerprintEnrollFindSensorViewModel::class.java]
   }
@@ -83,31 +70,18 @@
     container: ViewGroup?,
     savedInstanceState: Bundle?,
   ): View? {
-
-    contentLayoutId =
-      when (sensorType) {
-        FingerprintSensorType.UDFPS_OPTICAL,
-        FingerprintSensorType.UDFPS_ULTRASONIC -> R.layout.udfps_enroll_find_sensor_layout
-        FingerprintSensorType.POWER_BUTTON -> R.layout.sfps_enroll_find_sensor_layout
-        else -> R.layout.fingerprint_v2_enroll_find_sensor
-      }
-
-    val view = inflater.inflate(contentLayoutId, container, false)!! as GlifLayout
-    setTexts(sensorType, view)
+    val view =
+      inflater.inflate(R.layout.udfps_enroll_find_sensor_layout, container, false)!! as GlifLayout
+    view.setHeaderText(R.string.security_settings_udfps_enroll_find_sensor_title)
+    view.setDescriptionText(R.string.security_settings_udfps_enroll_find_sensor_message)
 
     // Set up footer bar
     val footerBarMixin = view.getMixin(FooterBarMixin::class.java)
     setupSecondaryButton(footerBarMixin)
+
     lifecycleScope.launch {
       viewModel.showPrimaryButton.collect { setupPrimaryButton(footerBarMixin) }
     }
-
-    // Set up lottie or animation
-    lifecycleScope.launch {
-      viewModel.sfpsLottieInfo.collect { (isFolded, rotation) ->
-        setupLottie(view, getSfpsIllustrationLottieAnimation(isFolded, rotation))
-      }
-    }
     lifecycleScope.launch {
       viewModel.udfpsLottieInfo.collect { isAccessibilityEnabled ->
         val lottieAnimation =
@@ -115,12 +89,6 @@
         setupLottie(view, lottieAnimation) { viewModel.proceedToEnrolling() }
       }
     }
-    lifecycleScope.launch {
-      viewModel.showRfpsAnimation.collect {
-        animation = view.findViewById(R.id.fingerprint_sensor_location_animation)
-        animation!!.startAnimation()
-      }
-    }
 
     lifecycleScope.launch {
       viewModel.showErrorDialog.collect { (errMsgId, isSetup) ->
@@ -131,11 +99,6 @@
     return view
   }
 
-  override fun onDestroy() {
-    animation?.stopAnimation()
-    super.onDestroy()
-  }
-
   private fun setupSecondaryButton(footerBarMixin: FooterBarMixin) {
     footerBarMixin.secondaryButton =
       FooterButton.Builder(requireActivity())
@@ -159,36 +122,6 @@
         .build()
   }
 
-  private fun setupLottie(
-    view: View,
-    lottieAnimation: Int,
-    lottieClickListener: View.OnClickListener? = null,
-  ) {
-    val illustrationLottie: LottieAnimationView? = view.findViewById(R.id.illustration_lottie)
-    illustrationLottie?.setAnimation(lottieAnimation)
-    illustrationLottie?.playAnimation()
-    illustrationLottie?.setOnClickListener(lottieClickListener)
-    illustrationLottie?.visibility = View.VISIBLE
-  }
-
-  private fun setTexts(sensorType: FingerprintSensorType?, view: GlifLayout) {
-    when (sensorType) {
-      FingerprintSensorType.UDFPS_OPTICAL,
-      FingerprintSensorType.UDFPS_ULTRASONIC -> {
-        view.setHeaderText(R.string.security_settings_udfps_enroll_find_sensor_title)
-        view.setDescriptionText(R.string.security_settings_udfps_enroll_find_sensor_message)
-      }
-      FingerprintSensorType.POWER_BUTTON -> {
-        view.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
-        view.setDescriptionText(R.string.security_settings_sfps_enroll_find_sensor_message)
-      }
-      else -> {
-        view.setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title)
-        view.setDescriptionText(R.string.security_settings_fingerprint_enroll_find_sensor_message)
-      }
-    }
-  }
-
   private fun getSfpsIllustrationLottieAnimation(isFolded: Boolean, rotation: Int): Int {
     val animation: Int
     when (rotation) {
@@ -211,4 +144,20 @@
     }
     return animation
   }
+
+  private fun setupLottie(
+    view: View,
+    lottieAnimation: Int,
+    lottieClickListener: View.OnClickListener? = null,
+  ) {
+    val illustrationLottie: LottieAnimationView? = view.findViewById(R.id.illustration_lottie)
+    illustrationLottie?.setAnimation(lottieAnimation)
+    illustrationLottie?.playAnimation()
+    illustrationLottie?.setOnClickListener(lottieClickListener)
+    illustrationLottie?.visibility = View.VISIBLE
+  }
+
+  companion object {
+    private const val TAG = "UdfpsEnrollFindSensor"
+  }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/common/util/FingerprintOptionUtil.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/common/util/FingerprintOptionUtil.kt
new file mode 100644
index 0000000..7742fd9
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/common/util/FingerprintOptionUtil.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.util
+
+import android.content.Intent
+import android.hardware.fingerprint.FingerprintEnrollOptions
+import com.android.settings.biometrics.BiometricUtils
+
+fun Intent.toFingerprintEnrollOptions(): FingerprintEnrollOptions {
+  val reason: Int = this.getIntExtra(BiometricUtils.EXTRA_ENROLL_REASON, -1)
+  val builder: FingerprintEnrollOptions.Builder = FingerprintEnrollOptions.Builder()
+  builder.setEnrollReason(FingerprintEnrollOptions.ENROLL_REASON_UNKNOWN)
+  if (reason != -1) {
+    builder.setEnrollReason(reason)
+  }
+  return builder.build()
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/FingerprintErrorDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/common/widget/FingerprintErrorDialog.kt
similarity index 96%
rename from src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/FingerprintErrorDialog.kt
rename to src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/common/widget/FingerprintErrorDialog.kt
index 9c0040b..dc4842a 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/FingerprintErrorDialog.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/common/widget/FingerprintErrorDialog.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget
+package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.widget
 
 import android.app.AlertDialog
 import android.app.Dialog
@@ -29,8 +29,6 @@
 import kotlin.coroutines.resume
 import kotlinx.coroutines.suspendCancellableCoroutine
 
-private const val TAG = "FingerprintErrorDialog"
-
 /** A Dialog used for fingerprint enrollment when an error occurs. */
 class FingerprintErrorDialog : InstrumentedDialogFragment() {
   private lateinit var onContinue: DialogInterface.OnClickListener
@@ -82,6 +80,7 @@
   }
 
   companion object {
+    private const val TAG = "FingerprintErrorDialog"
     private const val KEY_MESSAGE = "fingerprint_message"
     private const val KEY_TITLE = "fingerprint_title"
     private const val KEY_SHOULD_TRY_AGAIN = "should_try_again"
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt
index f6917f3..0ec0bdd 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt
@@ -34,9 +34,9 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.settings.R
 import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.widget.FingerprintErrorDialog
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSIconTouchViewModel
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSViewModel
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.FingerprintErrorDialog
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.IconTouchDialog
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.RFPSProgressBar
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt
index 6ee5709..0645081 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt
@@ -43,7 +43,7 @@
   orientationInteractor: OrientationInteractor,
 ) : ViewModel() {
 
-  private val _textViewIsVisible = MutableStateFlow<Boolean>(false)
+  private val _textViewIsVisible = MutableStateFlow(false)
   /** Value to indicate if the text view is visible or not */
   val textViewIsVisible: Flow<Boolean> = _textViewIsVisible.asStateFlow()
 
@@ -52,7 +52,7 @@
   /** Indicates if the icon should be animating or not */
   val shouldAnimateIcon = _shouldAnimateIcon
 
-  private var enrollFlow: Flow<FingerEnrollState?> = fingerprintEnrollViewModel.enrollFLow
+  private var enrollFlow: Flow<FingerEnrollState?> = fingerprintEnrollViewModel.enrollFlow
 
   /**
    * Enroll progress message with a replay of size 1 allowing for new subscribers to get the most
@@ -142,7 +142,7 @@
     _textViewIsVisible.update { false }
     _shouldAnimateIcon = fingerprintEnrollViewModel.enrollFlowShouldBeRunning
     /** Indicates if the icon should be animating or not */
-    enrollFlow = fingerprintEnrollViewModel.enrollFLow
+    enrollFlow = fingerprintEnrollViewModel.enrollFlow
   }
 
   class RFPSViewModelFactory(
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt
index d1abcd0..c96a1b4 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt
@@ -18,6 +18,8 @@
 
 import android.os.Bundle
 import android.util.Log
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_HOVER_MOVE
 import android.view.View
 import android.view.WindowManager
 import android.widget.TextView
@@ -30,10 +32,12 @@
 import com.airbnb.lottie.LottieAnimationView
 import com.airbnb.lottie.LottieCompositionFactory
 import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
+import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.widget.FingerprintErrorDialog
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.DescriptionText
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.HeaderText
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.EducationAnimationModel
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.StageViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.HeaderText
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsViewModel
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget.UdfpsEnrollViewV2
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep
@@ -47,6 +51,7 @@
   private var factory: ViewModelProvider.Factory? = null
   private val viewModel: UdfpsViewModel by lazy { viewModelProvider[UdfpsViewModel::class.java] }
   private lateinit var udfpsEnrollView: UdfpsEnrollViewV2
+  private lateinit var lottie: LottieAnimationView
 
   private val viewModelProvider: ViewModelProvider by lazy {
     if (factory != null) {
@@ -63,7 +68,8 @@
 
   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
     super.onViewCreated(view, savedInstanceState)
-    val illustrationLottie: LottieAnimationView = view.findViewById(R.id.illustration_lottie)!!
+    val fragment = this
+    lottie = view.findViewById(R.id.illustration_lottie)!!
     udfpsEnrollView = view.findViewById(R.id.udfps_animation_view)!!
     val titleTextView = view.findViewById<TextView>(R.id.title)!!
     val descriptionTextView = view.findViewById<TextView>(R.id.description)!!
@@ -79,6 +85,11 @@
 
     viewLifecycleOwner.lifecycleScope.launch {
       repeatOnLifecycle(Lifecycle.State.RESUMED) {
+        launch {
+          viewModel.sensorLocation.collect { sensor ->
+            udfpsEnrollView.setSensorRect(sensor.sensorBounds, sensor.sensorType)
+          }
+        }
         viewLifecycleOwner.lifecycleScope.launch {
           viewModel.headerText.collect { titleTextView.setText(it.toResource()) }
         }
@@ -92,35 +103,59 @@
             }
           }
         }
-
         viewLifecycleOwner.lifecycleScope.launch {
-          viewModel.sensorLocation.collect { rect -> udfpsEnrollView.setSensorRect(rect) }
-        }
-
-        viewLifecycleOwner.lifecycleScope.launch {
-          viewModel.accessibilityEnabled.collect { isEnabled -> udfpsEnrollView.setAccessibilityEnabled(isEnabled) }
+          viewModel.shouldShowLottie.collect {
+            lottie.visibility = if (it) View.VISIBLE else View.GONE
+          }
         }
 
         viewLifecycleOwner.lifecycleScope.launch {
           viewModel.lottie.collect { lottieModel ->
+            if (lottie.visibility == View.GONE) {
+              return@collect
+            }
             val resource = lottieModel.toResource()
             if (resource != null) {
               LottieCompositionFactory.fromRawRes(requireContext(), resource).addListener { comp ->
                 comp?.let { composition ->
-                  illustrationLottie.setComposition(composition)
-                  illustrationLottie.visibility = View.VISIBLE
-                  illustrationLottie.playAnimation()
+                  lottie.setComposition(composition)
+                  lottie.visibility = View.VISIBLE
+                  lottie.playAnimation()
                 }
               }
             } else {
-              illustrationLottie.visibility = View.INVISIBLE
+              lottie.visibility = View.INVISIBLE
             }
           }
         }
+
         viewLifecycleOwner.lifecycleScope.launch {
-          viewModel.udfpsEvent.collect {
-           Log.d(TAG, "EnrollEvent $it")
-            udfpsEnrollView.onUdfpsEvent(it) }
+          repeatOnLifecycle(Lifecycle.State.DESTROYED) { viewModel.stopEnrollment() }
+        }
+
+        viewLifecycleOwner.lifecycleScope.launch {
+          viewModel.accessibilityEnabled.collect { enabled ->
+            udfpsEnrollView.setAccessibilityEnabled(enabled)
+          }
+        }
+
+        viewLifecycleOwner.lifecycleScope.launch {
+          viewModel.enrollState.collect {
+            Log.d(TAG, "EnrollEvent $it")
+            if (it is FingerEnrollState.EnrollError) {
+              try {
+                FingerprintErrorDialog.showInstance(it, fragment)
+              } catch (exception: Exception) {
+                Log.e(TAG, "Exception occurred $exception")
+              }
+            } else {
+              udfpsEnrollView.onUdfpsEvent(it)
+            }
+          }
+        }
+
+        viewLifecycleOwner.lifecycleScope.launch {
+          viewModel.progressSaved.collect { udfpsEnrollView.onEnrollProgressSaved(it) }
         }
 
         viewLifecycleOwner.lifecycleScope.launch {
@@ -128,6 +163,15 @@
         }
       }
     }
+
+    viewLifecycleOwner.lifecycleScope.launch {
+      viewModel.touchExplorationDebug.collect {
+        udfpsEnrollView.sendDebugTouchExplorationEvent(
+          MotionEvent.obtain(100, 100, ACTION_HOVER_MOVE, it.x.toFloat(), it.y.toFloat(), 0)
+        )
+      }
+    }
+    viewModel.readyForEnrollment()
   }
 
   private fun HeaderText.toResource(): Int {
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt
index 192a787..175fea0 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt
@@ -16,6 +16,8 @@
 
 package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
 
+import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
+
 /** Represents the description text for UDFPS enrollment */
 data class DescriptionText(
   val isSuw: Boolean,
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt
index cf125c3..a274179 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt
@@ -16,6 +16,8 @@
 
 package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
 
+import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
+
 /** Represents the lottie for UDFPS enrollment */
 data class EducationAnimationModel(
   val isSuw: Boolean,
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt
index 9cfcddc..c565f35 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt
@@ -16,6 +16,8 @@
 
 package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
 
+import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
+
 /** Represents the header text for UDFPS enrollment */
 data class HeaderText(
   val isSuw: Boolean,
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsEnrollEvent.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsEnrollEvent.kt
deleted file mode 100644
index e349ceb..0000000
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsEnrollEvent.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
-
-/** A class indicating a udfps enroll event occurred. */
-sealed class UdfpsEnrollEvent
-
-/** Describes how many [remainingSteps] and how many [totalSteps] are left in udfps enrollment. */
-data class UdfpsProgress(val remainingSteps: Int, val totalSteps: Int) : UdfpsEnrollEvent()
-
-/** Indicates a help event has been sent by enrollment */
-data class UdfpsHelp(val helpMsgId: Int, val helpString: String) : UdfpsEnrollEvent()
-
-/** Indicates a error event has been sent by enrollment */
-data class UdfpsError(val errMsgId: Int, val errString: String) : UdfpsEnrollEvent()
-
-/** Indicates an acquired event has occurred */
-data class Acquired(val acquiredGood: Boolean) : UdfpsEnrollEvent()
-
-/** Indicates a pointer down event has occurred */
-data object PointerDown : UdfpsEnrollEvent()
-
-/** Indicates a pointer up event has occurred */
-data object PointerUp : UdfpsEnrollEvent()
-
-/** Indicates the overlay has shown */
-data object OverlayShown : UdfpsEnrollEvent()
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt
index 7d43e1b..3782237 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt
@@ -16,167 +16,284 @@
 
 package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
 
-import android.graphics.Rect
+import android.graphics.Point
+import android.view.Surface
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository
+import com.android.settings.biometrics.fingerprint2.data.repository.SimulatedTouchEventsRepository
+import com.android.settings.biometrics.fingerprint2.domain.interactor.DebuggingInteractor
+import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractor
+import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractor
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintVibrationEffects
+import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor
+import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor
+import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
+import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintAction
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationViewModel
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.combineTransform
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterIsInstance
 import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
 
 /** ViewModel used to drive UDFPS Enrollment through [UdfpsEnrollFragment] */
-class UdfpsViewModel() : ViewModel() {
+class UdfpsViewModel(
+  val vibrationInteractor: VibrationInteractor,
+  displayDensityInteractor: DisplayDensityInteractor,
+  val navigationViewModel: FingerprintNavigationViewModel,
+  debuggingInteractor: DebuggingInteractor,
+  val fingerprintEnrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel,
+  simulatedTouchEventsDebugRepository: SimulatedTouchEventsRepository,
+  enrollStageInteractor: EnrollStageInteractor,
+  orientationInteractor: OrientationInteractor,
+  backgroundViewModel: BackgroundViewModel,
+  sensorRepository: FingerprintSensorRepository,
+) : ViewModel() {
 
   private val isSetupWizard = flowOf(false)
 
-  /** Indicates which Enrollment stage we are currently in. */
-  private val sensorLocationInternal = Pair(540, 1713)
-  private val sensorRadius = 100
-  private val sensorRect =
-    Rect(
-      this.sensorLocationInternal.first - sensorRadius,
-      this.sensorLocationInternal.second - sensorRadius,
-      this.sensorLocationInternal.first + sensorRadius,
-      this.sensorLocationInternal.second + sensorRadius,
-    )
-
-  private val stageThresholds = flowOf(listOf(.25, .5, .75, .875))
-
-  /** Indicates if accessibility is enabled */
-  val accessibilityEnabled = flowOf(false)
-
-  /** Indicates the locates of the fingerprint sensor. */
-  val sensorLocation: Flow<Rect> = flowOf(sensorRect)
-
-  /** This is currently not hooked up to fingerprint manager, and is being fed mock events. */
-  val udfpsEvent: Flow<UdfpsEnrollEvent> =
-    flow {
-        enrollEvents.forEach { events ->
-          events.forEach { event -> emit(event) }
-          delay(1000)
-        }
-      }
-      .flowOn(Dispatchers.IO)
-
-  /** Determines the current [StageViewModel] enrollment is in */
-  val enrollStage: Flow<StageViewModel> =
-    combine(stageThresholds, udfpsEvent) { thresholds, event ->
-        if (event is UdfpsProgress) {
-          thresholdToStageMap(thresholds, event.totalSteps - event.remainingSteps, event.totalSteps)
+  private var _enrollState: Flow<FingerEnrollState?> =
+    fingerprintEnrollEnrollingViewModel.enrollFlow
+  /** The current state of the enrollment. */
+  var enrollState: Flow<FingerEnrollState> =
+    combine(fingerprintEnrollEnrollingViewModel.enrollFlowShouldBeRunning, _enrollState) {
+        shouldBeRunning,
+        state ->
+        if (shouldBeRunning) {
+          state
         } else {
           null
         }
       }
       .filterNotNull()
 
+  /**
+   * Forwards the property sensor information. This is typically used to recreate views that must be
+   * aligned with the sensor.
+   */
+  val sensorLocation = sensorRepository.fingerprintSensor
+
+  /** Indicates if accessibility is enabled */
+  val accessibilityEnabled = flowOf(true).shareIn(viewModelScope, SharingStarted.Eagerly, 1)
+
+  init {
+    viewModelScope.launch {
+      enrollState
+        .combine(accessibilityEnabled) { event, isEnabled -> Pair(event, isEnabled) }
+        .collect {
+          if (
+            when (it.first) {
+              is FingerEnrollState.EnrollError -> true
+              is FingerEnrollState.EnrollHelp -> it.second
+              is FingerEnrollState.EnrollProgress -> true
+              else -> false
+            }
+          ) {
+            vibrate(it.first)
+          }
+        }
+    }
+
+    viewModelScope.launch {
+      backgroundViewModel.background.filter { it }.collect { didGoToBackground() }
+    }
+  }
+
+  /**
+   * This is the saved progress, this is for when views are recreated and need saved state for the
+   * first time.
+   */
+  var progressSaved: Flow<FingerEnrollState.EnrollProgress> =
+    enrollState
+      .filterIsInstance<FingerEnrollState.EnrollProgress>()
+      .filterNotNull()
+      .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
+
+  /** This sends touch exploration events only used for debugging purposes. */
+  val touchExplorationDebug: Flow<Point> =
+    debuggingInteractor.debuggingEnabled.combineTransform(
+      simulatedTouchEventsDebugRepository.touchExplorationDebug
+    ) { enabled, point ->
+      if (enabled) {
+        emit(point)
+      }
+    }
+
+  /** Determines the current [StageViewModel] enrollment is in */
+  val enrollStage: Flow<StageViewModel> =
+    combine(enrollStageInteractor.enrollStageThresholds, enrollState) { thresholds, event ->
+        if (event is FingerEnrollState.EnrollProgress) {
+          val progress =
+            (event.totalStepsRequired - event.remainingSteps).toFloat() / event.totalStepsRequired
+          var stageToReturn: StageViewModel = StageViewModel.Center
+          thresholds.forEach { (threshold, stage) ->
+            if (progress < threshold) {
+              return@forEach
+            }
+            stageToReturn = stage
+          }
+          stageToReturn
+        } else {
+          null
+        }
+      }
+      .filterNotNull()
+      .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
+
+  /** Indicates if we should show the lottie. */
+  val shouldShowLottie: Flow<Boolean> =
+    combine(
+        displayDensityInteractor.displayDensity,
+        displayDensityInteractor.defaultDisplayDensity,
+        displayDensityInteractor.fontScale,
+        orientationInteractor.rotation,
+      ) { currDisplayDensity, defaultDisplayDensity, fontScale, rotation ->
+        val canShowLottieForRotation =
+          when (rotation) {
+            Surface.ROTATION_0 -> true
+            else -> false
+          }
+
+        canShowLottieForRotation &&
+          if (fontScale > 1.0f) {
+            false
+          } else {
+            defaultDisplayDensity == currDisplayDensity
+          }
+      }
+      .shareIn(viewModelScope, SharingStarted.Eagerly, 1)
+
   /** The header text for UDFPS enrollment */
   val headerText: Flow<HeaderText> =
     combine(isSetupWizard, accessibilityEnabled, enrollStage) { isSuw, isAccessibility, stage ->
-      return@combine HeaderText(isSuw, isAccessibility, stage)
-    }
+        return@combine HeaderText(isSuw, isAccessibility, stage)
+      }
+      .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
 
   private val shouldClearDescriptionText = enrollStage.map { it is StageViewModel.Unknown }
 
   /** The description text for UDFPS enrollment */
   val descriptionText: Flow<DescriptionText?> =
     combine(isSetupWizard, accessibilityEnabled, enrollStage, shouldClearDescriptionText) {
-      isSuw,
-      isAccessibility,
-      stage,
-      shouldClearText ->
-      if (shouldClearText) {
-        return@combine null
-      } else {
-        return@combine DescriptionText(isSuw, isAccessibility, stage)
+        isSuw,
+        isAccessibility,
+        stage,
+        shouldClearText ->
+        if (shouldClearText) {
+          return@combine null
+        } else {
+          return@combine DescriptionText(isSuw, isAccessibility, stage)
+        }
       }
-    }
+      .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
+
+  /** Indicates if the consumer is ready for enrollment */
+  fun readyForEnrollment() {
+    fingerprintEnrollEnrollingViewModel.canEnroll()
+  }
+
+  /** Indicates if enrollment should stop */
+  fun stopEnrollment() {
+    fingerprintEnrollEnrollingViewModel.stopEnroll()
+  }
+
+  /** Indicates the negative button has been clicked */
+  fun negativeButtonClicked() {
+    doReset()
+    navigationViewModel.update(
+      FingerprintAction.NEGATIVE_BUTTON_PRESSED,
+      navStep,
+      "$TAG#negativeButtonClicked",
+    )
+  }
+
+  /** Indicates that an enrollment was completed */
+  fun finishedSuccessfully() {
+    doReset()
+    navigationViewModel.update(FingerprintAction.NEXT, navStep, "${TAG}#progressFinished")
+  }
+
+  /** Indicates that the application went to the background. */
+  private fun didGoToBackground() {
+    navigationViewModel.update(
+      FingerprintAction.DID_GO_TO_BACKGROUND,
+      navStep,
+      "$TAG#didGoToBackground",
+    )
+    stopEnrollment()
+  }
+
+  private fun doReset() {
+    /** Indicates if the icon should be animating or not */
+    _enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow
+  }
 
   /** The lottie that should be shown for UDFPS Enrollment */
   val lottie: Flow<EducationAnimationModel> =
     combine(isSetupWizard, accessibilityEnabled, enrollStage) { isSuw, isAccessibility, stage ->
-      return@combine EducationAnimationModel(isSuw, isAccessibility, stage)
-    }.distinctUntilChanged()
+        return@combine EducationAnimationModel(isSuw, isAccessibility, stage)
+      }
+      .distinctUntilChanged()
+      .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
 
-  class UdfpsEnrollmentFactory() : ViewModelProvider.Factory {
+  /** Indicates we should send a vibration event */
+  private fun vibrate(event: FingerEnrollState) {
+    val vibrationEvent =
+      when (event) {
+        is FingerEnrollState.EnrollError -> FingerprintVibrationEffects.UdfpsError
+        is FingerEnrollState.EnrollHelp -> FingerprintVibrationEffects.UdfpsHelp
+        is FingerEnrollState.EnrollProgress -> FingerprintVibrationEffects.UdfpsSuccess
+        else -> FingerprintVibrationEffects.UdfpsError
+      }
+    vibrationInteractor.vibrate(vibrationEvent, "UdfpsEnrollFragment")
+  }
+
+  class UdfpsEnrollmentFactory(
+    private val vibrationInteractor: VibrationInteractor,
+    private val displayDensityInteractor: DisplayDensityInteractor,
+    private val navigationViewModel: FingerprintNavigationViewModel,
+    private val debuggingInteractor: DebuggingInteractor,
+    private val fingerprintEnrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel,
+    private val simulatedTouchEventsRepository: SimulatedTouchEventsRepository,
+    private val enrollStageInteractor: EnrollStageInteractor,
+    private val orientationInteractor: OrientationInteractor,
+    private val backgroundViewModel: BackgroundViewModel,
+    private val sensorRepository: FingerprintSensorRepository,
+  ) : ViewModelProvider.Factory {
 
     @Suppress("UNCHECKED_CAST")
     override fun <T : ViewModel> create(modelClass: Class<T>): T {
-      return UdfpsViewModel() as T
+      return UdfpsViewModel(
+        vibrationInteractor,
+        displayDensityInteractor,
+        navigationViewModel,
+        debuggingInteractor,
+        fingerprintEnrollEnrollingViewModel,
+        simulatedTouchEventsRepository,
+        enrollStageInteractor,
+        orientationInteractor,
+        backgroundViewModel,
+        sensorRepository,
+      )
+        as T
     }
   }
 
   companion object {
     private val navStep = FingerprintNavigationStep.Enrollment::class
     private const val TAG = "UDFPSViewModel"
-    private val ENROLLMENT_STAGES_ORDERED =
-      listOf(
-        StageViewModel.Center,
-        StageViewModel.Guided,
-        StageViewModel.Fingertip,
-        StageViewModel.LeftEdge,
-        StageViewModel.RightEdge,
-      )
-
-    /**
-     * [thresholds] is a list of 4 numbers from [0,1] that separate enrollment into 5 stages. The
-     * stage is determined by mapping [thresholds] * [maxSteps] and finding where the [currentStep]
-     * is.
-     *
-     * Each number in the array should be strictly increasing such as [0.2, 0.5, 0.6, 0.8]
-     */
-    private fun thresholdToStageMap(
-      thresholds: List<Double>,
-      currentStep: Int,
-      maxSteps: Int,
-    ): StageViewModel {
-      val stageIterator = ENROLLMENT_STAGES_ORDERED.iterator()
-      thresholds.forEach {
-        val thresholdLimit = it * maxSteps
-        val curr = stageIterator.next()
-        if (currentStep < thresholdLimit) {
-          return curr
-        }
-      }
-      return stageIterator.next()
-    }
-
-    /** This will be removed */
-    private val enrollEvents: List<List<UdfpsEnrollEvent>> =
-      listOf(
-        listOf(OverlayShown),
-        listOf(UdfpsHelp(1,"hi")),
-        listOf(UdfpsHelp(1,"hi")),
-        CreateProgress(15, 16),
-        listOf(UdfpsHelp(1,"hi")),
-        CreateProgress(14, 16),
-        listOf(PointerDown, UdfpsHelp(1,"hi"), PointerUp),
-        listOf(PointerDown, UdfpsHelp(1,"hi"), PointerUp),
-        CreateProgress(13, 16),
-        CreateProgress(12, 16),
-        CreateProgress(11, 16),
-        CreateProgress(10, 16),
-        CreateProgress(9, 16),
-        CreateProgress(8, 16),
-        CreateProgress(7, 16),
-        CreateProgress(6, 16),
-        CreateProgress(5, 16),
-        CreateProgress(4, 16),
-        CreateProgress(3, 16),
-        CreateProgress(2, 16),
-        CreateProgress(1, 16),
-        CreateProgress(0, 16),
-      )
-
-    /** This will be removed */
-    private fun CreateProgress(remaining: Int, total: Int): List<UdfpsEnrollEvent> {
-      return listOf(PointerDown, Acquired(true), UdfpsProgress(remaining, total), PointerUp)
-    }
   }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt
index 5d4607c4..1419241 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt
@@ -20,7 +20,7 @@
 import android.graphics.PointF
 import android.util.TypedValue
 import android.view.accessibility.AccessibilityManager
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.StageViewModel
+import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
 
 /** Keeps track of which guided enrollment point we should be using */
 class UdfpsEnrollHelperV2(private val mContext: Context) {
@@ -28,6 +28,7 @@
   private var isGuidedEnrollment: Boolean = false
   private val accessibilityEnabled: Boolean
   private val guidedEnrollmentPoints: MutableList<PointF>
+  /** The current index of [guidedEnrollmentPoints] for the guided enrollment. */
   private var index = 0
 
   init {
@@ -76,7 +77,7 @@
       if (accessibilityEnabled || !isGuidedEnrollment) {
         return null
       }
-      var scale = SCALE
+      val scale = SCALE
       val originalPoint = guidedEnrollmentPoints[index % guidedEnrollmentPoints.size]
       return PointF(originalPoint.x * scale, originalPoint.y * scale)
     }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt
index 210cb2b..0d48995 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt
@@ -37,7 +37,7 @@
 import androidx.core.graphics.toRect
 import androidx.core.graphics.toRectF
 import com.android.settings.R
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.StageViewModel
+import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
 import kotlin.math.sin
 
 /**
@@ -45,6 +45,7 @@
  * various stages of enrollment
  */
 class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeSet?) : Drawable() {
+  private var targetAnimationDuration: Long = TARGET_ANIM_DURATION_LONG
   private var targetAnimatorSet: AnimatorSet? = null
   private val movingTargetFpIcon: Drawable
   private val fingerprintDrawable: ShapeDrawable
@@ -88,22 +89,25 @@
         it.recycle()
       }
 
-    sensorOutlinePaint = Paint(0 /* flags */).apply {
-      isAntiAlias = true
-      setColor(movingTargetFill)
-      style = Paint.Style.FILL
-    }
+    sensorOutlinePaint =
+      Paint(0 /* flags */).apply {
+        isAntiAlias = true
+        setColor(movingTargetFill)
+        style = Paint.Style.FILL
+      }
 
-    blueFill = Paint(0 /* flags */).apply {
-      isAntiAlias = true
-      setColor(movingTargetFill)
-      style = Paint.Style.FILL
-    }
+    blueFill =
+      Paint(0 /* flags */).apply {
+        isAntiAlias = true
+        setColor(movingTargetFill)
+        style = Paint.Style.FILL
+      }
 
-    movingTargetFpIcon = context.resources.getDrawable(R.drawable.ic_enrollment_fingerprint, null).apply {
-      setTint(enrollIconColor)
-      mutate()
-    }
+    movingTargetFpIcon =
+      context.resources.getDrawable(R.drawable.ic_enrollment_fingerprint, null).apply {
+        setTint(enrollIconColor)
+        mutate()
+      }
 
     fingerprintDrawable.setTint(enrollIconColor)
     setAlpha(255)
@@ -140,7 +144,16 @@
   }
 
   /** Update the progress of the icon */
-  fun onEnrollmentProgress(remaining: Int, totalSteps: Int) {
+  fun onEnrollmentProgress(remaining: Int, totalSteps: Int, isRecreating: Boolean = false) {
+    restoreAnimationTime()
+    // If we are restoring this view from a saved state, set animation duration to 0 to avoid
+    // animating progress that has already occurred.
+    if (isRecreating) {
+      setAnimationTimeToZero()
+    } else {
+      restoreAnimationTime()
+    }
+
     helper.onEnrollmentProgress(remaining, totalSteps)
     val offset = helper.guidedEnrollmentLocation
     val currentBounds = getCurrLocation().toRect()
@@ -149,10 +162,10 @@
       // offsets the initial sensor rect by a bit to get the user to move their finger a bit more.
       val targetRect = Rect(sensorRectBounds).toRectF()
       targetRect.offset(offset.x, offset.y)
-      var shouldAnimateMovement =
+      val shouldAnimateMovement =
         !currentBounds.equals(targetRect) && offset.x != 0f && offset.y != 0f
       if (shouldAnimateMovement) {
-        targetAnimatorSet?.let { it.cancel() }
+        targetAnimatorSet?.cancel()
         animateMovement(currentBounds, targetRect, true)
       }
     } else {
@@ -186,7 +199,7 @@
     val currLocation = getCurrLocation()
     canvas.scale(currentScale, currentScale, currLocation.centerX(), currLocation.centerY())
 
-    sensorRectBounds?.let { canvas.drawOval(currLocation, sensorOutlinePaint) }
+    canvas.drawOval(currLocation, sensorOutlinePaint)
     fingerprintDrawable.bounds = currLocation.toRect()
     fingerprintDrawable.draw(canvas)
   }
@@ -234,6 +247,19 @@
     }
   }
 
+  /**
+   * This sets animation time to 0. This typically happens after an activity recreation, we don't
+   * want to re-animate the progress/success animation with the default timer
+   */
+  private fun setAnimationTimeToZero() {
+    targetAnimationDuration = 0
+  }
+
+  /** This sets animation timers back to normal, this happens after we have */
+  private fun restoreAnimationTime() {
+    targetAnimationDuration = TARGET_ANIM_DURATION_LONG
+  }
+
   companion object {
     private const val TAG = "UdfpsEnrollDrawableV2"
     private const val DEFAULT_STROKE_WIDTH = 3f
@@ -242,12 +268,13 @@
 
     private fun createUdfpsIcon(context: Context): ShapeDrawable {
       val fpPath = context.resources.getString(R.string.config_udfpsIcon)
-      val drawable = ShapeDrawable(PathShape(PathParser.createPathFromPathData(fpPath), 72f, 72f)).apply {
-        mutate()
-        paint.style = Paint.Style.STROKE
-        paint.strokeCap = Paint.Cap.ROUND
-        paint.strokeWidth = DEFAULT_STROKE_WIDTH
-      }
+      val drawable =
+        ShapeDrawable(PathShape(PathParser.createPathFromPathData(fpPath), 72f, 72f)).apply {
+          mutate()
+          paint.style = Paint.Style.STROKE
+          paint.strokeCap = Paint.Cap.ROUND
+          paint.strokeWidth = DEFAULT_STROKE_WIDTH
+        }
       return drawable
     }
   }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt
index 5f7d2f9..8f0e845 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt
@@ -25,28 +25,28 @@
 import android.graphics.PixelFormat
 import android.graphics.Rect
 import android.graphics.drawable.Drawable
-import android.os.Process
-import android.os.VibrationAttributes
-import android.os.VibrationEffect
-import android.os.Vibrator
 import android.util.AttributeSet
 import android.util.DisplayMetrics
 import android.view.animation.DecelerateInterpolator
 import android.view.animation.Interpolator
+import android.view.animation.OvershootInterpolator
 import androidx.annotation.ColorInt
 import androidx.core.animation.doOnEnd
 import androidx.core.graphics.toRectF
 import com.android.internal.annotations.VisibleForTesting
 import com.android.settings.R
+import kotlin.math.cos
 import kotlin.math.max
+import kotlin.math.sin
 
 /**
  * UDFPS enrollment progress bar. This view is responsible for drawing the progress ring and its
  * fill around the center of the UDFPS sensor.
  */
-class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: AttributeSet?) :
+class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: AttributeSet?) :
   Drawable() {
   private val sensorRect: Rect = Rect()
+  private var rotation: Int = 0
   private val strokeWidthPx: Float
 
   @ColorInt private val progressColor: Int
@@ -56,7 +56,6 @@
   private val backgroundPaint: Paint
 
   @VisibleForTesting val fillPaint: Paint
-  private val vibrator: Vibrator
   private var isAccessibilityEnabled: Boolean = false
   private var afterFirstTouch = false
   private var remainingSteps = 0
@@ -64,22 +63,27 @@
   private var progress = 0f
   private var progressAnimator: ValueAnimator? = null
   private val progressUpdateListener: AnimatorUpdateListener
-  private var showingHelp = false
   private var fillColorAnimator: ValueAnimator? = null
   private val fillColorUpdateListener: AnimatorUpdateListener
   private var backgroundColorAnimator: ValueAnimator? = null
   private val backgroundColorUpdateListener: AnimatorUpdateListener
-  private var complete = false
   private var movingTargetFill = 0
   private var movingTargetFillError = 0
   private var enrollProgressColor = 0
   private var enrollProgressHelp = 0
   private var enrollProgressHelpWithTalkback = 0
   private val progressBarRadius: Int
+  private var checkMarkDrawable: Drawable
+  private var checkMarkAnimator: ValueAnimator? = null
+
+  private var fillColorAnimationDuration = FILL_COLOR_ANIMATION_DURATION_MS
+  private var animateArcDuration = PROGRESS_ANIMATION_DURATION_MS
+  private var checkmarkAnimationDelayDuration = CHECKMARK_ANIMATION_DELAY_MS
+  private var checkmarkAnimationDuration = CHECKMARK_ANIMATION_DURATION_MS
 
   init {
     val ta =
-      mContext.obtainStyledAttributes(
+      context.obtainStyledAttributes(
         attrs,
         R.styleable.BiometricsEnrollView,
         R.attr.biometricsEnrollStyle,
@@ -94,30 +98,33 @@
     enrollProgressHelpWithTalkback =
       ta.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollProgressHelpWithTalkback, 0)
     ta.recycle()
-    val density = mContext.resources.displayMetrics.densityDpi.toFloat()
+    val density = context.resources.displayMetrics.densityDpi.toFloat()
     strokeWidthPx = STROKE_WIDTH_DP * (density / DisplayMetrics.DENSITY_DEFAULT)
     progressColor = enrollProgressColor
     onFirstBucketFailedColor = movingTargetFillError
     updateHelpColor()
-    backgroundPaint = Paint().apply {
-      strokeWidth = strokeWidthPx
-      setColor(movingTargetFill)
-      isAntiAlias = true
-      style = Paint.Style.STROKE
-      strokeCap = Paint.Cap.ROUND
-    }
+    backgroundPaint =
+      Paint().apply {
+        strokeWidth = strokeWidthPx
+        setColor(movingTargetFill)
+        isAntiAlias = true
+        style = Paint.Style.STROKE
+        strokeCap = Paint.Cap.ROUND
+      }
+
+    checkMarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark)!!
 
     // Progress fill should *not* use the extracted system color.
-    fillPaint = Paint().apply {
-      strokeWidth = strokeWidthPx
-      setColor(progressColor)
-      isAntiAlias = true
-      style = Paint.Style.STROKE
-      strokeCap = Paint.Cap.ROUND
-    }
-    vibrator = mContext.getSystemService(Vibrator::class.java)!!
+    fillPaint =
+      Paint().apply {
+        strokeWidth = strokeWidthPx
+        setColor(progressColor)
+        isAntiAlias = true
+        style = Paint.Style.STROKE
+        strokeCap = Paint.Cap.ROUND
+      }
 
-    progressBarRadius = mContext.resources.getInteger(R.integer.config_udfpsEnrollProgressBar)
+    progressBarRadius = context.resources.getInteger(R.integer.config_udfpsEnrollProgressBar)
 
     progressUpdateListener = AnimatorUpdateListener { animation: ValueAnimator ->
       progress = animation.getAnimatedValue() as Float
@@ -134,9 +141,10 @@
   }
 
   /** Indicates enrollment progress has occurred. */
-  fun onEnrollmentProgress(remaining: Int, totalSteps: Int) {
+  fun onEnrollmentProgress(remaining: Int, totalSteps: Int, isRecreating: Boolean = false) {
+
     afterFirstTouch = true
-    updateProgress(remaining, totalSteps)
+    updateProgress(remaining, totalSteps, isRecreating)
   }
 
   /** Indicates enrollment help has occurred. */
@@ -157,18 +165,12 @@
 
     canvas.save()
     // This takes the sensors bounding box and expands it by [progressBarRadius] in all directions
-    val sensorProgressRect = Rect(sensorRect)
-    sensorProgressRect.inset(
-      -progressBarRadius,
-      -progressBarRadius,
-      -progressBarRadius,
-      -progressBarRadius,
-    )
+    val sensorProgressRect = getSensorProgressRect()
 
     // Rotate -90 degrees to make the progress start from the top right and not the bottom
     // right
     canvas.rotate(
-      -90f,
+      rotation - 90f,
       sensorProgressRect.centerX().toFloat(),
       sensorProgressRect.centerY().toFloat(),
     )
@@ -176,9 +178,9 @@
       // Draw the background color of the progress circle.
       canvas.drawArc(
         sensorProgressRect.toRectF(),
-        0f /* startAngle */,
-        360f /* sweepAngle */,
-        false /* useCenter */,
+        0f, /* startAngle */
+        360f, /* sweepAngle */
+        false, /* useCenter */
         backgroundPaint,
       )
     }
@@ -186,13 +188,15 @@
       // Draw the filled portion of the progress circle.
       canvas.drawArc(
         sensorProgressRect.toRectF(),
-        0f /* startAngle */,
-        360f * progress /* sweepAngle */,
-        false /* useCenter */,
+        0f, /* startAngle */
+        360f * progress, /* sweepAngle */
+        false, /* useCenter */
         fillPaint,
       )
     }
+
     canvas.restore()
+    checkMarkDrawable.draw(canvas)
   }
 
   /** Do nothing here, we will control the alpha internally. */
@@ -211,6 +215,7 @@
    */
   fun drawProgressAt(sensorRect: Rect) {
     this.sensorRect.set(sensorRect)
+    invalidateSelf()
   }
 
   /** Indicates if accessibility is enabled or not. */
@@ -228,47 +233,21 @@
       }
   }
 
-  private fun updateProgress(remainingSteps: Int, totalSteps: Int) {
+  private fun updateProgress(remainingSteps: Int, totalSteps: Int, isRecreating: Boolean) {
     if (this.remainingSteps == remainingSteps && this.totalSteps == totalSteps) {
       return
     }
+
+    // If we are restoring this view from a saved state, set animation duration to 0 to avoid
+    // animating progress that has already occurred.
+    if (isRecreating) {
+      setAnimationTimeToZero()
+    } else {
+      restoreAnimationTime()
+    }
+
     this.remainingSteps = remainingSteps
     this.totalSteps = totalSteps
-    if (this.showingHelp) {
-      if (vibrator != null && isAccessibilityEnabled) {
-        vibrator.vibrate(
-          Process.myUid(),
-          mContext.opPackageName,
-          VIBRATE_EFFECT_ERROR,
-          javaClass.getSimpleName() + "::onEnrollmentHelp",
-          FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES,
-        )
-      }
-    } else {
-      // If the first touch is an error, remainingSteps will be -1 and the callback
-      // doesn't come from onEnrollmentHelp. If we are in the accessibility flow,
-      // we still would like to vibrate.
-      if (vibrator != null) {
-        if (remainingSteps == -1 && isAccessibilityEnabled) {
-          vibrator.vibrate(
-            Process.myUid(),
-            mContext.opPackageName,
-            VIBRATE_EFFECT_ERROR,
-            javaClass.getSimpleName() + "::onFirstTouchError",
-            FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES,
-          )
-        } else if (remainingSteps != -1 && !isAccessibilityEnabled) {
-          vibrator.vibrate(
-            Process.myUid(),
-            mContext.opPackageName,
-            SUCCESS_VIBRATION_EFFECT,
-            javaClass.getSimpleName() + "::OnEnrollmentProgress",
-            HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES,
-          )
-        }
-      }
-    }
-    this.showingHelp = showingHelp
     this.remainingSteps = remainingSteps
     this.totalSteps = totalSteps
     val targetProgress = (totalSteps - remainingSteps).toFloat().div(max(1, totalSteps))
@@ -276,12 +255,69 @@
     if (progressAnimator != null && progressAnimator!!.isRunning) {
       progressAnimator!!.cancel()
     }
+    /** The [progressUpdateListener] will force re-[draw]s to occur depending on the progress. */
     progressAnimator =
       ValueAnimator.ofFloat(progress, targetProgress).also {
-        it.setDuration(PROGRESS_ANIMATION_DURATION_MS)
+        it.setDuration(animateArcDuration)
         it.addUpdateListener(progressUpdateListener)
         it.start()
       }
+    if (remainingSteps == 0) {
+      runCompletionAnimation()
+    }
+  }
+
+  private fun runCompletionAnimation() {
+    checkMarkAnimator?.cancel()
+
+    checkMarkAnimator = ValueAnimator.ofFloat(0f, 1f)
+    checkMarkAnimator?.apply {
+      startDelay = checkmarkAnimationDelayDuration
+      setDuration(checkmarkAnimationDuration)
+      interpolator = OvershootInterpolator()
+      addUpdateListener {
+        val newBounds = getCheckMarkStartBounds()
+        val scale = it.animatedFraction
+        newBounds.set(
+          newBounds.left,
+          newBounds.top,
+          (newBounds.left + (newBounds.width() * scale)).toInt(),
+          (newBounds.top + (newBounds.height() * scale)).toInt(),
+        )
+        checkMarkDrawable.bounds = newBounds
+        checkMarkDrawable.setVisible(true, false)
+      }
+      start()
+    }
+  }
+
+  /**
+   * This returns the bounds for which the checkmark drawable should be drawn at. It should be drawn
+   * on the arc of the progress bar at the 315 degree mark.
+   */
+  private fun getCheckMarkStartBounds(): Rect {
+    val progressBounds = getSensorProgressRect()
+    val radius = progressBounds.width() / 2.0
+
+    var x = (cos(Math.toRadians(315.0)) * radius).toInt() + progressBounds.centerX()
+    // Remember to negate this value as sin(>180) will return negative value
+    var y = (-sin(Math.toRadians(315.0)) * radius).toInt() + progressBounds.centerY()
+    // Subtract height|width /2 to make sure we draw in the middle of the arc.
+    x -= (checkMarkDrawable.intrinsicWidth / 2.0).toInt()
+    y -= (checkMarkDrawable.intrinsicHeight / 2.0).toInt()
+
+    return Rect(x, y, x + checkMarkDrawable.intrinsicWidth, y + checkMarkDrawable.intrinsicHeight)
+  }
+
+  private fun getSensorProgressRect(): Rect {
+    val sensorProgressRect = Rect(sensorRect)
+    sensorProgressRect.inset(
+      -progressBarRadius,
+      -progressBarRadius,
+      -progressBarRadius,
+      -progressBarRadius,
+    )
+    return sensorProgressRect
   }
 
   /**
@@ -294,7 +330,7 @@
     }
     backgroundColorAnimator =
       ValueAnimator.ofArgb(backgroundPaint.color, onFirstBucketFailedColor).also {
-        it.setDuration(FILL_COLOR_ANIMATION_DURATION_MS)
+        it.setDuration(fillColorAnimationDuration)
         it.repeatCount = 1
         it.repeatMode = ValueAnimator.REVERSE
         it.interpolator = DEACCEL
@@ -315,7 +351,7 @@
     @ColorInt val targetColor = helpColor
     fillColorAnimator =
       ValueAnimator.ofArgb(fillPaint.color, targetColor).also {
-        it.setDuration(FILL_COLOR_ANIMATION_DURATION_MS)
+        it.setDuration(fillColorAnimationDuration)
         it.repeatCount = 1
         it.repeatMode = ValueAnimator.REVERSE
         it.interpolator = DEACCEL
@@ -325,33 +361,32 @@
       }
   }
 
-  private fun startCompletionAnimation() {
-    if (complete) {
-      return
-    }
-    complete = true
+  /**
+   * This sets animation time to 0. This typically happens after an activity recreation, we don't
+   * want to re-animate the progress/success animation with the default timer
+   */
+  private fun setAnimationTimeToZero() {
+    fillColorAnimationDuration = 0
+    animateArcDuration = 0
+    checkmarkAnimationDelayDuration = 0
+    checkmarkAnimationDuration = 0
   }
 
-  private fun rollBackCompletionAnimation() {
-    if (!complete) {
-      return
-    }
-    complete = false
+  /** This sets animation timers back to normal, this happens after we have */
+  private fun restoreAnimationTime() {
+    fillColorAnimationDuration = FILL_COLOR_ANIMATION_DURATION_MS
+    animateArcDuration = PROGRESS_ANIMATION_DURATION_MS
+    checkmarkAnimationDelayDuration = CHECKMARK_ANIMATION_DELAY_MS
+    checkmarkAnimationDuration = CHECKMARK_ANIMATION_DURATION_MS
   }
 
-  private fun loadResources(context: Context, attrs: AttributeSet?) {}
-
   companion object {
     private const val TAG = "UdfpsProgressBar"
     private const val FILL_COLOR_ANIMATION_DURATION_MS = 350L
     private const val PROGRESS_ANIMATION_DURATION_MS = 400L
+    private const val CHECKMARK_ANIMATION_DELAY_MS = 200L
+    private const val CHECKMARK_ANIMATION_DURATION_MS = 300L
     private const val STROKE_WIDTH_DP = 12f
     private val DEACCEL: Interpolator = DecelerateInterpolator()
-    private val VIBRATE_EFFECT_ERROR = VibrationEffect.createWaveform(longArrayOf(0, 5, 55, 60), -1)
-    private val FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES =
-      VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY)
-    private val HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
-      VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK)
-    private val SUCCESS_VIBRATION_EFFECT = VibrationEffect.get(VibrationEffect.EFFECT_CLICK)
   }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt
index 5af3f4b..b355f77 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt
@@ -17,59 +17,99 @@
 package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget
 
 import android.content.Context
+import android.graphics.Point
 import android.graphics.Rect
 import android.util.AttributeSet
 import android.util.Log
+import android.view.DisplayInfo
+import android.view.MotionEvent
+import android.view.Surface
+import android.view.View
+import android.view.View.OnHoverListener
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import android.widget.ImageView
 import com.android.settings.R
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.Acquired
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.OverlayShown
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.PointerDown
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.PointerUp
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.StageViewModel
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsEnrollEvent
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsError
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsHelp
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsProgress
+import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
+import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
+import com.android.systemui.biometrics.UdfpsUtils
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
+import com.android.systemui.biometrics.shared.model.toInt
 
 /**
  * View corresponding with fingerprint_v2_udfps_enroll_view.xml. This view is responsible for
  * drawing the [UdfpsEnrollIconV2] and the [UdfpsEnrollProgressBarDrawableV2].
  */
 class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
+  private lateinit var fingerprintSensorType: FingerprintSensorType
+  private var onHoverListener: OnHoverListener = OnHoverListener { _, _ -> false }
   private var isAccessibilityEnabled: Boolean = false
   private lateinit var sensorRect: Rect
   private val fingerprintIcon: UdfpsEnrollIconV2 = UdfpsEnrollIconV2(mContext, attrs)
   private val fingerprintProgressDrawable: UdfpsEnrollProgressBarDrawableV2 =
     UdfpsEnrollProgressBarDrawableV2(mContext, attrs)
-  private var mTotalSteps = -1
-  private var mRemainingSteps = -1
+  private var remainingSteps = -1
+  private val udfpsUtils: UdfpsUtils = UdfpsUtils()
+  private lateinit var touchExplorationAnnouncer: TouchExplorationAnnouncer
+  private var isRecreating = false
 
   /**
    * This function computes the center (x,y) location with respect to the parent [FrameLayout] for
    * the [UdfpsEnrollProgressBarDrawableV2]. It also computes the [Rect] with respect to the parent
-   * [FrameLayout] for the [UdfpsEnrollIconV2].
+   * [FrameLayout] for the [UdfpsEnrollIconV2]. This function will also setup the
+   * [touchExplorationAnnouncer]
    */
-  fun setSensorRect(rect: Rect) {
+  fun setSensorRect(rect: Rect, sensorType: FingerprintSensorType) {
     this.sensorRect = rect
-
+    this.fingerprintSensorType = sensorType
     findViewById<ImageView?>(R.id.udfps_enroll_animation_fp_progress_view)?.also {
       it.setImageDrawable(fingerprintProgressDrawable)
     }
     findViewById<ImageView>(R.id.udfps_enroll_animation_fp_view)?.also {
       it.setImageDrawable(fingerprintIcon)
     }
+
+    val rotation = display.rotation
+    var displayInfo = DisplayInfo()
+    context.display.getDisplayInfo(displayInfo)
+    val scaleFactor = udfpsUtils.getScaleFactor(displayInfo)
+    val overlayParams =
+      UdfpsOverlayParams(
+        sensorRect,
+        fingerprintProgressDrawable.bounds,
+        displayInfo.naturalWidth,
+        displayInfo.naturalHeight,
+        scaleFactor,
+        rotation,
+        sensorType.toInt(),
+      )
     val parentView = parent as ViewGroup
     val coords = parentView.getLocationOnScreen()
     val parentLeft = coords[0]
     val parentTop = coords[1]
     val sensorRectOffset = Rect(sensorRect)
+    // If the view has been rotated, we need to translate the sensor coordinates
+    // to the new rotated view.
+    when (rotation) {
+      Surface.ROTATION_90,
+      Surface.ROTATION_270 -> {
+        sensorRectOffset.set(
+          sensorRectOffset.top,
+          sensorRectOffset.left,
+          sensorRectOffset.bottom,
+          sensorRectOffset.right,
+        )
+      }
+      else -> {}
+    }
+    // Translate the sensor position into UdfpsEnrollView's view space.
     sensorRectOffset.offset(-parentLeft, -parentTop)
 
     fingerprintIcon.drawSensorRectAt(sensorRectOffset)
     fingerprintProgressDrawable.drawProgressAt(sensorRectOffset)
+
+    touchExplorationAnnouncer = TouchExplorationAnnouncer(context, this, overlayParams, udfpsUtils)
   }
 
   /** Updates the current enrollment stage. */
@@ -78,15 +118,17 @@
   }
 
   /** Receive enroll progress event */
-  fun onUdfpsEvent(event: UdfpsEnrollEvent) {
+  fun onUdfpsEvent(event: FingerEnrollState) {
     when (event) {
-      is UdfpsProgress -> onEnrollmentProgress(event.remainingSteps, event.totalSteps)
-      is Acquired -> onAcquired(event.acquiredGood)
-      is UdfpsHelp -> onEnrollmentHelp()
-      is PointerDown -> onPointerDown()
-      is PointerUp -> onPointerUp()
-      OverlayShown -> overlayShown()
-      is UdfpsError -> udfpsError(event.errMsgId, event.errString)
+      is FingerEnrollState.EnrollProgress ->
+        onEnrollmentProgress(event.remainingSteps, event.totalStepsRequired)
+      is FingerEnrollState.Acquired -> onAcquired(event.acquiredGood)
+      is FingerEnrollState.EnrollHelp -> onEnrollmentHelp()
+      is FingerEnrollState.PointerDown -> onPointerDown()
+      is FingerEnrollState.PointerUp -> onPointerUp()
+      is FingerEnrollState.OverlayShown -> overlayShown()
+      is FingerEnrollState.EnrollError ->
+        throw IllegalArgumentException("$TAG should not handle udfps error")
     }
   }
 
@@ -94,9 +136,37 @@
   fun setAccessibilityEnabled(enabled: Boolean) {
     this.isAccessibilityEnabled = enabled
     fingerprintProgressDrawable.setAccessibilityEnabled(enabled)
+    if (enabled) {
+      addHoverListener()
+    } else {
+      clearHoverListener()
+    }
   }
 
   private fun udfpsError(errMsgId: Int, errString: String) {}
+  /**
+   * Sends a touch exploration event to the [onHoverListener] this should only be used for
+   * debugging.
+   */
+  fun sendDebugTouchExplorationEvent(motionEvent: MotionEvent) {
+    touchExplorationAnnouncer.onTouch(motionEvent)
+  }
+
+  /** Sets the addHoverListener, this should happen when talkback is enabled. */
+  private fun addHoverListener() {
+    onHoverListener = OnHoverListener { _: View, event: MotionEvent ->
+      sendDebugTouchExplorationEvent(event)
+      false
+    }
+    this.setOnHoverListener(onHoverListener)
+  }
+
+  /** Clears the hover listener if one was set. */
+  private fun clearHoverListener() {
+    val listener = OnHoverListener { _, _ -> false }
+    this.setOnHoverListener(listener)
+    onHoverListener = listener
+  }
 
   private fun overlayShown() {
     Log.e(TAG, "Implement overlayShown")
@@ -115,7 +185,7 @@
 
   /** Receive onAcquired event */
   private fun onAcquired(isAcquiredGood: Boolean) {
-    val animateIfLastStepGood = isAcquiredGood && mRemainingSteps <= 2 && mRemainingSteps >= 0
+    val animateIfLastStepGood = isAcquiredGood && remainingSteps <= 2 && remainingSteps >= 0
     if (animateIfLastStepGood) fingerprintProgressDrawable.onLastStepAcquired()
   }
 
@@ -129,6 +199,52 @@
     fingerprintIcon.startDrawing()
   }
 
+  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+    super.onLayout(changed, left, top, right, bottom)
+    // Because the layout has changed, we need to recompute all locations.
+    if (this::sensorRect.isInitialized && this::fingerprintSensorType.isInitialized) {
+      setSensorRect(sensorRect, fingerprintSensorType)
+    }
+  }
+
+  /**
+   * This class is responsible for announcing touch events that are outside of the sensort rect
+   * area. Generally, if a touch is to the left of the sensor, the accessibility announcement will
+   * be something like "move right"
+   */
+  private class TouchExplorationAnnouncer(
+    val context: Context,
+    val view: View,
+    val overlayParams: UdfpsOverlayParams,
+    val udfpsUtils: UdfpsUtils,
+  ) {
+    /** Will announce accessibility event for touches outside of the sensor rect. */
+    fun onTouch(event: MotionEvent) {
+      val scaledTouch: Point =
+        udfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0), event, overlayParams)
+      if (udfpsUtils.isWithinSensorArea(event.getPointerId(0), event, overlayParams)) {
+        return
+      }
+      val theStr: String =
+        udfpsUtils.onTouchOutsideOfSensorArea(
+          true /*touchExplorationEnabled*/,
+          context,
+          scaledTouch.x,
+          scaledTouch.y,
+          overlayParams,
+        )
+      if (theStr != null) {
+        view.announceForAccessibility(theStr)
+      }
+    }
+  }
+
+  /** Indicates we should should restore the views saved state. */
+  fun onEnrollProgressSaved(it: FingerEnrollState.EnrollProgress) {
+    fingerprintIcon.onEnrollmentProgress(it.remainingSteps, it.totalStepsRequired, true)
+    fingerprintProgressDrawable.onEnrollmentProgress(it.remainingSteps, it.totalStepsRequired, true)
+  }
+
   companion object {
     private const val TAG = "UdfpsEnrollView"
   }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt
index 63182bb..7ddb142 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt
@@ -18,9 +18,11 @@
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
+import com.android.systemui.biometrics.shared.model.FingerprintSensor
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.transformLatest
 import kotlinx.coroutines.flow.update
 
@@ -63,7 +65,7 @@
   }
 
   /** Collects the enrollment flow based on [enrollFlowShouldBeRunning] */
-  val enrollFLow =
+  val enrollFlow =
     enrollFlowShouldBeRunning.transformLatest {
       if (it) {
         fingerprintEnrollViewModel.enrollFlow.collect { event -> emit(event) }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt
index 3bf806b..ddbf1cb 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt
@@ -25,7 +25,6 @@
 import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor
 import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
 import com.android.settings.biometrics.fingerprint2.lib.model.SetupWizard
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollFindSensorV2Fragment
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep.Education
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import kotlinx.coroutines.flow.Flow
@@ -38,7 +37,7 @@
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 
-/** Models the UI state for [FingerprintEnrollFindSensorV2Fragment]. */
+/** Models the UI state for fingerprint enroll education */
 class FingerprintEnrollFindSensorViewModel(
   private val navigationViewModel: FingerprintNavigationViewModel,
   private val fingerprintEnrollViewModel: FingerprintEnrollViewModel,
@@ -70,7 +69,7 @@
     combineTransform(
       _showSfpsLottie,
       foldStateInteractor.isFolded,
-      orientationInteractor.rotation,
+      orientationInteractor.rotationFromDefault,
     ) { _, isFolded, rotation ->
       emit(Pair(isFolded, rotation))
     }
@@ -147,6 +146,7 @@
                   }
                 }
                 is FingerEnrollState.EnrollHelp -> {}
+                else -> {}
               }
             }
         }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt
index bd90524..7900ed7 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt
@@ -45,12 +45,14 @@
 import com.android.settings.biometrics.GatekeeperPasswordProvider
 import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling
 import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintEnrollInteractorImpl
 import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepositoryImpl
-import com.android.settings.biometrics.fingerprint2.domain.interactor.PressToAuthInteractorImpl
 import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
+import com.android.settings.biometrics.fingerprint2.domain.interactor.PressToAuthInteractorImpl
 import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel
 import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData
 import com.android.settings.biometrics.fingerprint2.lib.model.Settings
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.util.toFingerprintEnrollOptions
 import com.android.settings.biometrics.fingerprint2.ui.settings.binder.FingerprintSettingsViewBinder
 import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsNavigationViewModel
 import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsViewModel
@@ -222,6 +224,13 @@
     val fingerprintSensorProvider =
       FingerprintSensorRepositoryImpl(fingerprintManager, backgroundDispatcher, lifecycleScope)
     val pressToAuthInteractor = PressToAuthInteractorImpl(context, backgroundDispatcher)
+    val fingerprintEnrollStateRepository =
+      FingerprintEnrollInteractorImpl(
+        requireContext().applicationContext,
+        intent.toFingerprintEnrollOptions(),
+        fingerprintManager,
+        Settings,
+      )
 
     val interactor =
       FingerprintManagerInteractorImpl(
@@ -230,9 +239,7 @@
         fingerprintManager,
         fingerprintSensorProvider,
         GatekeeperPasswordProvider(LockPatternUtils(context.applicationContext)),
-        pressToAuthInteractor,
-        Settings,
-        getIntent()
+        fingerprintEnrollStateRepository,
       )
 
     val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
diff --git a/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/Injector.kt b/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/Injector.kt
index 4d28650..ebdc504 100644
--- a/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/Injector.kt
+++ b/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/Injector.kt
@@ -91,6 +91,7 @@
     object : OrientationInteractor {
       override val orientation: Flow<Int> = flowOf(Configuration.ORIENTATION_LANDSCAPE)
       override val rotation: Flow<Int> = flowOf(Surface.ROTATION_0)
+      override val rotationFromDefault: Flow<Int> = rotation
 
       override fun getRotationFromDefault(rotation: Int): Int = rotation
     }
diff --git a/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/FingerprintEnrollFindSensorScreenshotTest.kt b/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/RfpsEnrollFindSensorScreenshotTest.kt
similarity index 73%
rename from tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/FingerprintEnrollFindSensorScreenshotTest.kt
rename to tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/RfpsEnrollFindSensorScreenshotTest.kt
index 18257c2..594aade 100644
--- a/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/FingerprintEnrollFindSensorScreenshotTest.kt
+++ b/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/RfpsEnrollFindSensorScreenshotTest.kt
@@ -17,9 +17,7 @@
  */
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollFindSensorV2Fragment
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep
-import com.android.settings.tests.screenshot.biometrics.fingerprint.Injector
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.education.RfpsEnrollFindSensorFragment
 import com.android.settings.tests.screenshot.biometrics.fingerprint.Injector.Companion.BiometricFragmentScreenShotRule
 import org.junit.Rule
 import org.junit.Test
@@ -28,10 +26,7 @@
 import platform.test.screenshot.ViewScreenshotTestRule.Mode
 
 @RunWith(AndroidJUnit4::class)
-class FingerprintEnrollFindSensorScreenshotTest {
-  private val injector: Injector =
-    Injector(FingerprintNavigationStep.Education(Injector.interactor.sensorProp))
-
+class RfpsEnrollFindSensorScreenshotTest {
   @Rule @JvmField var rule: FragmentScreenshotTestRule = BiometricFragmentScreenShotRule()
 
   @Test
@@ -39,7 +34,7 @@
     rule.screenshotTest(
       "fp_enroll_find_sensor",
       Mode.MatchSize,
-      FingerprintEnrollFindSensorV2Fragment(injector.fingerprintSensor.sensorType, injector.factory),
+      RfpsEnrollFindSensorFragment(),
     )
   }
 }
diff --git a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt
index 58d18f6..900afd1 100644
--- a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt
+++ b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt
@@ -33,8 +33,8 @@
 import androidx.test.core.app.ApplicationProvider
 import com.android.settings.biometrics.GatekeeperPasswordProvider
 import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintEnrollInteractorImpl
 import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
-import com.android.settings.biometrics.fingerprint2.domain.interactor.PressToAuthInteractor
 import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor
 import com.android.settings.biometrics.fingerprint2.lib.model.Default
 import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason
@@ -82,10 +82,6 @@
   @Mock private lateinit var gateKeeperPasswordProvider: GatekeeperPasswordProvider
 
   private var testScope = TestScope(backgroundDispatcher)
-  private var pressToAuthInteractor =
-    object : PressToAuthInteractor {
-      override val isEnabled = flowOf(false)
-    }
 
   @Before
   fun setup() {
@@ -113,9 +109,12 @@
         fingerprintManager,
         fingerprintSensorRepository,
         gateKeeperPasswordProvider,
-        pressToAuthInteractor,
-        Default,
-        Intent(),
+        FingerprintEnrollInteractorImpl(
+          context,
+          FingerprintEnrollOptions.Builder().build(),
+          fingerprintManager,
+          Default,
+        ),
       )
   }
 
diff --git a/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt b/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt
index 5fcd772..4906e84 100644
--- a/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt
+++ b/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt
@@ -145,7 +145,8 @@
     orientationInteractor =
       object : OrientationInteractor {
         override val orientation: Flow<Int> = flowOf(Configuration.ORIENTATION_LANDSCAPE)
-        override val rotation: Flow<Int> = flowOf(Surface.ROTATION_0)
+          override val rotation: Flow<Int> = flowOf(Surface.ROTATION_0)
+          override val rotationFromDefault: Flow<Int> = flowOf(Surface.ROTATION_0)
         override fun getRotationFromDefault(rotation: Int): Int = rotation
       }
     underTest =