Implement basic Fingerprint functionality.

Test: Verified enroll/deletion/renaming/authentication flows.
Test: atest FingerprintSettingsViewModelTest
Test: atest FingerprintManagerInteractorTest
Bug: 280862076
Change-Id: Ic34fd89f01f24468d0f769ef0492e742d9330112
diff --git a/res/xml/security_settings_fingerprint_limbo.xml b/res/xml/security_settings_fingerprint_limbo.xml
index b0c06c7..02a3dfb 100644
--- a/res/xml/security_settings_fingerprint_limbo.xml
+++ b/res/xml/security_settings_fingerprint_limbo.xml
@@ -15,4 +15,37 @@
   ~ limitations under the License.
   -->
 
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"/>
\ No newline at end of file
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:title="@string/security_settings_fingerprint_preference_title">
+
+    <PreferenceCategory
+        android:key="security_settings_fingerprints_enrolled"
+        settings:controller="com.android.settings.biometrics.fingerprint.FingerprintsEnrolledCategoryPreferenceController">
+    </PreferenceCategory>
+
+    <androidx.preference.Preference
+        android:icon="@drawable/ic_add_24dp"
+        android:key="key_fingerprint_add"
+        android:title="@string/fingerprint_add_title" />
+
+    <PreferenceCategory
+        android:key="security_settings_fingerprint_unlock_category"
+        android:title="@string/security_settings_fingerprint_settings_preferences_category"
+        android:visibility="gone">
+
+        <com.android.settingslib.RestrictedSwitchPreference
+            android:key="security_settings_require_screen_on_to_auth"
+            android:title="@string/security_settings_require_screen_on_to_auth_title"
+            android:summary="@string/security_settings_require_screen_on_to_auth_description"
+            settings:keywords="@string/security_settings_require_screen_on_to_auth_keywords"
+            settings:controller="com.android.settings.biometrics.fingerprint.FingerprintSettingsRequireScreenOnToAuthPreferenceController" />
+    </PreferenceCategory>
+
+    <PreferenceCategory
+        android:key="security_settings_fingerprint_footer">
+    </PreferenceCategory>
+
+</PreferenceScreen>
+
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt
new file mode 100644
index 0000000..2fbdedf
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+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.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.password.ChooseLockSettingsHelper
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+private const val TAG = "FingerprintManagerInteractor"
+
+/** Encapsulates business logic related to managing fingerprints. */
+interface FingerprintManagerInteractor {
+  /** Returns the list of current fingerprints. */
+  val enrolledFingerprints: Flow<List<FingerprintViewModel>>
+
+  /** Returns the max enrollable fingerprints, note during SUW this might be 1 */
+  val maxEnrollableFingerprints: Flow<Int>
+
+  /** Runs [FingerprintManager.authenticate] */
+  suspend fun authenticate(): FingerprintAuthAttemptViewModel
+
+  /**
+   * Generates a challenge with the provided [gateKeeperPasswordHandle] and on success returns a
+   * challenge and challenge token. This info can be used for secure operations such as
+   * [FingerprintManager.enroll]
+   *
+   * @param gateKeeperPasswordHandle GateKeeper password handle generated by a Confirm
+   * @return A [Pair] of the challenge and challenge token
+   */
+  suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray>
+
+  /** Returns true if a user can enroll a fingerprint false otherwise. */
+  fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean>
+
+  /**
+   * Removes the given fingerprint, returning true if it was successfully removed and false
+   * otherwise
+   */
+  suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean
+
+  /** Renames the given fingerprint if one exists */
+  suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String)
+
+  /** Indicates if the device has side fingerprint */
+  suspend fun hasSideFps(): Boolean
+
+  /** Indicates if the press to auth feature has been enabled */
+  suspend fun pressToAuthEnabled(): Boolean
+
+  /** Retrieves the sensor properties of a device */
+  suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal>
+}
+
+class FingerprintManagerInteractorImpl(
+  applicationContext: Context,
+  private val backgroundDispatcher: CoroutineDispatcher,
+  private val fingerprintManager: FingerprintManager,
+  private val gatekeeperPasswordProvider: GatekeeperPasswordProvider,
+  private val pressToAuthProvider: () -> Boolean,
+) : FingerprintManagerInteractor {
+
+  private val maxFingerprints =
+    applicationContext.resources.getInteger(
+      com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser
+    )
+  private val applicationContext = applicationContext.applicationContext
+
+  override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> =
+    suspendCoroutine {
+      val callback = GenerateChallengeCallback { _, userId, challenge ->
+        val intent = Intent()
+        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle)
+        val challengeToken =
+          gatekeeperPasswordProvider.requestGatekeeperHat(intent, challenge, userId)
+
+        gatekeeperPasswordProvider.removeGatekeeperPasswordHandle(intent, false)
+        val p = Pair(challenge, challengeToken)
+        it.resume(p)
+      }
+      fingerprintManager.generateChallenge(applicationContext.userId, callback)
+    }
+
+  override val enrolledFingerprints: Flow<List<FingerprintViewModel>> = flow {
+    emit(
+      fingerprintManager
+        .getEnrolledFingerprints(applicationContext.userId)
+        .map { (FingerprintViewModel(it.name.toString(), it.biometricId, it.deviceId)) }
+        .toList()
+    )
+  }
+
+  override fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean> = flow {
+    emit(numFingerprints < maxFingerprints)
+  }
+
+  override val maxEnrollableFingerprints = flow { emit(maxFingerprints) }
+
+  override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean = suspendCoroutine {
+    val callback =
+      object : RemovalCallback() {
+        override fun onRemovalError(
+          fp: android.hardware.fingerprint.Fingerprint,
+          errMsgId: Int,
+          errString: CharSequence
+        ) {
+          it.resume(false)
+        }
+
+        override fun onRemovalSucceeded(
+          fp: android.hardware.fingerprint.Fingerprint?,
+          remaining: Int
+        ) {
+          it.resume(true)
+        }
+      }
+    fingerprintManager.remove(
+      android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId),
+      applicationContext.userId,
+      callback
+    )
+  }
+
+  override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
+    withContext(backgroundDispatcher) {
+      fingerprintManager.rename(fp.fingerId, applicationContext.userId, newName)
+    }
+  }
+
+  override suspend fun hasSideFps(): Boolean = suspendCancellableCoroutine {
+    it.resume(fingerprintManager.isPowerbuttonFps)
+  }
+
+  override suspend fun pressToAuthEnabled(): Boolean = suspendCancellableCoroutine {
+    it.resume(pressToAuthProvider())
+  }
+
+  override suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal> =
+    suspendCancellableCoroutine {
+      it.resume(fingerprintManager.sensorPropertiesInternal)
+    }
+
+  override suspend fun authenticate(): FingerprintAuthAttemptViewModel =
+    suspendCancellableCoroutine { c: CancellableContinuation<FingerprintAuthAttemptViewModel> ->
+      val authenticationCallback =
+        object : FingerprintManager.AuthenticationCallback() {
+
+          override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+            super.onAuthenticationError(errorCode, errString)
+            if (c.isCompleted) {
+              Log.d(TAG, "framework sent down onAuthError after finish")
+              return
+            }
+            c.resume(FingerprintAuthAttemptViewModel.Error(errorCode, errString.toString()))
+          }
+
+          override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
+            super.onAuthenticationSucceeded(result)
+            if (c.isCompleted) {
+              Log.d(TAG, "framework sent down onAuthError after finish")
+              return
+            }
+            c.resume(FingerprintAuthAttemptViewModel.Success(result.fingerprint?.biometricId ?: -1))
+          }
+        }
+
+      val cancellationSignal = CancellationSignal()
+      c.invokeOnCancellation { cancellationSignal.cancel() }
+      fingerprintManager.authenticate(
+        null,
+        cancellationSignal,
+        authenticationCallback,
+        null,
+        applicationContext.userId
+      )
+    }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintSettingsViewBinder.kt b/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintSettingsViewBinder.kt
new file mode 100644
index 0000000..d9f3e43
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintSettingsViewBinder.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.binder
+
+import android.hardware.fingerprint.FingerprintManager
+import android.util.Log
+import androidx.lifecycle.LifecycleCoroutineScope
+import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintSettingsViewBinder.FingerprintView
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollAdditionalFingerprint
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintStateViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchedActivity
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.PreferenceViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+private const val TAG = "FingerprintSettingsViewBinder"
+
+/** Binds a [FingerprintSettingsViewModel] to a [FingerprintView] */
+object FingerprintSettingsViewBinder {
+
+  interface FingerprintView {
+    /**
+     * Helper function to launch fingerprint enrollment(This should be the default behavior when a
+     * user enters their PIN/PATTERN/PASS and no fingerprints are enrolled).
+     */
+    fun launchFullFingerprintEnrollment(
+      userId: Int,
+      gateKeeperPasswordHandle: Long?,
+      challenge: Long?,
+      challengeToken: ByteArray?
+    )
+
+    /** Helper to launch an add fingerprint request */
+    fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?)
+    /**
+     * Helper function that will try and launch confirm lock, if that fails we will prompt user to
+     * choose a PIN/PATTERN/PASS.
+     */
+    fun launchConfirmOrChooseLock(userId: Int)
+
+    /** Used to indicate that FingerprintSettings is finished. */
+    fun finish()
+
+    /** Indicates what result should be set for the returning callee */
+    fun setResultExternal(resultCode: Int)
+    /** Indicates the settings UI should be shown */
+    fun showSettings(state: FingerprintStateViewModel)
+    /** Indicates that a user has been locked out */
+    fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error)
+    /** Indicates a fingerprint preference should be highlighted */
+    suspend fun highlightPref(fingerId: Int)
+    /** Indicates a user should be prompted to delete a fingerprint */
+    suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintViewModel): Boolean
+    /** Indicates a user should be asked to renae ma dialog */
+    suspend fun askUserToRenameDialog(
+      fingerprintViewModel: FingerprintViewModel
+    ): Pair<FingerprintViewModel, String>?
+  }
+
+  fun bind(
+    view: FingerprintView,
+    viewModel: FingerprintSettingsViewModel,
+    navigationViewModel: FingerprintSettingsNavigationViewModel,
+    lifecycleScope: LifecycleCoroutineScope,
+  ) {
+
+    /** Result listener for launching enrollments **after** a user has reached the settings page. */
+
+    // Settings display flow
+    lifecycleScope.launch {
+      viewModel.fingerprintState.filterNotNull().collect { view.showSettings(it) }
+    }
+
+    // Dialog flow
+    lifecycleScope.launch {
+      viewModel.isShowingDialog.collectLatest {
+        if (it == null) {
+          return@collectLatest
+        }
+        when (it) {
+          is PreferenceViewModel.RenameDialog -> {
+            val willRename = view.askUserToRenameDialog(it.fingerprintViewModel)
+            if (willRename != null) {
+              Log.d(TAG, "renaming fingerprint $it")
+              viewModel.renameFingerprint(willRename.first, willRename.second)
+            }
+            viewModel.onRenameDialogFinished()
+          }
+          is PreferenceViewModel.DeleteDialog -> {
+            if (view.askUserToDeleteDialog(it.fingerprintViewModel)) {
+              Log.d(TAG, "deleting fingerprint $it")
+              viewModel.deleteFingerprint(it.fingerprintViewModel)
+            }
+            viewModel.onDeleteDialogFinished()
+          }
+        }
+      }
+    }
+
+    // Auth flow
+    lifecycleScope.launch {
+      viewModel.authFlow.filterNotNull().collect {
+        when (it) {
+          is FingerprintAuthAttemptViewModel.Success -> {
+            view.highlightPref(it.fingerId)
+          }
+          is FingerprintAuthAttemptViewModel.Error -> {
+            if (it.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
+              view.userLockout(it)
+            }
+          }
+        }
+      }
+    }
+
+    // Launch this on Dispatchers.Default and not main.
+    // Otherwise it takes too long for state transitions such as PIN/PATTERN/PASS
+    // to enrollment, which makes gives the user a janky experience.
+    lifecycleScope.launch(Dispatchers.Default) {
+      var settingsShowingJob: Job? = null
+      navigationViewModel.nextStep.filterNotNull().collect { nextStep ->
+        settingsShowingJob?.cancel()
+        settingsShowingJob = null
+        Log.d(TAG, "next step = $nextStep")
+        when (nextStep) {
+          is EnrollFirstFingerprint ->
+            view.launchFullFingerprintEnrollment(
+              nextStep.userId,
+              nextStep.gateKeeperPasswordHandle,
+              nextStep.challenge,
+              nextStep.challengeToken
+            )
+          is EnrollAdditionalFingerprint ->
+            view.launchAddFingerprint(nextStep.userId, nextStep.challengeToken)
+          is LaunchConfirmDeviceCredential -> view.launchConfirmOrChooseLock(nextStep.userId)
+          is FinishSettings -> {
+            Log.d(TAG, "Finishing due to ${nextStep.reason}")
+            view.finish()
+          }
+          is FinishSettingsWithResult -> {
+            Log.d(TAG, "Finishing with result ${nextStep.result} due to ${nextStep.reason}")
+            view.setResultExternal(nextStep.result)
+            view.finish()
+          }
+          is ShowSettings -> Log.d(TAG, "Showing settings")
+          is LaunchedActivity -> Log.d(TAG, "Launched activity, awaiting result")
+        }
+      }
+    }
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintViewBinder.kt b/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintViewBinder.kt
deleted file mode 100644
index d4249ff..0000000
--- a/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintViewBinder.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.biometrics.fingerprint2.ui.binder
-
-import androidx.lifecycle.LifecycleCoroutineScope
-import com.android.settings.biometrics.fingerprint2.ui.fragment.FingerprintSettingsV2Fragment
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollAdditionalFingerprint
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.launch
-
-/**
- * Binds a [FingerprintSettingsViewModel] to a [FingerprintSettingsV2Fragment]
- */
-object FingerprintViewBinder {
-
-    interface Binding {
-        fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?)
-        fun onEnrollSuccess()
-        fun onEnrollAdditionalFailure()
-        fun onEnrollFirstFailure(reason: String)
-        fun onEnrollFirstFailure(reason: String, resultCode: Int)
-        fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?)
-    }
-
-    /** Initial listener for the first enrollment request */
-    fun bind(
-        viewModel: FingerprintSettingsViewModel,
-        lifecycleScope: LifecycleCoroutineScope,
-        token: ByteArray?,
-        challenge: Long?,
-        launchFullFingerprintEnrollment: (
-            userId: Int,
-            gateKeeperPasswordHandle: Long?,
-            challenge: Long?,
-            challengeToken: ByteArray?
-        ) -> Unit,
-        launchAddFingerprint: (userId: Int, challengeToken: ByteArray?) -> Unit,
-        launchConfirmOrChooseLock: (userId: Int) -> Unit,
-        finish: () -> Unit,
-        setResultExternal: (resultCode: Int) -> Unit,
-    ): Binding {
-
-        lifecycleScope.launch {
-            viewModel.nextStep.filterNotNull().collect { nextStep ->
-                when (nextStep) {
-                    is EnrollFirstFingerprint -> launchFullFingerprintEnrollment(
-                        nextStep.userId,
-                        nextStep.gateKeeperPasswordHandle,
-                        nextStep.challenge,
-                        nextStep.challengeToken
-                    )
-
-                    is EnrollAdditionalFingerprint -> launchAddFingerprint(
-                        nextStep.userId, nextStep.challengeToken
-                    )
-
-                    is LaunchConfirmDeviceCredential -> launchConfirmOrChooseLock(nextStep.userId)
-
-                    is FinishSettings -> {
-                        println("Finishing due to ${nextStep.reason}")
-                        finish()
-                    }
-
-                    is FinishSettingsWithResult -> {
-                        println("Finishing with result ${nextStep.result} due to ${nextStep.reason}")
-                        setResultExternal(nextStep.result)
-                        finish()
-                    }
-
-                    is ShowSettings -> println("show settings")
-                }
-
-                viewModel.onUiCommandExecuted()
-            }
-        }
-
-        viewModel.updateTokenAndChallenge(token, if (challenge == -1L) null else challenge)
-
-        return object : Binding {
-            override fun onConfirmDevice(
-                wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?
-            ) {
-                viewModel.onConfirmDevice(wasSuccessful, theGateKeeperPasswordHandle)
-            }
-
-            override fun onEnrollSuccess() {
-                viewModel.onEnrollSuccess()
-            }
-
-            override fun onEnrollAdditionalFailure() {
-                viewModel.onEnrollAdditionalFailure()
-            }
-
-            override fun onEnrollFirstFailure(reason: String) {
-                viewModel.onEnrollFirstFailure(reason)
-            }
-
-            override fun onEnrollFirstFailure(reason: String, resultCode: Int) {
-                viewModel.onEnrollFirstFailure(reason, resultCode)
-            }
-
-            override fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?) {
-                viewModel.onEnrollFirst(token, keyChallenge)
-            }
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintDeletionDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintDeletionDialog.kt
new file mode 100644
index 0000000..42e2047
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintDeletionDialog.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.fragment
+
+import android.app.Dialog
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_FINGERPRINT_LAST_DELETE_MESSAGE
+import android.app.admin.DevicePolicyResources.UNDEFINED
+import android.app.settings.SettingsEnums
+import android.content.DialogInterface
+import android.os.Bundle
+import android.os.UserManager
+import androidx.appcompat.app.AlertDialog
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+private const val KEY_IS_LAST_FINGERPRINT = "IS_LAST_FINGERPRINT"
+
+class FingerprintDeletionDialog : InstrumentedDialogFragment() {
+  private lateinit var fingerprintViewModel: FingerprintViewModel
+  private var isLastFingerprint: Boolean = false
+  private lateinit var alertDialog: AlertDialog
+  lateinit var onClickListener: DialogInterface.OnClickListener
+  lateinit var onNegativeClickListener: DialogInterface.OnClickListener
+  lateinit var onCancelListener: DialogInterface.OnCancelListener
+
+  override fun getMetricsCategory(): Int {
+    return SettingsEnums.DIALOG_FINGERPINT_EDIT
+  }
+
+  override fun onCancel(dialog: DialogInterface) {
+    onCancelListener.onCancel(dialog)
+  }
+
+  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+    val fp = requireArguments().get(KEY_FINGERPRINT) as android.hardware.fingerprint.Fingerprint
+    fingerprintViewModel = FingerprintViewModel(fp.name.toString(), fp.biometricId, fp.deviceId)
+    isLastFingerprint = requireArguments().getBoolean(KEY_IS_LAST_FINGERPRINT)
+    val title = getString(R.string.fingerprint_delete_title, fingerprintViewModel.name)
+    var message = getString(R.string.fingerprint_v2_delete_message, fingerprintViewModel.name)
+    val context = requireContext()
+
+    if (isLastFingerprint) {
+      val isProfileChallengeUser = UserManager.get(context).isManagedProfile(context.userId)
+      val messageId =
+        if (isProfileChallengeUser) {
+          WORK_PROFILE_FINGERPRINT_LAST_DELETE_MESSAGE
+        } else {
+          UNDEFINED
+        }
+      val defaultMessageId =
+        if (isProfileChallengeUser) {
+          R.string.fingerprint_last_delete_message_profile_challenge
+        } else {
+          R.string.fingerprint_last_delete_message
+        }
+      val devicePolicyManager = requireContext().getSystemService(DevicePolicyManager::class.java)
+      message =
+        devicePolicyManager?.resources?.getString(messageId) {
+          message + "\n\n" + context.getString(defaultMessageId)
+        }
+          ?: ""
+    }
+
+    alertDialog =
+      AlertDialog.Builder(requireActivity())
+        .setTitle(title)
+        .setMessage(message)
+        .setPositiveButton(
+          R.string.security_settings_fingerprint_enroll_dialog_delete,
+          onClickListener
+        )
+        .setNegativeButton(R.string.cancel, onNegativeClickListener)
+        .create()
+    return alertDialog
+  }
+
+  companion object {
+    private const val KEY_FINGERPRINT = "fingerprint"
+    suspend fun showInstance(
+      fp: FingerprintViewModel,
+      lastFingerprint: Boolean,
+      target: FingerprintSettingsV2Fragment,
+    ) = suspendCancellableCoroutine { continuation ->
+      val dialog = FingerprintDeletionDialog()
+      dialog.onClickListener = DialogInterface.OnClickListener { _, _ -> continuation.resume(true) }
+      dialog.onNegativeClickListener =
+        DialogInterface.OnClickListener { _, _ -> continuation.resume(false) }
+      dialog.onCancelListener = DialogInterface.OnCancelListener { continuation.resume(false) }
+
+      continuation.invokeOnCancellation { dialog.dismiss() }
+      val bundle = Bundle()
+      bundle.putObject(
+        KEY_FINGERPRINT,
+        android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId)
+      )
+      bundle.putBoolean(KEY_IS_LAST_FINGERPRINT, lastFingerprint)
+      dialog.arguments = bundle
+      dialog.show(target.parentFragmentManager, FingerprintDeletionDialog::class.java.toString())
+    }
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsPreference.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsPreference.kt
new file mode 100644
index 0000000..e12785d
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsPreference.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.fragment
+
+import android.content.Context
+import android.util.Log
+import android.view.View
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.PreferenceViewHolder
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settingslib.widget.TwoTargetPreference
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+private const val TAG = "FingerprintSettingsPreference"
+
+class FingerprintSettingsPreference(
+  context: Context,
+  val fingerprintViewModel: FingerprintViewModel,
+  val fragment: FingerprintSettingsV2Fragment,
+  val isLastFingerprint: Boolean
+) : TwoTargetPreference(context) {
+  private lateinit var myView: View
+
+  init {
+    key = "FINGERPRINT_" + fingerprintViewModel.fingerId
+    Log.d(TAG, "FingerprintPreference $this with frag $fragment $key")
+    title = fingerprintViewModel.name
+    isPersistent = false
+    setIcon(R.drawable.ic_fingerprint_24dp)
+    setOnPreferenceClickListener {
+      fragment.lifecycleScope.launch { fragment.onPrefClicked(fingerprintViewModel) }
+      true
+    }
+  }
+
+  override fun onBindViewHolder(view: PreferenceViewHolder) {
+    super.onBindViewHolder(view)
+    myView = view.itemView
+    view.itemView.findViewById<View>(R.id.delete_button)?.setOnClickListener {
+      fragment.lifecycleScope.launch { fragment.onDeletePrefClicked(fingerprintViewModel) }
+    }
+  }
+
+  /** Highlights this dialog. */
+  suspend fun highlight() {
+    fragment.activity?.getDrawable(R.drawable.preference_highlight)?.let { highlight ->
+      val centerX: Float = myView.width / 2.0f
+      val centerY: Float = myView.height / 2.0f
+      highlight.setHotspot(centerX, centerY)
+      myView.background = highlight
+      myView.isPressed = true
+      myView.isPressed = false
+      delay(300)
+      myView.background = null
+    }
+  }
+
+  override fun getSecondTargetResId(): Int {
+    return R.layout.preference_widget_delete
+  }
+
+  suspend fun askUserToDeleteDialog(): Boolean {
+    return FingerprintDeletionDialog.showInstance(fingerprintViewModel, isLastFingerprint, fragment)
+  }
+
+  suspend fun askUserToRenameDialog(): Pair<FingerprintViewModel, String>? {
+    return FingerprintSettingsRenameDialog.showInstance(fingerprintViewModel, fragment)
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsRenameDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsRenameDialog.kt
new file mode 100644
index 0000000..a08b3db
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsRenameDialog.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.fragment
+
+import android.app.Dialog
+import android.app.settings.SettingsEnums
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.InputFilter
+import android.text.Spanned
+import android.text.TextUtils
+import android.util.Log
+import android.widget.ImeAwareEditText
+import androidx.appcompat.app.AlertDialog
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+private const val TAG = "FingerprintSettingsRenameDialog"
+
+class FingerprintSettingsRenameDialog : InstrumentedDialogFragment() {
+  lateinit var onClickListener: DialogInterface.OnClickListener
+  lateinit var onCancelListener: DialogInterface.OnCancelListener
+
+  override fun onCancel(dialog: DialogInterface) {
+    Log.d(TAG, "onCancel $dialog")
+    onCancelListener.onCancel(dialog)
+  }
+
+  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+    Log.d(TAG, "onCreateDialog $this")
+    val fp = requireArguments().get(KEY_FINGERPRINT) as android.hardware.fingerprint.Fingerprint
+    val fingerprintViewModel = FingerprintViewModel(fp.name.toString(), fp.biometricId, fp.deviceId)
+
+    val context = requireContext()
+    val alertDialog =
+      AlertDialog.Builder(context)
+        .setView(R.layout.fingerprint_rename_dialog)
+        .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok, onClickListener)
+        .create()
+    alertDialog.setOnShowListener {
+      (dialog?.findViewById(R.id.fingerprint_rename_field) as ImeAwareEditText?)?.apply {
+        val name = fingerprintViewModel.name
+        setText(name)
+        filters = this@FingerprintSettingsRenameDialog.getFilters()
+        selectAll()
+        requestFocus()
+        scheduleShowSoftInput()
+      }
+    }
+
+    return alertDialog
+  }
+
+  private fun getFilters(): Array<InputFilter> {
+    val filter: InputFilter =
+      object : InputFilter {
+
+        override fun filter(
+          source: CharSequence,
+          start: Int,
+          end: Int,
+          dest: Spanned?,
+          dstart: Int,
+          dend: Int
+        ): CharSequence? {
+          for (index in start until end) {
+            val c = source[index]
+            // KXMLSerializer does not allow these characters,
+            // see KXmlSerializer.java:162.
+            if (c.code < 0x20) {
+              return ""
+            }
+          }
+          return null
+        }
+      }
+    return arrayOf(filter)
+  }
+
+  override fun getMetricsCategory(): Int {
+    return SettingsEnums.DIALOG_FINGERPINT_EDIT
+  }
+
+  companion object {
+    private const val KEY_FINGERPRINT = "fingerprint"
+
+    suspend fun showInstance(fp: FingerprintViewModel, target: FingerprintSettingsV2Fragment) =
+      suspendCancellableCoroutine { continuation ->
+        val dialog = FingerprintSettingsRenameDialog()
+        val onClick =
+          DialogInterface.OnClickListener { _, _ ->
+            val dialogTextField =
+              dialog.requireDialog().findViewById(R.id.fingerprint_rename_field) as ImeAwareEditText
+            val newName = dialogTextField.text.toString()
+            if (!TextUtils.equals(newName, fp.name)) {
+              Log.d(TAG, "rename $fp.name to $newName for $dialog")
+              continuation.resume(Pair(fp, newName))
+            } else {
+              continuation.resume(null)
+            }
+          }
+
+        dialog.onClickListener = onClick
+        dialog.onCancelListener =
+          DialogInterface.OnCancelListener {
+            Log.d(TAG, "onCancelListener clicked $dialog")
+            continuation.resume(null)
+          }
+
+        continuation.invokeOnCancellation {
+          Log.d(TAG, "invokeOnCancellation $dialog")
+          dialog.dismiss()
+        }
+
+        val bundle = Bundle()
+        bundle.putObject(
+          KEY_FINGERPRINT,
+          android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId)
+        )
+        dialog.arguments = bundle
+        Log.d(TAG, "showing dialog $dialog")
+        dialog.show(
+          target.parentFragmentManager,
+          FingerprintSettingsRenameDialog::class.java.toString()
+        )
+      }
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt
index 9b85564..b82f7c1 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt
@@ -17,30 +17,65 @@
 package com.android.settings.biometrics.fingerprint2.ui.fragment
 
 import android.app.Activity
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRINT_UNLOCK_DISABLED_EXPLANATION
 import android.app.settings.SettingsEnums
 import android.content.Context.FINGERPRINT_SERVICE
 import android.content.Intent
 import android.hardware.fingerprint.FingerprintManager
 import android.os.Bundle
+import android.provider.Settings.Secure
+import android.text.TextUtils
 import android.util.FeatureFlagUtils
 import android.util.Log
-import androidx.activity.result.contract.ActivityResultContracts
+import android.view.View
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import com.android.internal.widget.LockPatternUtils
 import com.android.settings.R
-import com.android.settings.Utils
+import com.android.settings.Utils.SETTINGS_PACKAGE_NAME
 import com.android.settings.biometrics.BiometricEnrollBase
+import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST
+import com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY
+import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED
+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.ui.binder.FingerprintViewBinder
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
+import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintSettingsViewBinder
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
 import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintStateViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
 import com.android.settings.core.SettingsBaseActivity
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment
 import com.android.settings.dashboard.DashboardFragment
 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.HelpUtils
+import com.android.settingslib.RestrictedLockUtils
+import com.android.settingslib.RestrictedLockUtilsInternal
 import com.android.settingslib.transition.SettingsTransitionHelper
+import com.android.settingslib.widget.FooterPreference
+import com.google.android.setupdesign.util.DeviceHelper
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 
-const val TAG = "FingerprintSettingsV2Fragment"
+private const val TAG = "FingerprintSettingsV2Fragment"
+private const val KEY_FINGERPRINTS_ENROLLED_CATEGORY = "security_settings_fingerprints_enrolled"
+private const val KEY_FINGERPRINT_SIDE_FPS_CATEGORY =
+  "security_settings_fingerprint_unlock_category"
+private const val KEY_FINGERPRINT_ADD = "key_fingerprint_add"
+private const val KEY_FINGERPRINT_SIDE_FPS_SCREEN_ON_TO_AUTH =
+  "security_settings_require_screen_on_to_auth"
+private const val KEY_FINGERPRINT_FOOTER = "security_settings_fingerprint_footer"
 
 /**
  * A class responsible for showing FingerprintSettings. Typical activity Flows are
@@ -53,200 +88,494 @@
  * 3. Renaming a fingerprint
  * 4. Enabling/Disabling a feature
  */
-class FingerprintSettingsV2Fragment : DashboardFragment() {
-    private lateinit var binding: FingerprintViewBinder.Binding
+class FingerprintSettingsV2Fragment :
+  DashboardFragment(), FingerprintSettingsViewBinder.FingerprintView {
+  private lateinit var settingsViewModel: FingerprintSettingsViewModel
+  private lateinit var navigationViewModel: FingerprintSettingsNavigationViewModel
 
-    private val launchFirstEnrollmentListener =
-        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
-
-            val resultCode = result.resultCode
-            val data = result.data
-
-            Log.d(
-                TAG, "onEnrollFirstFingerprint($resultCode, $data)"
-            )
-            if (resultCode != BiometricEnrollBase.RESULT_FINISHED || data == null) {
-                if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
-                    binding.onEnrollFirstFailure(
-                        "Received RESULT_TIMEOUT when enrolling", resultCode
-                    )
-                } else {
-                    binding.onEnrollFirstFailure("Incorrect resultCode or data was null")
-                }
-            } else {
-                val token =
-                    data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
-                val keyChallenge = data.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
-                binding.onEnrollFirst(token, keyChallenge)
-            }
-        }
-
-    /** Result listener for launching enrollments **after** a user has reached the settings page. */
-    private val launchAdditionalFingerprintListener =
-        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
-            val resultCode = result.resultCode
-            Log.d(
-                TAG, "onEnrollAdditionalFingerprint($resultCode)"
-            )
-
-            if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
-                binding.onEnrollAdditionalFailure()
-            } else {
-                binding.onEnrollSuccess()
-            }
-        }
-
-    /** Result listener for ChooseLock activity flow. */
-    private val confirmDeviceResultListener =
-        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
-            val resultCode = result.resultCode
-            val data = result.data
-            onConfirmDevice(resultCode, data)
-        }
-
-    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-        // This is needed to support ChooseLockSettingBuilder...show(). All other activity
-        // calls should use the registerForActivity method call.
-        super.onActivityResult(requestCode, resultCode, data)
-        val wasSuccessful =
-            resultCode == BiometricEnrollBase.RESULT_FINISHED || resultCode == Activity.RESULT_OK
-        val gateKeeperPasswordHandle =
-            data?.getExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE) as Long?
-        binding.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
+  /** Result listener for ChooseLock activity flow. */
+  private val confirmDeviceResultListener =
+    registerForActivityResult(StartActivityForResult()) { result ->
+      val resultCode = result.resultCode
+      val data = result.data
+      onConfirmDevice(resultCode, data)
     }
 
+  /** Result listener for launching enrollments **after** a user has reached the settings page. */
+  private val launchAdditionalFingerprintListener: ActivityResultLauncher<Intent> =
+    registerForActivityResult(StartActivityForResult()) { result ->
+      lifecycleScope.launch {
+        val resultCode = result.resultCode
+        Log.d(TAG, "onEnrollAdditionalFingerprint($resultCode)")
 
-    override fun onCreate(icicle: Bundle?) {
-        super.onCreate(icicle)
-        if (!FeatureFlagUtils.isEnabled(
-                context, FeatureFlagUtils.SETTINGS_BIOMETRICS2_FINGERPRINT_SETTINGS
-            )
-        ) {
-            Log.d(
-                TAG, "Finishing due to feature not being enabled"
-            )
-            finish()
-            return
-        }
-        val viewModel = ViewModelProvider(
-            this, FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
-                requireContext().applicationContext.userId, requireContext().getSystemService(
-                    FINGERPRINT_SERVICE
-                ) as FingerprintManager
-            )
-        )[FingerprintSettingsViewModel::class.java]
-
-        val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
-        val challenge = intent.getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L)
-
-        binding = FingerprintViewBinder.bind(
-            viewModel,
-            lifecycleScope,
-            token,
-            challenge,
-            ::launchFullFingerprintEnrollment,
-            ::launchAddFingerprint,
-            ::launchConfirmOrChooseLock,
-            ::finish,
-            ::setResultExternal,
-        )
-    }
-
-    override fun getMetricsCategory(): Int {
-        return SettingsEnums.FINGERPRINT
-    }
-
-    override fun getPreferenceScreenResId(): Int {
-        return R.xml.security_settings_fingerprint_limbo
-    }
-
-    override fun getLogTag(): String {
-        return TAG
-    }
-
-    /**
-     * Helper function that will try and launch confirm lock, if that fails we will prompt user
-     * to choose a PIN/PATTERN/PASS.
-     */
-    private fun launchConfirmOrChooseLock(userId: Int) {
-        val intent = Intent()
-        val builder = ChooseLockSettingsHelper.Builder(requireActivity(), this)
-        val launched = builder.setRequestCode(BiometricEnrollBase.CONFIRM_REQUEST)
-            .setTitle(getString(R.string.security_settings_fingerprint_preference_title))
-            .setRequestGatekeeperPasswordHandle(true).setUserId(userId).setForegroundOnly(true)
-            .setReturnCredentials(true).show()
-        if (!launched) {
-            intent.setClassName(
-                Utils.SETTINGS_PACKAGE_NAME, ChooseLockGeneric::class.java.name
-            )
-            intent.putExtra(
-                ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true
-            )
-            intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true)
-            intent.putExtra(Intent.EXTRA_USER_ID, userId)
-            confirmDeviceResultListener.launch(intent)
-        }
-    }
-
-    /**
-     * Helper for confirming a PIN/PATTERN/PASS
-     */
-    private fun onConfirmDevice(resultCode: Int, data: Intent?) {
-        val wasSuccessful =
-            resultCode == BiometricEnrollBase.RESULT_FINISHED || resultCode == Activity.RESULT_OK
-        val gateKeeperPasswordHandle =
-            data?.getExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE) as Long?
-        binding.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
-    }
-
-    /**
-     * Helper function to launch fingerprint enrollment(This should be the default behavior
-     * when a user enters their PIN/PATTERN/PASS and no fingerprints are enrolled.
-     */
-    private fun launchFullFingerprintEnrollment(
-        userId: Int,
-        gateKeeperPasswordHandle: Long?,
-        challenge: Long?,
-        challengeToken: ByteArray?,
-    ) {
-        val intent = Intent()
-        intent.setClassName(
-            Utils.SETTINGS_PACKAGE_NAME, FingerprintEnrollIntroductionInternal::class.java.name
-        )
-        intent.putExtra(BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY, true)
-        intent.putExtra(
-            SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
-            SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE
-        )
-
-        intent.putExtra(Intent.EXTRA_USER_ID, userId)
-
-        if (gateKeeperPasswordHandle != null) {
-            intent.putExtra(
-                ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle
-            )
+        if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
+          navigationViewModel.onEnrollAdditionalFailure()
         } else {
-            intent.putExtra(
-                ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken
-            )
-            intent.putExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge)
+          navigationViewModel.onEnrollSuccess()
         }
-        launchFirstEnrollmentListener.launch(intent)
+      }
     }
 
-    private fun setResultExternal(resultCode: Int) {
-        setResult(resultCode)
+  /** Initial listener for the first enrollment request */
+  private val launchFirstEnrollmentListener: ActivityResultLauncher<Intent> =
+    registerForActivityResult(StartActivityForResult()) { result ->
+      lifecycleScope.launch {
+        val resultCode = result.resultCode
+        val data = result.data
+
+        Log.d(TAG, "onEnrollFirstFingerprint($resultCode, $data)")
+        if (resultCode != RESULT_FINISHED || data == null) {
+          if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
+            navigationViewModel.onEnrollFirstFailure(
+              "Received RESULT_TIMEOUT when enrolling",
+              resultCode
+            )
+          } else {
+            navigationViewModel.onEnrollFirstFailure(
+              "Incorrect resultCode or data was null",
+              resultCode
+            )
+          }
+        } else {
+          val token = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
+          val challenge = data.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
+          navigationViewModel.onEnrollFirst(token, challenge)
+        }
+      }
     }
 
-    /** Helper to launch an add fingerprint request */
-    private fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?) {
-        val intent = Intent()
-        intent.setClassName(
-            Utils.SETTINGS_PACKAGE_NAME, FingerprintEnrollEnrolling::class.qualifiedName.toString()
+  override fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error) {
+    Toast.makeText(activity, authAttemptViewModel.message, Toast.LENGTH_SHORT).show()
+  }
+
+  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+    // This is needed to support ChooseLockSettingBuilder...show(). All other activity
+    // calls should use the registerForActivity method call.
+    super.onActivityResult(requestCode, resultCode, data)
+    onConfirmDevice(resultCode, data)
+  }
+
+  override fun onCreate(icicle: Bundle?) {
+    super.onCreate(icicle)
+
+    if (icicle != null) {
+      Log.d(TAG, "onCreateWithSavedState")
+    } else {
+      Log.d(TAG, "onCreate()")
+    }
+
+    if (
+      !FeatureFlagUtils.isEnabled(
+        context,
+        FeatureFlagUtils.SETTINGS_BIOMETRICS2_FINGERPRINT_SETTINGS
+      )
+    ) {
+      Log.d(TAG, "Finishing due to feature not being enabled")
+      finish()
+      return
+    }
+
+    val context = requireContext()
+    val userId = context.userId
+
+    preferenceScreen.isVisible = false
+
+    val fingerprintManager = context.getSystemService(FINGERPRINT_SERVICE) as FingerprintManager
+
+    val backgroundDispatcher = Dispatchers.IO
+    val activity = requireActivity()
+    val userHandle = activity.user.identifier
+
+    val interactor =
+      FingerprintManagerInteractorImpl(
+        context.applicationContext,
+        backgroundDispatcher,
+        fingerprintManager,
+        GatekeeperPasswordProvider(LockPatternUtils(context.applicationContext))
+      ) {
+        var toReturn: Int =
+          Secure.getIntForUser(
+            context.contentResolver,
+            Secure.SFPS_PERFORMANT_AUTH_ENABLED,
+            -1,
+            userHandle,
+          )
+        if (toReturn == -1) {
+          toReturn =
+            if (
+              context.resources.getBoolean(com.android.internal.R.bool.config_performantAuthDefault)
+            ) {
+              1
+            } else {
+              0
+            }
+          Secure.putIntForUser(
+            context.contentResolver,
+            Secure.SFPS_PERFORMANT_AUTH_ENABLED,
+            toReturn,
+            userHandle
+          )
+        }
+
+        toReturn == 1
+      }
+
+    val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
+    val challenge = intent.getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L)
+
+    navigationViewModel =
+      ViewModelProvider(
+        this,
+        FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
+          userId,
+          interactor,
+          backgroundDispatcher,
+          token,
+          challenge
         )
-        intent.putExtra(Intent.EXTRA_USER_ID, userId)
-        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
-        launchAdditionalFingerprintListener.launch(intent)
+      )[FingerprintSettingsNavigationViewModel::class.java]
+
+    settingsViewModel =
+      ViewModelProvider(
+        this,
+        FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+          userId,
+          interactor,
+          backgroundDispatcher,
+          navigationViewModel,
+        )
+      )[FingerprintSettingsViewModel::class.java]
+
+    FingerprintSettingsViewBinder.bind(
+      this,
+      settingsViewModel,
+      navigationViewModel,
+      lifecycleScope,
+    )
+  }
+
+  override fun getMetricsCategory(): Int {
+    return SettingsEnums.FINGERPRINT
+  }
+
+  override fun getPreferenceScreenResId(): Int {
+    return R.xml.security_settings_fingerprint_limbo
+  }
+
+  override fun getLogTag(): String {
+    return TAG
+  }
+
+  override fun onStop() {
+    super.onStop()
+    navigationViewModel.maybeFinishActivity(requireActivity().isChangingConfigurations)
+  }
+
+  override fun onPause() {
+    super.onPause()
+    settingsViewModel.shouldAuthenticate(false)
+    val transaction = parentFragmentManager.beginTransaction()
+    for (frag in parentFragmentManager.fragments) {
+      if (frag is InstrumentedDialogFragment) {
+        Log.d(TAG, "removing dialog settings fragment $frag")
+        frag.dismiss()
+        transaction.remove(frag)
+      }
+    }
+    transaction.commit()
+  }
+
+  override fun onResume() {
+    super.onResume()
+    settingsViewModel.shouldAuthenticate(true)
+  }
+
+  /** Used to indicate that preference has been clicked */
+  fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) {
+    Log.d(TAG, "onPrefClicked(${fingerprintViewModel})")
+    settingsViewModel.onPrefClicked(fingerprintViewModel)
+  }
+
+  /** Used to indicate that a delete pref has been clicked */
+  fun onDeletePrefClicked(fingerprintViewModel: FingerprintViewModel) {
+    Log.d(TAG, "onDeletePrefClicked(${fingerprintViewModel})")
+    settingsViewModel.onDeleteClicked(fingerprintViewModel)
+  }
+
+  override fun showSettings(state: FingerprintStateViewModel) {
+    val category =
+      this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)
+        as PreferenceCategory?
+
+    category?.removeAll()
+
+    state.fingerprintViewModels.forEach { fingerprint ->
+      category?.addPreference(
+        FingerprintSettingsPreference(
+          requireContext(),
+          fingerprint,
+          this@FingerprintSettingsV2Fragment,
+          state.fingerprintViewModels.size == 1,
+        )
+      )
+    }
+    category?.isVisible = true
+
+    createFingerprintsFooterPreference(state.canEnroll, state.maxFingerprints)
+    preferenceScreen.isVisible = true
+
+    val sideFpsPref =
+      this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_SIDE_FPS_CATEGORY)
+        as PreferenceCategory?
+    sideFpsPref?.isVisible = false
+
+    if (state.hasSideFps) {
+      sideFpsPref?.isVisible = state.fingerprintViewModels.isNotEmpty()
+      val otherPref =
+        this@FingerprintSettingsV2Fragment.findPreference(
+          KEY_FINGERPRINT_SIDE_FPS_SCREEN_ON_TO_AUTH
+        ) as Preference?
+      otherPref?.isVisible = state.fingerprintViewModels.isNotEmpty()
+    }
+    addFooter(state.hasSideFps)
+  }
+  private fun addFooter(hasSideFps: Boolean) {
+    val footer =
+      this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_FOOTER)
+        as PreferenceCategory?
+    val admin =
+      RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
+        activity,
+        DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT,
+        requireActivity().userId
+      )
+    val activity = requireActivity()
+    val helpIntent =
+      HelpUtils.getHelpIntent(activity, getString(helpResource), activity::class.java.name)
+    val learnMoreClickListener =
+      View.OnClickListener { v: View? -> activity.startActivityForResult(helpIntent, 0) }
+
+    class FooterColumn {
+      var title: CharSequence? = null
+      var learnMoreOverrideText: CharSequence? = null
+      var learnMoreOnClickListener: View.OnClickListener? = null
     }
 
-}
\ No newline at end of file
+    var footerColumns = mutableListOf<FooterColumn>()
+    if (admin != null) {
+      val devicePolicyManager = getSystemService(DevicePolicyManager::class.java)
+      val column1 = FooterColumn()
+      column1.title =
+        devicePolicyManager.resources.getString(FINGERPRINT_UNLOCK_DISABLED_EXPLANATION) {
+          getString(R.string.security_fingerprint_disclaimer_lockscreen_disabled_1)
+        }
+
+      column1.learnMoreOnClickListener =
+        View.OnClickListener { _ ->
+          RestrictedLockUtils.sendShowAdminSupportDetailsIntent(activity, admin)
+        }
+      column1.learnMoreOverrideText = getText(R.string.admin_support_more_info)
+      footerColumns.add(column1)
+      val column2 = FooterColumn()
+      column2.title = getText(R.string.security_fingerprint_disclaimer_lockscreen_disabled_2)
+      if (hasSideFps) {
+        column2.learnMoreOverrideText =
+          getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
+      }
+      column2.learnMoreOnClickListener = learnMoreClickListener
+      footerColumns.add(column2)
+    } else {
+      val column = FooterColumn()
+      column.title =
+        getString(
+          R.string.security_settings_fingerprint_enroll_introduction_v3_message,
+          DeviceHelper.getDeviceName(requireActivity())
+        )
+      column.learnMoreOnClickListener = learnMoreClickListener
+      if (hasSideFps) {
+        column.learnMoreOverrideText =
+          getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
+      }
+      footerColumns.add(column)
+    }
+
+    footer?.removeAll()
+    for (i in 0 until footerColumns.size) {
+      val column = footerColumns[i]
+      val footerPrefToAdd: FooterPreference =
+        FooterPreference.Builder(requireContext()).setTitle(column.title).build()
+      if (i > 0) {
+        footerPrefToAdd.setIconVisibility(View.GONE)
+      }
+      if (column.learnMoreOnClickListener != null) {
+        footerPrefToAdd.setLearnMoreAction(column.learnMoreOnClickListener)
+        if (!TextUtils.isEmpty(column.learnMoreOverrideText)) {
+          footerPrefToAdd.setLearnMoreText(column.learnMoreOverrideText)
+        }
+      }
+      footer?.addPreference(footerPrefToAdd)
+    }
+  }
+
+  override suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintViewModel): Boolean {
+    Log.d(TAG, "showing delete dialog for (${fingerprintViewModel})")
+
+    try {
+      val willDelete =
+        fingerprintPreferences()
+          .first { it?.fingerprintViewModel == fingerprintViewModel }
+          ?.askUserToDeleteDialog()
+          ?: false
+      if (willDelete) {
+        mMetricsFeatureProvider.action(
+          context,
+          SettingsEnums.ACTION_FINGERPRINT_DELETE,
+          fingerprintViewModel.fingerId
+        )
+      }
+      return willDelete
+    } catch (exception: Exception) {
+      Log.d(TAG, "askUserToDeleteDialog exception $exception")
+      return false
+    }
+  }
+
+  override suspend fun askUserToRenameDialog(
+    fingerprintViewModel: FingerprintViewModel
+  ): Pair<FingerprintViewModel, String>? {
+    Log.d(TAG, "showing rename dialog for (${fingerprintViewModel})")
+    try {
+      val toReturn =
+        fingerprintPreferences()
+          .first { it?.fingerprintViewModel == fingerprintViewModel }
+          ?.askUserToRenameDialog()
+      if (toReturn != null) {
+        mMetricsFeatureProvider.action(
+          context,
+          SettingsEnums.ACTION_FINGERPRINT_RENAME,
+          toReturn.first.fingerId
+        )
+      }
+      return toReturn
+    } catch (exception: Exception) {
+      Log.d(TAG, "askUserToRenameDialog exception $exception")
+      return null
+    }
+  }
+
+  override suspend fun highlightPref(fingerId: Int) {
+    fingerprintPreferences()
+      .first { pref -> pref?.fingerprintViewModel?.fingerId == fingerId }
+      ?.highlight()
+  }
+
+  override fun launchConfirmOrChooseLock(userId: Int) {
+    lifecycleScope.launch(Dispatchers.Default) {
+      navigationViewModel.setStepToLaunched()
+      val intent = Intent()
+      val builder =
+        ChooseLockSettingsHelper.Builder(requireActivity(), this@FingerprintSettingsV2Fragment)
+      val launched =
+        builder
+          .setRequestCode(CONFIRM_REQUEST)
+          .setTitle(getString(R.string.security_settings_fingerprint_preference_title))
+          .setRequestGatekeeperPasswordHandle(true)
+          .setUserId(userId)
+          .setForegroundOnly(true)
+          .setReturnCredentials(true)
+          .show()
+      if (!launched) {
+        intent.setClassName(SETTINGS_PACKAGE_NAME, ChooseLockGeneric::class.java.name)
+        intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true)
+        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true)
+        intent.putExtra(Intent.EXTRA_USER_ID, userId)
+        confirmDeviceResultListener.launch(intent)
+      }
+    }
+  }
+
+  override fun launchFullFingerprintEnrollment(
+    userId: Int,
+    gateKeeperPasswordHandle: Long?,
+    challenge: Long?,
+    challengeToken: ByteArray?,
+  ) {
+    navigationViewModel.setStepToLaunched()
+    Log.d(TAG, "launchFullFingerprintEnrollment")
+    val intent = Intent()
+    intent.setClassName(
+      SETTINGS_PACKAGE_NAME,
+      FingerprintEnrollIntroductionInternal::class.java.name
+    )
+    intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true)
+    intent.putExtra(
+      SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
+      SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE
+    )
+
+    intent.putExtra(Intent.EXTRA_USER_ID, userId)
+
+    if (gateKeeperPasswordHandle != null) {
+      intent.putExtra(EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle)
+    } else {
+      intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
+      intent.putExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge)
+    }
+    launchFirstEnrollmentListener.launch(intent)
+  }
+
+  override fun setResultExternal(resultCode: Int) {
+    setResult(resultCode)
+  }
+
+  override fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?) {
+    navigationViewModel.setStepToLaunched()
+    val intent = Intent()
+    intent.setClassName(
+      SETTINGS_PACKAGE_NAME,
+      FingerprintEnrollEnrolling::class.qualifiedName.toString()
+    )
+    intent.putExtra(Intent.EXTRA_USER_ID, userId)
+    intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
+    launchAdditionalFingerprintListener.launch(intent)
+  }
+
+  private fun onConfirmDevice(resultCode: Int, data: Intent?) {
+    val wasSuccessful = resultCode == RESULT_FINISHED || resultCode == Activity.RESULT_OK
+    val gateKeeperPasswordHandle = data?.getExtra(EXTRA_KEY_GK_PW_HANDLE) as Long?
+    lifecycleScope.launch {
+      navigationViewModel.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
+    }
+  }
+
+  private fun createFingerprintsFooterPreference(canEnroll: Boolean, maxFingerprints: Int) {
+    val pref = this@FingerprintSettingsV2Fragment.findPreference<Preference>(KEY_FINGERPRINT_ADD)
+    val maxSummary = context?.getString(R.string.fingerprint_add_max, maxFingerprints) ?: ""
+    pref?.summary = maxSummary
+    pref?.isEnabled = canEnroll
+    pref?.setOnPreferenceClickListener {
+      navigationViewModel.onAddFingerprintClicked()
+      true
+    }
+    pref?.isVisible = true
+  }
+
+  private fun fingerprintPreferences(): List<FingerprintSettingsPreference?> {
+    val category =
+      this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)
+        as PreferenceCategory?
+
+    return category?.let { cat ->
+      cat.childrenToList().map { it as FingerprintSettingsPreference? }
+    }
+      ?: emptyList()
+  }
+
+  private fun PreferenceCategory.childrenToList(): List<Preference> {
+    val mutable: MutableList<Preference> = mutableListOf()
+    for (i in 0 until this.preferenceCount) {
+      mutable.add(this.getPreference(i))
+    }
+    return mutable.toList()
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt
new file mode 100644
index 0000000..a3a5d3c
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.viewmodel
+
+import android.hardware.fingerprint.FingerprintManager
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.settings.biometrics.BiometricEnrollBase
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+/** A Viewmodel that represents the navigation of the FingerprintSettings activity. */
+class FingerprintSettingsNavigationViewModel(
+  private val userId: Int,
+  private val fingerprintManagerInteractor: FingerprintManagerInteractor,
+  private val backgroundDispatcher: CoroutineDispatcher,
+  tokenInit: ByteArray?,
+  challengeInit: Long?,
+) : ViewModel() {
+
+  private var token = tokenInit
+  private var challenge = challengeInit
+
+  private val _nextStep: MutableStateFlow<NextStepViewModel?> = MutableStateFlow(null)
+  /** This flow represents the high level state for the FingerprintSettingsV2Fragment. */
+  val nextStep: StateFlow<NextStepViewModel?> = _nextStep.asStateFlow()
+
+  init {
+    if (challengeInit == null || tokenInit == null) {
+      _nextStep.update { LaunchConfirmDeviceCredential(userId) }
+    } else {
+      viewModelScope.launch { showSettingsHelper() }
+    }
+  }
+
+  /** Used to indicate that FingerprintSettings is complete. */
+  fun finish() {
+    _nextStep.update { null }
+  }
+
+  /** Used to finish settings in certain cases. */
+  fun maybeFinishActivity(changingConfig: Boolean) {
+    val isConfirmingOrEnrolling =
+      _nextStep.value is LaunchConfirmDeviceCredential ||
+        _nextStep.value is EnrollAdditionalFingerprint ||
+        _nextStep.value is EnrollFirstFingerprint ||
+        _nextStep.value is LaunchedActivity
+    if (!isConfirmingOrEnrolling && !changingConfig)
+      _nextStep.update {
+        FinishSettingsWithResult(BiometricEnrollBase.RESULT_TIMEOUT, "onStop finishing settings")
+      }
+  }
+
+  /** Used to indicate that we have launched another activity and we should await its result. */
+  fun setStepToLaunched() {
+    _nextStep.update { LaunchedActivity }
+  }
+
+  /** Indicates a successful enroll has occurred */
+  fun onEnrollSuccess() {
+    showSettingsHelper()
+  }
+
+  /** Add fingerprint clicked */
+  fun onAddFingerprintClicked() {
+    _nextStep.update { EnrollAdditionalFingerprint(userId, token) }
+  }
+
+  /** Enrolling of an additional fingerprint failed */
+  fun onEnrollAdditionalFailure() {
+    launchFinishSettings("Failed to enroll additional fingerprint")
+  }
+
+  /** The first fingerprint enrollment failed */
+  fun onEnrollFirstFailure(reason: String) {
+    launchFinishSettings(reason)
+  }
+
+  /** The first fingerprint enrollment failed with a result code */
+  fun onEnrollFirstFailure(reason: String, resultCode: Int) {
+    launchFinishSettings(reason, resultCode)
+  }
+
+  /** Notifies that a users first enrollment succeeded. */
+  fun onEnrollFirst(theToken: ByteArray?, theChallenge: Long?) {
+    if (theToken == null) {
+      launchFinishSettings("Error, empty token")
+      return
+    }
+    if (theChallenge == null) {
+      launchFinishSettings("Error, empty keyChallenge")
+      return
+    }
+    token = theToken!!
+    challenge = theChallenge!!
+
+    showSettingsHelper()
+  }
+
+  /**
+   * Indicates to the view model that a confirm device credential action has been completed with a
+   * [theGateKeeperPasswordHandle] which will be used for [FingerprintManager] operations such as
+   * [FingerprintManager.enroll].
+   */
+  suspend fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?) {
+    if (!wasSuccessful) {
+      launchFinishSettings("ConfirmDeviceCredential was unsuccessful")
+      return
+    }
+    if (theGateKeeperPasswordHandle == null) {
+      launchFinishSettings("ConfirmDeviceCredential gatekeeper password was null")
+      return
+    }
+
+    launchEnrollNextStep(theGateKeeperPasswordHandle)
+  }
+
+  private fun showSettingsHelper() {
+    _nextStep.update { ShowSettings }
+  }
+
+  private suspend fun launchEnrollNextStep(gateKeeperPasswordHandle: Long?) {
+    fingerprintManagerInteractor.enrolledFingerprints.collect {
+      if (it.isEmpty()) {
+        _nextStep.update { EnrollFirstFingerprint(userId, gateKeeperPasswordHandle, null, null) }
+      } else {
+        viewModelScope.launch(backgroundDispatcher) {
+          val challengePair =
+            fingerprintManagerInteractor.generateChallenge(gateKeeperPasswordHandle!!)
+          challenge = challengePair.first
+          token = challengePair.second
+
+          showSettingsHelper()
+        }
+      }
+    }
+  }
+
+  private fun launchFinishSettings(reason: String) {
+    _nextStep.update { FinishSettings(reason) }
+  }
+
+  private fun launchFinishSettings(reason: String, errorCode: Int) {
+    _nextStep.update { FinishSettingsWithResult(errorCode, reason) }
+  }
+  class FingerprintSettingsNavigationModelFactory(
+    private val userId: Int,
+    private val interactor: FingerprintManagerInteractor,
+    private val backgroundDispatcher: CoroutineDispatcher,
+    private val token: ByteArray?,
+    private val challenge: Long?,
+  ) : ViewModelProvider.Factory {
+
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : ViewModel> create(
+      modelClass: Class<T>,
+    ): T {
+
+      return FingerprintSettingsNavigationViewModel(
+        userId,
+        interactor,
+        backgroundDispatcher,
+        token,
+        challenge,
+      )
+        as T
+    }
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt
index 6cddb24..554f336 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt
@@ -17,171 +17,308 @@
 package com.android.settings.biometrics.fingerprint2.ui.viewmodel
 
 import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
+import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.util.Log
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.combineTransform
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.last
+import kotlinx.coroutines.flow.sample
+import kotlinx.coroutines.flow.transformLatest
 import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
 
-/**
- * Models the UI state for fingerprint settings.
- */
+private const val TAG = "FingerprintSettingsViewModel"
+private const val DEBUG = false
+
+/** Models the UI state for fingerprint settings. */
 class FingerprintSettingsViewModel(
-    private val userId: Int,
-    gateKeeperPassword: Long?,
-    theChallenge: Long?,
-    theChallengeToken: ByteArray?,
-    private val fingerprintManager: FingerprintManager
+  private val userId: Int,
+  private val fingerprintManagerInteractor: FingerprintManagerInteractor,
+  private val backgroundDispatcher: CoroutineDispatcher,
+  private val navigationViewModel: FingerprintSettingsNavigationViewModel,
 ) : ViewModel() {
 
-    private val _nextStep: MutableStateFlow<NextStepViewModel?> = MutableStateFlow(null)
-    /**
-     *  This flow represents the high level state for the FingerprintSettingsV2Fragment. The
-     *  consumer of this flow should call [onUiCommandExecuted] which will set the state to null,
-     *  confirming that the UI has consumed the last command and is ready to consume another
-     *  command.
-     */
-    val nextStep = _nextStep.asStateFlow()
+  private val _consumerShouldAuthenticate: MutableStateFlow<Boolean> = MutableStateFlow(false)
 
+  private val fingerprintSensorPropertiesInternal:
+    MutableStateFlow<List<FingerprintSensorPropertiesInternal>?> =
+    MutableStateFlow(null)
 
-    private var gateKeeperPasswordHandle: Long? = gateKeeperPassword
-    private var challenge: Long? = theChallenge
-    private var challengeToken: ByteArray? = theChallengeToken
-
-    /**
-     * Indicates to the view model that a confirm device credential action has been completed
-     * with a [theGateKeeperPasswordHandle] which will be used for [FingerprintManager]
-     * operations such as [FingerprintManager.enroll].
-     */
-    fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?) {
-
-        if (!wasSuccessful) {
-            launchFinishSettings("ConfirmDeviceCredential was unsuccessful")
-            return
-        }
-        if (theGateKeeperPasswordHandle == null) {
-            launchFinishSettings("ConfirmDeviceCredential gatekeeper password was null")
-            return
-        }
-
-        gateKeeperPasswordHandle = theGateKeeperPasswordHandle
-        launchEnrollNextStep()
+  private val _isShowingDialog: MutableStateFlow<PreferenceViewModel?> = MutableStateFlow(null)
+  val isShowingDialog =
+    _isShowingDialog.combine(navigationViewModel.nextStep) { dialogFlow, nextStep ->
+      if (nextStep is ShowSettings) {
+        return@combine dialogFlow
+      } else {
+        return@combine null
+      }
     }
 
-    /**
-     * Notifies that enrollment was successful.
-     */
-    fun onEnrollSuccess() {
-        _nextStep.update {
-            ShowSettings(userId)
+  init {
+    viewModelScope.launch {
+      fingerprintSensorPropertiesInternal.update {
+        fingerprintManagerInteractor.sensorPropertiesInternal()
+      }
+    }
+
+    viewModelScope.launch {
+      navigationViewModel.nextStep.filterNotNull().collect {
+        _isShowingDialog.update { null }
+        if (it is ShowSettings) {
+          // reset state
+          updateSettingsData()
         }
+      }
+    }
+  }
+
+  private val _fingerprintStateViewModel: MutableStateFlow<FingerprintStateViewModel?> =
+    MutableStateFlow(null)
+  val fingerprintState: Flow<FingerprintStateViewModel?> =
+    _fingerprintStateViewModel.combineTransform(navigationViewModel.nextStep) {
+      settingsShowingViewModel,
+      currStep ->
+      if (currStep != null && currStep is ShowSettings) {
+        emit(settingsShowingViewModel)
+      }
     }
 
-    /**
-     * Notifies that an additional enrollment failed.
-     */
-    fun onEnrollAdditionalFailure() {
-        launchFinishSettings("Failed to enroll additional fingerprint")
-    }
+  private val _isLockedOut: MutableStateFlow<FingerprintAuthAttemptViewModel.Error?> =
+    MutableStateFlow(null)
 
-    /**
-     * Notifies that the first enrollment failed.
-     */
-    fun onEnrollFirstFailure(reason: String) {
-        launchFinishSettings(reason)
-    }
+  private val _authSucceeded: MutableSharedFlow<FingerprintAuthAttemptViewModel.Success?> =
+    MutableSharedFlow()
 
-    /**
-     * Notifies that first enrollment failed (with resultCode)
-     */
-    fun onEnrollFirstFailure(reason: String, resultCode: Int) {
-        launchFinishSettings(reason, resultCode)
-    }
+  private val attemptsSoFar: MutableStateFlow<Int> = MutableStateFlow(0)
 
-    /**
-     * Notifies that a users first enrollment succeeded.
-     */
-    fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?) {
-        if (token == null) {
-            launchFinishSettings("Error, empty token")
-            return
+  /**
+   * This is a very tricky flow. The current fingerprint manager APIs are not robust, and a proper
+   * implementation would take quite a lot of code to implement, it might be easier to rewrite
+   * FingerprintManager.
+   *
+   * The hack to note is the sample(400), if we call authentications in too close of proximity
+   * without waiting for a response, the fingerprint manager will send us the results of the
+   * previous attempt.
+   */
+  private val canAuthenticate: Flow<Boolean> =
+    combine(
+        _isShowingDialog,
+        navigationViewModel.nextStep,
+        _consumerShouldAuthenticate,
+        _fingerprintStateViewModel,
+        _isLockedOut,
+        attemptsSoFar,
+        fingerprintSensorPropertiesInternal
+      ) { dialogShowing, step, resume, fingerprints, isLockedOut, attempts, sensorProps ->
+        if (DEBUG) {
+          Log.d(
+            TAG,
+            "canAuthenticate(isShowingDialog=${dialogShowing != null}," +
+              "nextStep=${step}," +
+              "resumed=${resume}," +
+              "fingerprints=${fingerprints}," +
+              "lockedOut=${isLockedOut}," +
+              "attempts=${attempts}," +
+              "sensorProps=${sensorProps}"
+          )
         }
-        if (keyChallenge == null) {
-            launchFinishSettings("Error, empty keyChallenge")
-            return
+        if (sensorProps.isNullOrEmpty()) {
+          return@combine false
         }
-        challengeToken = token
-        challenge = keyChallenge
-
-        _nextStep.update {
-            ShowSettings(userId)
+        val sensorType = sensorProps[0].sensorType
+        if (listOf(TYPE_UDFPS_OPTICAL, TYPE_UDFPS_ULTRASONIC).contains(sensorType)) {
+          return@combine false
         }
-    }
 
+        if (step != null && step is ShowSettings) {
+          if (fingerprints?.fingerprintViewModels?.isNotEmpty() == true) {
+            return@combine dialogShowing == null && isLockedOut == null && resume && attempts < 15
+          }
+        }
+        false
+      }
+      .sample(400)
+      .distinctUntilChanged()
 
-    /**
-     * Indicates if this settings activity has been called with correct token and challenge
-     * and that we do not need to launch confirm device credential.
-     */
-    fun updateTokenAndChallenge(token: ByteArray?, theChallenge: Long?) {
-        challengeToken = token
-        challenge = theChallenge
-        if (challengeToken == null) {
-            _nextStep.update {
-                LaunchConfirmDeviceCredential(userId)
+  /** Represents a consistent stream of authentication attempts. */
+  val authFlow: Flow<FingerprintAuthAttemptViewModel> =
+    canAuthenticate
+      .transformLatest {
+        try {
+          Log.d(TAG, "canAuthenticate $it")
+          while (it && navigationViewModel.nextStep.value is ShowSettings) {
+            Log.d(TAG, "canAuthenticate authing")
+            attemptingAuth()
+            when (val authAttempt = fingerprintManagerInteractor.authenticate()) {
+              is FingerprintAuthAttemptViewModel.Success -> {
+                onAuthSuccess(authAttempt)
+                emit(authAttempt)
+              }
+              is FingerprintAuthAttemptViewModel.Error -> {
+                if (authAttempt.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
+                  lockout(authAttempt)
+                  emit(authAttempt)
+                  return@transformLatest
+                }
+              }
             }
-        } else {
-            launchEnrollNextStep()
+          }
+        } catch (exception: Exception) {
+          Log.d(TAG, "shouldAuthenticate exception $exception")
         }
+      }
+      .flowOn(backgroundDispatcher)
+
+  /** The rename dialog has finished */
+  fun onRenameDialogFinished() {
+    _isShowingDialog.update { null }
+  }
+
+  /** The delete dialog has finished */
+  fun onDeleteDialogFinished() {
+    _isShowingDialog.update { null }
+  }
+
+  override fun toString(): String {
+    return "userId: $userId\n" + "fingerprintState: ${_fingerprintStateViewModel.value}\n"
+  }
+
+  /** The fingerprint delete button has been clicked. */
+  fun onDeleteClicked(fingerprintViewModel: FingerprintViewModel) {
+    viewModelScope.launch {
+      if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) {
+        _isShowingDialog.tryEmit(PreferenceViewModel.DeleteDialog(fingerprintViewModel))
+      } else {
+        Log.d(TAG, "Ignoring onDeleteClicked due to dialog showing ${_isShowingDialog.value}")
+      }
     }
+  }
 
-    /**
-     * Indicates a UI command has been consumed by the UI, and the logic can send another
-     * UI command.
-     */
-    fun onUiCommandExecuted() {
-        _nextStep.update {
-            null
-        }
+  /** The rename fingerprint dialog has been clicked. */
+  fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) {
+    viewModelScope.launch {
+      if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) {
+        _isShowingDialog.tryEmit(PreferenceViewModel.RenameDialog(fingerprintViewModel))
+      } else {
+        Log.d(TAG, "Ignoring onPrefClicked due to dialog showing ${_isShowingDialog.value}")
+      }
     }
+  }
 
-    private fun launchEnrollNextStep() {
-        if (fingerprintManager.getEnrolledFingerprints(userId).isEmpty()) {
-            _nextStep.update {
-                EnrollFirstFingerprint(userId, gateKeeperPasswordHandle, challenge, challengeToken)
-            }
-        } else {
-            _nextStep.update {
-                ShowSettings(userId)
-            }
-        }
+  /** A request to delete a fingerprint */
+  fun deleteFingerprint(fp: FingerprintViewModel) {
+    viewModelScope.launch(backgroundDispatcher) {
+      if (fingerprintManagerInteractor.removeFingerprint(fp)) {
+        updateSettingsData()
+      }
     }
+  }
 
-    private fun launchFinishSettings(reason: String) {
-        _nextStep.update {
-            FinishSettings(reason)
-        }
+  /** A request to rename a fingerprint */
+  fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
+    viewModelScope.launch {
+      fingerprintManagerInteractor.renameFingerprint(fp, newName)
+      updateSettingsData()
     }
+  }
 
-    private fun launchFinishSettings(reason: String, errorCode: Int) {
-        _nextStep.update {
-            FinishSettingsWithResult(errorCode, reason)
-        }
+  private fun attemptingAuth() {
+    attemptsSoFar.update { it + 1 }
+  }
+
+  private suspend fun onAuthSuccess(success: FingerprintAuthAttemptViewModel.Success) {
+    _authSucceeded.emit(success)
+    attemptsSoFar.update { 0 }
+  }
+
+  private fun lockout(attemptViewModel: FingerprintAuthAttemptViewModel.Error) {
+    _isLockedOut.update { attemptViewModel }
+  }
+
+  /**
+   * This function is sort of a hack, it's used whenever we want to check for fingerprint state
+   * updates.
+   */
+  private suspend fun updateSettingsData() {
+    Log.d(TAG, "update settings data called")
+    val fingerprints = fingerprintManagerInteractor.enrolledFingerprints.last()
+    val canEnrollFingerprint =
+      fingerprintManagerInteractor.canEnrollFingerprints(fingerprints.size).last()
+    val maxFingerprints = fingerprintManagerInteractor.maxEnrollableFingerprints.last()
+    val hasSideFps = fingerprintManagerInteractor.hasSideFps()
+    val pressToAuthEnabled = fingerprintManagerInteractor.pressToAuthEnabled()
+    _fingerprintStateViewModel.update {
+      FingerprintStateViewModel(
+        fingerprints,
+        canEnrollFingerprint,
+        maxFingerprints,
+        hasSideFps,
+        pressToAuthEnabled
+      )
     }
+  }
 
-    class FingerprintSettingsViewModelFactory(
-        private val userId: Int,
-        private val fingerprintManager: FingerprintManager,
-    ) : ViewModelProvider.Factory {
+  /** Used to indicate whether the consumer of the view model is ready for authentication. */
+  fun shouldAuthenticate(authenticate: Boolean) {
+    _consumerShouldAuthenticate.update { authenticate }
+  }
 
-        @Suppress("UNCHECKED_CAST")
-        override fun <T : ViewModel> create(
-            modelClass: Class<T>,
-        ): T {
+  class FingerprintSettingsViewModelFactory(
+    private val userId: Int,
+    private val interactor: FingerprintManagerInteractor,
+    private val backgroundDispatcher: CoroutineDispatcher,
+    private val navigationViewModel: FingerprintSettingsNavigationViewModel,
+  ) : ViewModelProvider.Factory {
 
-            return FingerprintSettingsViewModel(
-                userId, null, null, null, fingerprintManager
-            ) as T
-        }
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : ViewModel> create(
+      modelClass: Class<T>,
+    ): T {
+
+      return FingerprintSettingsViewModel(
+        userId,
+        interactor,
+        backgroundDispatcher,
+        navigationViewModel,
+      )
+        as T
     }
+  }
+}
+
+private inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
+  flow: Flow<T1>,
+  flow2: Flow<T2>,
+  flow3: Flow<T3>,
+  flow4: Flow<T4>,
+  flow5: Flow<T5>,
+  flow6: Flow<T6>,
+  flow7: Flow<T7>,
+  crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
+): Flow<R> {
+  return combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> ->
+    @Suppress("UNCHECKED_CAST")
+    transform(
+      args[0] as T1,
+      args[1] as T2,
+      args[2] as T3,
+      args[3] as T4,
+      args[4] as T5,
+      args[5] as T6,
+      args[6] as T7,
+    )
+  }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintViewModel.kt
new file mode 100644
index 0000000..1df0e34
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintViewModel.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.viewmodel
+
+/** Represents the fingerprint data nad the relevant state. */
+data class FingerprintStateViewModel(
+  val fingerprintViewModels: List<FingerprintViewModel>,
+  val canEnroll: Boolean,
+  val maxFingerprints: Int,
+  val hasSideFps: Boolean,
+  val pressToAuth: Boolean,
+)
+
+data class FingerprintViewModel(
+  val name: String,
+  val fingerId: Int,
+  val deviceId: Long,
+)
+
+sealed class FingerprintAuthAttemptViewModel {
+  data class Success(
+    val fingerId: Int,
+  ) : FingerprintAuthAttemptViewModel()
+
+  data class Error(
+    val error: Int,
+    val message: String,
+  ) : FingerprintAuthAttemptViewModel()
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt
index 1046f51..f9dbbff 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt
@@ -17,32 +17,29 @@
 package com.android.settings.biometrics.fingerprint2.ui.viewmodel
 
 /**
- * A class to represent a next step for FingerprintSettings. This is typically to perform an action
- * such that launches another activity such as EnrollFirstFingerprint() or
- * LaunchConfirmDeviceCredential().
+ * A class to represent a high level step for FingerprintSettings. This is typically to perform an
+ * action like launching an activity.
  */
 sealed class NextStepViewModel
 
 data class EnrollFirstFingerprint(
-    val userId: Int, val gateKeeperPasswordHandle: Long?,
-    val challenge: Long?,
-    val challengeToken: ByteArray?,
+  val userId: Int,
+  val gateKeeperPasswordHandle: Long?,
+  val challenge: Long?,
+  val challengeToken: ByteArray?,
 ) : NextStepViewModel()
 
 data class EnrollAdditionalFingerprint(
-    val userId: Int,
-    val challengeToken: ByteArray?,
+  val userId: Int,
+  val challengeToken: ByteArray?,
 ) : NextStepViewModel()
 
-data class FinishSettings(
-    val reason: String
-) : NextStepViewModel()
+data class FinishSettings(val reason: String) : NextStepViewModel()
 
-data class FinishSettingsWithResult(
-    val result: Int, val reason: String
-) : NextStepViewModel()
+data class FinishSettingsWithResult(val result: Int, val reason: String) : NextStepViewModel()
 
-data class ShowSettings(val userId: Int) : NextStepViewModel()
+object ShowSettings : NextStepViewModel()
+
+object LaunchedActivity : NextStepViewModel()
 
 data class LaunchConfirmDeviceCredential(val userId: Int) : NextStepViewModel()
-
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/PreferenceViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/PreferenceViewModel.kt
new file mode 100644
index 0000000..05764a2
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/PreferenceViewModel.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.viewmodel
+
+/** Classed use to represent a Dialogs state. */
+sealed class PreferenceViewModel {
+  data class RenameDialog(
+    val fingerprintViewModel: FingerprintViewModel,
+  ) : PreferenceViewModel()
+
+  data class DeleteDialog(
+    val fingerprintViewModel: FingerprintViewModel,
+  ) : PreferenceViewModel()
+}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 3a3ca99..1587e00 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -21,6 +21,7 @@
     ],
 
     static_libs: [
+	"androidx.arch.core_core-testing",
         "androidx.test.core",
         "androidx.test.rules",
         "androidx.test.espresso.core",
diff --git a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FakeFingerprintManagerInteractor.kt b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FakeFingerprintManagerInteractor.kt
new file mode 100644
index 0000000..0509d8a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FakeFingerprintManagerInteractor.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fingerprint2.domain.interactor
+
+import android.hardware.biometrics.SensorProperties
+import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+/** Fake to be used by other classes to easily fake the FingerprintManager implementation. */
+class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
+
+  var enrollableFingerprints: Int = 5
+  var enrolledFingerprintsInternal: MutableList<FingerprintViewModel> = mutableListOf()
+  var challengeToGenerate: Pair<Long, ByteArray> = Pair(-1L, byteArrayOf())
+  var authenticateAttempt = FingerprintAuthAttemptViewModel.Success(1)
+  var pressToAuthEnabled = true
+
+  var sensorProps =
+    listOf(
+      FingerprintSensorPropertiesInternal(
+        0 /* sensorId */,
+        SensorProperties.STRENGTH_STRONG,
+        5 /* maxEnrollmentsPerUser */,
+        emptyList() /* ComponentInfoInternal */,
+        TYPE_POWER_BUTTON,
+        true /* resetLockoutRequiresHardwareAuthToken */
+      )
+    )
+
+  override suspend fun authenticate(): FingerprintAuthAttemptViewModel {
+    return authenticateAttempt
+  }
+
+  override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> {
+    return challengeToGenerate
+  }
+  override val enrolledFingerprints: Flow<List<FingerprintViewModel>> = flow {
+    emit(enrolledFingerprintsInternal)
+  }
+
+  override fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean> = flow {
+    emit(numFingerprints < enrollableFingerprints)
+  }
+
+  override val maxEnrollableFingerprints: Flow<Int> = flow { emit(enrollableFingerprints) }
+
+  override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean {
+    return enrolledFingerprintsInternal.remove(fp)
+  }
+
+  override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {}
+
+  override suspend fun hasSideFps(): Boolean {
+    return sensorProps.any { it.isAnySidefpsType }
+  }
+
+  override suspend fun pressToAuthEnabled(): Boolean {
+    return pressToAuthEnabled
+  }
+
+  override suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal> =
+    sensorProps
+}
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
new file mode 100644
index 0000000..7af740a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fingerprint2.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.hardware.fingerprint.Fingerprint
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintManager.CryptoObject
+import android.hardware.fingerprint.FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT
+import android.os.CancellationSignal
+import android.os.Handler
+import androidx.test.core.app.ApplicationProvider
+import com.android.settings.biometrics.GatekeeperPasswordProvider
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.password.ChooseLockSettingsHelper
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.flow.last
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.nullable
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoJUnitRunner
+
+@RunWith(MockitoJUnitRunner::class)
+class FingerprintManagerInteractorTest {
+
+  @JvmField @Rule var rule = MockitoJUnit.rule()
+  private lateinit var underTest: FingerprintManagerInteractor
+  private var context: Context = ApplicationProvider.getApplicationContext()
+  private var backgroundDispatcher = StandardTestDispatcher()
+  @Mock private lateinit var fingerprintManager: FingerprintManager
+  @Mock private lateinit var gateKeeperPasswordProvider: GatekeeperPasswordProvider
+
+  private var testScope = TestScope(backgroundDispatcher)
+  private var pressToAuthProvider = { true }
+
+  @Before
+  fun setup() {
+    underTest =
+      FingerprintManagerInteractorImpl(
+        context,
+        backgroundDispatcher,
+        fingerprintManager,
+        gateKeeperPasswordProvider,
+        pressToAuthProvider,
+      )
+  }
+
+  @Test
+  fun testEmptyFingerprints() =
+    testScope.runTest {
+      Mockito.`when`(fingerprintManager.getEnrolledFingerprints(Mockito.anyInt()))
+        .thenReturn(emptyList())
+
+      val emptyFingerprintList: List<Fingerprint> = emptyList()
+      assertThat(underTest.enrolledFingerprints.last()).isEqualTo(emptyFingerprintList)
+    }
+
+  @Test
+  fun testOneFingerprint() =
+    testScope.runTest {
+      val expected = Fingerprint("Finger 1,", 2, 3L)
+      val fingerprintList: List<Fingerprint> = listOf(expected)
+      Mockito.`when`(fingerprintManager.getEnrolledFingerprints(Mockito.anyInt()))
+        .thenReturn(fingerprintList)
+
+      val list = underTest.enrolledFingerprints.last()
+      assertThat(list.size).isEqualTo(fingerprintList.size)
+      val actual = list[0]
+      assertThat(actual.name).isEqualTo(expected.name)
+      assertThat(actual.fingerId).isEqualTo(expected.biometricId)
+      assertThat(actual.deviceId).isEqualTo(expected.deviceId)
+    }
+
+  @Test
+  fun testCanEnrollFingerprint() =
+    testScope.runTest {
+      val mockContext = Mockito.mock(Context::class.java)
+      val resources = Mockito.mock(Resources::class.java)
+      Mockito.`when`(mockContext.resources).thenReturn(resources)
+      Mockito.`when`(resources.getInteger(anyInt())).thenReturn(3)
+      underTest =
+        FingerprintManagerInteractorImpl(
+          mockContext,
+          backgroundDispatcher,
+          fingerprintManager,
+          gateKeeperPasswordProvider,
+          pressToAuthProvider,
+        )
+
+      assertThat(underTest.canEnrollFingerprints(2).last()).isTrue()
+      assertThat(underTest.canEnrollFingerprints(3).last()).isFalse()
+    }
+
+  @Test
+  fun testGenerateChallenge() =
+    testScope.runTest {
+      val byteArray = byteArrayOf(5, 3, 2)
+      val challenge = 100L
+      val intent = Intent()
+      intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, challenge)
+      Mockito.`when`(
+          gateKeeperPasswordProvider.requestGatekeeperHat(
+            any(Intent::class.java),
+            anyLong(),
+            anyInt()
+          )
+        )
+        .thenReturn(byteArray)
+
+      val generateChallengeCallback: ArgumentCaptor<FingerprintManager.GenerateChallengeCallback> =
+        ArgumentCaptor.forClass(FingerprintManager.GenerateChallengeCallback::class.java)
+
+      var result: Pair<Long, ByteArray?>? = null
+      val job = testScope.launch { result = underTest.generateChallenge(1L) }
+      runCurrent()
+
+      Mockito.verify(fingerprintManager)
+        .generateChallenge(anyInt(), capture(generateChallengeCallback))
+      generateChallengeCallback.value.onChallengeGenerated(1, 2, challenge)
+
+      runCurrent()
+      job.cancelAndJoin()
+
+      assertThat(result?.first).isEqualTo(challenge)
+      assertThat(result?.second).isEqualTo(byteArray)
+    }
+
+  @Test
+  fun testRemoveFingerprint_succeeds() =
+    testScope.runTest {
+      val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L)
+      val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L)
+
+      val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> =
+        ArgumentCaptor.forClass(FingerprintManager.RemovalCallback::class.java)
+
+      var result: Boolean? = null
+      val job =
+        testScope.launch { result = underTest.removeFingerprint(fingerprintViewModelToRemove) }
+      runCurrent()
+
+      Mockito.verify(fingerprintManager)
+        .remove(any(Fingerprint::class.java), anyInt(), capture(removalCallback))
+      removalCallback.value.onRemovalSucceeded(fingerprintToRemove, 1)
+
+      runCurrent()
+      job.cancelAndJoin()
+
+      assertThat(result).isTrue()
+    }
+
+  @Test
+  fun testRemoveFingerprint_fails() =
+    testScope.runTest {
+      val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L)
+      val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L)
+
+      val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> =
+        ArgumentCaptor.forClass(FingerprintManager.RemovalCallback::class.java)
+
+      var result: Boolean? = null
+      val job =
+        testScope.launch { result = underTest.removeFingerprint(fingerprintViewModelToRemove) }
+      runCurrent()
+
+      Mockito.verify(fingerprintManager)
+        .remove(any(Fingerprint::class.java), anyInt(), capture(removalCallback))
+      removalCallback.value.onRemovalError(
+        fingerprintToRemove,
+        100,
+        "Oh no, we couldn't find that one"
+      )
+
+      runCurrent()
+      job.cancelAndJoin()
+
+      assertThat(result).isFalse()
+    }
+
+  @Test
+  fun testRenameFingerprint_succeeds() =
+    testScope.runTest {
+      val fingerprintToRename = FingerprintViewModel("Finger 2", 1, 2L)
+
+      underTest.renameFingerprint(fingerprintToRename, "Woo")
+
+      Mockito.verify(fingerprintManager)
+        .rename(eq(fingerprintToRename.fingerId), anyInt(), safeEq("Woo"))
+    }
+
+  @Test
+  fun testAuth_succeeds() =
+    testScope.runTest {
+      val fingerprint = Fingerprint("Woooo", 100, 101L)
+
+      var result: FingerprintAuthAttemptViewModel? = null
+      val job = launch { result = underTest.authenticate() }
+
+      val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> =
+        ArgumentCaptor.forClass(FingerprintManager.AuthenticationCallback::class.java)
+
+      runCurrent()
+
+      Mockito.verify(fingerprintManager)
+        .authenticate(
+          nullable(CryptoObject::class.java),
+          any(CancellationSignal::class.java),
+          capture(authCallback),
+          nullable(Handler::class.java),
+          anyInt()
+        )
+      authCallback.value.onAuthenticationSucceeded(
+        FingerprintManager.AuthenticationResult(null, fingerprint, 1, false)
+      )
+
+      runCurrent()
+      job.cancelAndJoin()
+      assertThat(result).isEqualTo(FingerprintAuthAttemptViewModel.Success(fingerprint.biometricId))
+    }
+
+  @Test
+  fun testAuth_lockout() =
+    testScope.runTest {
+      var result: FingerprintAuthAttemptViewModel? = null
+      val job = launch { result = underTest.authenticate() }
+
+      val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> =
+        ArgumentCaptor.forClass(FingerprintManager.AuthenticationCallback::class.java)
+
+      runCurrent()
+
+      Mockito.verify(fingerprintManager)
+        .authenticate(
+          nullable(CryptoObject::class.java),
+          any(CancellationSignal::class.java),
+          capture(authCallback),
+          nullable(Handler::class.java),
+          anyInt()
+        )
+      authCallback.value.onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!")
+
+      runCurrent()
+      job.cancelAndJoin()
+      assertThat(result)
+        .isEqualTo(
+          FingerprintAuthAttemptViewModel.Error(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!")
+        )
+    }
+
+  private fun <T : Any> safeEq(value: T): T = eq(value) ?: value
+  private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+  private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+}
diff --git a/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsNavigationViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsNavigationViewModelTest.kt
new file mode 100644
index 0000000..4e1f6b1
--- /dev/null
+++ b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsNavigationViewModelTest.kt
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fingerprint2.viewmodel
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.NextStepViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
+import com.android.settings.fingerprint2.domain.interactor.FakeFingerprintManagerInteractor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoJUnitRunner
+
+@RunWith(MockitoJUnitRunner::class)
+class FingerprintSettingsNavigationViewModelTest {
+
+  @JvmField @Rule var rule = MockitoJUnit.rule()
+
+  @get:Rule val instantTaskRule = InstantTaskExecutorRule()
+
+  private lateinit var underTest: FingerprintSettingsNavigationViewModel
+  private val defaultUserId = 0
+  private var backgroundDispatcher = StandardTestDispatcher()
+  private var testScope = TestScope(backgroundDispatcher)
+  private lateinit var fakeFingerprintManagerInteractor: FakeFingerprintManagerInteractor
+
+  @Before
+  fun setup() {
+    fakeFingerprintManagerInteractor = FakeFingerprintManagerInteractor()
+    backgroundDispatcher = StandardTestDispatcher()
+    testScope = TestScope(backgroundDispatcher)
+    Dispatchers.setMain(backgroundDispatcher)
+
+    underTest =
+      FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
+          defaultUserId,
+          fakeFingerprintManagerInteractor,
+          backgroundDispatcher,
+          null,
+          null,
+        )
+        .create(FingerprintSettingsNavigationViewModel::class.java)
+  }
+
+  @After
+  fun tearDown() {
+    Dispatchers.resetMain()
+  }
+
+  @Test
+  fun testNoGateKeeper_launchesConfirmDeviceCredential() =
+    testScope.runTest {
+      var nextStep: NextStepViewModel? = null
+      val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+      runCurrent()
+      assertThat(nextStep).isEqualTo(LaunchConfirmDeviceCredential(defaultUserId))
+      job.cancel()
+    }
+
+  @Test
+  fun testConfirmDevice_fails() =
+    testScope.runTest {
+      var nextStep: NextStepViewModel? = null
+      val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+      underTest.onConfirmDevice(false, null)
+      runCurrent()
+
+      assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
+      job.cancel()
+    }
+
+  @Test
+  fun confirmDeviceSuccess_noGateKeeper() =
+    testScope.runTest {
+      var nextStep: NextStepViewModel? = null
+      val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+      underTest.onConfirmDevice(true, null)
+      runCurrent()
+
+      assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
+      job.cancel()
+    }
+
+  @Test
+  fun confirmDeviceSuccess_launchesEnrollment_ifNoPreviousEnrollments() =
+    testScope.runTest {
+      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
+
+      var nextStep: NextStepViewModel? = null
+      val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+      underTest.onConfirmDevice(true, 10L)
+      runCurrent()
+
+      assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, 10L, null, null))
+      job.cancel()
+    }
+
+  @Test
+  fun firstEnrollment_fails() =
+    testScope.runTest {
+      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
+
+      var nextStep: NextStepViewModel? = null
+      val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+      underTest.onConfirmDevice(true, 10L)
+      underTest.onEnrollFirstFailure("We failed!!")
+      runCurrent()
+
+      assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
+      job.cancel()
+    }
+
+  @Test
+  fun firstEnrollment_failsWithReason() =
+    testScope.runTest {
+      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
+
+      var nextStep: NextStepViewModel? = null
+      val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+      val failStr = "We failed!!"
+      val failReason = 101
+
+      underTest.onConfirmDevice(true, 10L)
+      underTest.onEnrollFirstFailure(failStr, failReason)
+      runCurrent()
+
+      assertThat(nextStep).isEqualTo(FinishSettingsWithResult(failReason, failStr))
+      job.cancel()
+    }
+
+  @Test
+  fun firstEnrollmentSucceeds_noToken() =
+    testScope.runTest {
+      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
+
+      var nextStep: NextStepViewModel? = null
+      val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+      underTest.onConfirmDevice(true, 10L)
+      underTest.onEnrollFirst(null, null)
+      runCurrent()
+
+      assertThat(nextStep).isEqualTo(FinishSettings("Error, empty token"))
+      job.cancel()
+    }
+
+  @Test
+  fun firstEnrollmentSucceeds_noKeyChallenge() =
+    testScope.runTest {
+      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
+
+      var nextStep: NextStepViewModel? = null
+      val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+      val byteArray = ByteArray(1) { 3 }
+
+      underTest.onConfirmDevice(true, 10L)
+      underTest.onEnrollFirst(byteArray, null)
+      runCurrent()
+
+      assertThat(nextStep).isEqualTo(FinishSettings("Error, empty keyChallenge"))
+      job.cancel()
+    }
+
+  @Test
+  fun firstEnrollment_succeeds() =
+    testScope.runTest {
+      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
+
+      var nextStep: NextStepViewModel? = null
+      val job = testScope.launch { underTest.nextStep.collect { nextStep = it } }
+
+      val byteArray = ByteArray(1) { 3 }
+      val keyChallenge = 89L
+
+      underTest.onConfirmDevice(true, 10L)
+      underTest.onEnrollFirst(byteArray, keyChallenge)
+      runCurrent()
+
+      assertThat(nextStep).isEqualTo(ShowSettings)
+      job.cancel()
+    }
+
+  @Test
+  fun enrollAdditionalFingerprints_fails() =
+    testScope.runTest {
+      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
+        mutableListOf(FingerprintViewModel("a", 1, 3L))
+      fakeFingerprintManagerInteractor.challengeToGenerate = Pair(4L, byteArrayOf(3, 3, 1))
+
+      var nextStep: NextStepViewModel? = null
+      val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+      underTest.onConfirmDevice(true, 10L)
+      runCurrent()
+      underTest.onEnrollAdditionalFailure()
+      runCurrent()
+
+      assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
+      job.cancel()
+    }
+
+  @Test
+  fun enrollAdditional_success() =
+    testScope.runTest {
+      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
+        mutableListOf(FingerprintViewModel("a", 1, 3L))
+
+      var nextStep: NextStepViewModel? = null
+      val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+      underTest.onConfirmDevice(true, 10L)
+      underTest.onEnrollSuccess()
+
+      runCurrent()
+
+      assertThat(nextStep).isEqualTo(ShowSettings)
+      job.cancel()
+    }
+
+  @Test
+  fun confirmDeviceCredential_withEnrolledFingerprint_showsSettings() =
+    testScope.runTest {
+      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
+        mutableListOf(FingerprintViewModel("a", 1, 3L))
+      fakeFingerprintManagerInteractor.challengeToGenerate = Pair(10L, byteArrayOf(1, 2, 3))
+
+      var nextStep: NextStepViewModel? = null
+      val job = launch { underTest.nextStep.collect { nextStep = it } }
+
+      underTest.onConfirmDevice(true, 10L)
+      runCurrent()
+
+      assertThat(nextStep).isEqualTo(ShowSettings)
+      job.cancel()
+    }
+}
diff --git a/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsViewModelTest.kt
index 7389543..d430827 100644
--- a/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsViewModelTest.kt
+++ b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsViewModelTest.kt
@@ -16,317 +16,232 @@
 
 package com.android.settings.fingerprint2.viewmodel
 
-import android.hardware.fingerprint.Fingerprint
-import android.hardware.fingerprint.FingerprintManager
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.NextStepViewModel
-import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
+import android.hardware.biometrics.SensorProperties
+import android.hardware.fingerprint.FingerprintSensorProperties
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
 import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.PreferenceViewModel
+import com.android.settings.fingerprint2.domain.interactor.FakeFingerprintManagerInteractor
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.take
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.resetMain
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.anyInt
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoJUnitRunner
-import org.mockito.Mockito.`when` as whenever
 
 @RunWith(MockitoJUnitRunner::class)
 class FingerprintSettingsViewModelTest {
 
-    @JvmField
-    @Rule
-    var rule = MockitoJUnit.rule()
+  @JvmField @Rule var rule = MockitoJUnit.rule()
 
-    @Mock
-    private lateinit var fingerprintManager: FingerprintManager
-    private lateinit var underTest: FingerprintSettingsViewModel
-    private val defaultUserId = 0
+  @get:Rule val instantTaskRule = InstantTaskExecutorRule()
 
-    @Before
-    fun setup() {
-        // @formatter:off
-        underTest = FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+  private lateinit var underTest: FingerprintSettingsViewModel
+  private lateinit var navigationViewModel: FingerprintSettingsNavigationViewModel
+  private val defaultUserId = 0
+  private var backgroundDispatcher = StandardTestDispatcher()
+  private var testScope = TestScope(backgroundDispatcher)
+  private lateinit var fakeFingerprintManagerInteractor: FakeFingerprintManagerInteractor
+
+  @Before
+  fun setup() {
+    fakeFingerprintManagerInteractor = FakeFingerprintManagerInteractor()
+    backgroundDispatcher = StandardTestDispatcher()
+    testScope = TestScope(backgroundDispatcher)
+    Dispatchers.setMain(backgroundDispatcher)
+
+    navigationViewModel =
+      FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
+          defaultUserId,
+          fakeFingerprintManagerInteractor,
+          backgroundDispatcher,
+          null,
+          null,
+        )
+        .create(FingerprintSettingsNavigationViewModel::class.java)
+
+    underTest =
+      FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+          defaultUserId,
+          fakeFingerprintManagerInteractor,
+          backgroundDispatcher,
+          navigationViewModel,
+        )
+        .create(FingerprintSettingsViewModel::class.java)
+  }
+
+  @After
+  fun tearDown() {
+    Dispatchers.resetMain()
+  }
+
+  @Test
+  fun authenticate_DoesNotRun_ifOptical() =
+    testScope.runTest {
+      fakeFingerprintManagerInteractor.sensorProps =
+        listOf(
+          FingerprintSensorPropertiesInternal(
+            0 /* sensorId */,
+            SensorProperties.STRENGTH_STRONG,
+            5 /* maxEnrollmentsPerUser */,
+            emptyList() /* ComponentInfoInternal */,
+            FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
+            true /* resetLockoutRequiresHardwareAuthToken */
+          )
+        )
+      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
+        mutableListOf(FingerprintViewModel("a", 1, 3L))
+
+      underTest =
+        FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
             defaultUserId,
-            fingerprintManager,
-        ).create(FingerprintSettingsViewModel::class.java)
-        // @formatter:on
+            fakeFingerprintManagerInteractor,
+            backgroundDispatcher,
+            navigationViewModel,
+          )
+          .create(FingerprintSettingsViewModel::class.java)
+
+      var authAttempt: FingerprintAuthAttemptViewModel? = null
+      val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
+
+      underTest.shouldAuthenticate(true)
+      // Ensure we are showing settings
+      navigationViewModel.onConfirmDevice(true, 10L)
+
+      runCurrent()
+      advanceTimeBy(400)
+
+      assertThat(authAttempt).isNull()
+      job.cancel()
     }
 
-    @Test
-    fun testNoGateKeeper_launchesConfirmDeviceCredential() = runTest {
-        var nextStep: NextStepViewModel? = null
-        val job = launch {
-            underTest.nextStep.collect {
-                nextStep = it
-            }
-        }
-
-        underTest.updateTokenAndChallenge(null, null)
-
-        runCurrent()
-        assertThat(nextStep).isEqualTo(LaunchConfirmDeviceCredential(defaultUserId))
-        job.cancel()
-    }
-
-    @Test
-    fun testConfirmDevice_fails() = runTest {
-        var nextStep: NextStepViewModel? = null
-        val job = launch {
-            underTest.nextStep.collect {
-                nextStep = it
-            }
-        }
-
-        underTest.updateTokenAndChallenge(null, null)
-        underTest.onConfirmDevice(false, null)
-
-        runCurrent()
-
-        assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
-        job.cancel()
-    }
-
-    @Test
-    fun confirmDeviceSuccess_noGateKeeper() = runTest {
-        var nextStep: NextStepViewModel? = null
-        val job = launch {
-            underTest.nextStep.collect {
-                nextStep = it
-            }
-        }
-
-        underTest.updateTokenAndChallenge(null, null)
-        underTest.onConfirmDevice(true, null)
-
-        runCurrent()
-
-        assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
-        job.cancel()
-    }
-
-    @Test
-    fun confirmDeviceSuccess_launchesEnrollment_ifNoPreviousEnrollments() = runTest {
-        whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
-
-        var nextStep: NextStepViewModel? = null
-        val job = launch {
-            underTest.nextStep.collect {
-                nextStep = it
-            }
-        }
-
-        underTest.updateTokenAndChallenge(null, null)
-        underTest.onConfirmDevice(true, 10L)
-
-        runCurrent()
-
-        assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, 10L, null, null))
-        job.cancel()
-    }
-
-    @Test
-    fun firstEnrollment_fails() = runTest {
-        whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
-
-        var nextStep: NextStepViewModel? = null
-        val job = launch {
-            underTest.nextStep.collect {
-                nextStep = it
-            }
-        }
-
-        underTest.updateTokenAndChallenge(null, null)
-        underTest.onConfirmDevice(true, 10L)
-        underTest.onEnrollFirstFailure("We failed!!")
-
-        runCurrent()
-
-        assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
-        job.cancel()
-    }
-
-    @Test
-    fun firstEnrollment_failsWithReason() = runTest {
-        whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
-
-        var nextStep: NextStepViewModel? = null
-        val job = launch {
-            underTest.nextStep.collect {
-                nextStep = it
-            }
-        }
-
-        val failStr = "We failed!!"
-        val failReason = 101
-
-        underTest.updateTokenAndChallenge(null, null)
-        underTest.onConfirmDevice(true, 10L)
-        underTest.onEnrollFirstFailure(failStr, failReason)
-
-        runCurrent()
-
-        assertThat(nextStep).isEqualTo(FinishSettingsWithResult(failReason, failStr))
-        job.cancel()
-    }
-
-    @Test
-    fun firstEnrollmentSucceeds_noToken() = runTest {
-        whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
-
-        var nextStep: NextStepViewModel? = null
-        val job = launch {
-            underTest.nextStep.collect {
-                nextStep = it
-            }
-        }
-
-        underTest.updateTokenAndChallenge(null, null)
-        underTest.onConfirmDevice(true, 10L)
-        underTest.onEnrollFirst(null, null)
-
-        runCurrent()
-
-        assertThat(nextStep).isEqualTo(FinishSettings("Error, empty token"))
-        job.cancel()
-    }
-
-    @Test
-    fun firstEnrollmentSucceeds_noKeyChallenge() = runTest {
-        whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
-
-        var nextStep: NextStepViewModel? = null
-        val job = launch {
-            underTest.nextStep.collect {
-                nextStep = it
-            }
-        }
-
-        val byteArray = ByteArray(1) {
-            3
-        }
-
-        underTest.updateTokenAndChallenge(null, null)
-        underTest.onConfirmDevice(true, 10L)
-        underTest.onEnrollFirst(byteArray, null)
-
-        runCurrent()
-
-        assertThat(nextStep).isEqualTo(FinishSettings("Error, empty keyChallenge"))
-        job.cancel()
-    }
-
-    @Test
-    fun firstEnrollment_succeeds() = runTest {
-        whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
-
-        var nextStep: NextStepViewModel? = null
-        val job = launch {
-            underTest.nextStep.collect {
-                nextStep = it
-            }
-        }
-
-        val byteArray = ByteArray(1) {
-            3
-        }
-        val keyChallenge = 89L
-
-        underTest.updateTokenAndChallenge(null, null)
-        underTest.onConfirmDevice(true, 10L)
-        underTest.onEnrollFirst(byteArray, keyChallenge)
-
-        runCurrent()
-
-        assertThat(nextStep).isEqualTo(ShowSettings(defaultUserId))
-        job.cancel()
-    }
-
-    @Test
-    fun confirmDeviceCredential_withEnrolledFingerprint_showsSettings() = runTest {
-        whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(
-            listOf(
-                Fingerprint(
-                    "a", 1, 2, 3L
-                )
-            )
+  @Test
+  fun authenticate_DoesNotRun_ifUltrasonic() =
+    testScope.runTest {
+      fakeFingerprintManagerInteractor.sensorProps =
+        listOf(
+          FingerprintSensorPropertiesInternal(
+            0 /* sensorId */,
+            SensorProperties.STRENGTH_STRONG,
+            5 /* maxEnrollmentsPerUser */,
+            emptyList() /* ComponentInfoInternal */,
+            FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC,
+            true /* resetLockoutRequiresHardwareAuthToken */
+          )
         )
+      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
+        mutableListOf(FingerprintViewModel("a", 1, 3L))
 
-        var nextStep: NextStepViewModel? = null
-        val job = launch {
-            underTest.nextStep.collect {
-                nextStep = it
-            }
-        }
+      underTest =
+        FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+            defaultUserId,
+            fakeFingerprintManagerInteractor,
+            backgroundDispatcher,
+            navigationViewModel,
+          )
+          .create(FingerprintSettingsViewModel::class.java)
 
-        underTest.updateTokenAndChallenge(null, null)
-        underTest.onConfirmDevice(true, 10L)
+      var authAttempt: FingerprintAuthAttemptViewModel? = null
+      val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
 
-        runCurrent()
+      underTest.shouldAuthenticate(true)
+      navigationViewModel.onConfirmDevice(true, 10L)
+      advanceTimeBy(400)
+      runCurrent()
 
-        assertThat(nextStep).isEqualTo(ShowSettings(defaultUserId))
-        job.cancel()
+      assertThat(authAttempt).isNull()
+      job.cancel()
     }
 
-    @Test
-    fun enrollAdditionalFingerprints_fails() = runTest {
-        whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(
-            listOf(
-                Fingerprint(
-                    "a", 1, 2, 3L
-                )
-            )
+  @Test
+  fun authenticate_DoesRun_ifNotUdfps() =
+    testScope.runTest {
+      fakeFingerprintManagerInteractor.sensorProps =
+        listOf(
+          FingerprintSensorPropertiesInternal(
+            0 /* sensorId */,
+            SensorProperties.STRENGTH_STRONG,
+            5 /* maxEnrollmentsPerUser */,
+            emptyList() /* ComponentInfoInternal */,
+            FingerprintSensorProperties.TYPE_POWER_BUTTON,
+            true /* resetLockoutRequiresHardwareAuthToken */
+          )
         )
+      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
+        mutableListOf(FingerprintViewModel("a", 1, 3L))
+      val success = FingerprintAuthAttemptViewModel.Success(1)
+      fakeFingerprintManagerInteractor.authenticateAttempt = success
 
-        var nextStep: NextStepViewModel? = null
-        val job = launch {
-            underTest.nextStep.collect {
-                nextStep = it
-            }
-        }
+      underTest =
+        FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+            defaultUserId,
+            fakeFingerprintManagerInteractor,
+            backgroundDispatcher,
+            navigationViewModel,
+          )
+          .create(FingerprintSettingsViewModel::class.java)
 
-        underTest.updateTokenAndChallenge(null, null)
-        underTest.onConfirmDevice(true, 10L)
-        underTest.onEnrollAdditionalFailure()
+      var authAttempt: FingerprintAuthAttemptViewModel? = null
 
-        runCurrent()
+      val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
+      underTest.shouldAuthenticate(true)
+      navigationViewModel.onConfirmDevice(true, 10L)
+      advanceTimeBy(400)
+      runCurrent()
 
-        assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
-        job.cancel()
+      assertThat(authAttempt).isEqualTo(success)
+      job.cancel()
     }
 
-    @Test
-    fun enrollAdditional_success() = runTest {
-        whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(
-            listOf(
-                Fingerprint(
-                    "a", 1, 2, 3L
-                )
-            )
+  @Test
+  fun deleteDialog_showAndDismiss() = runTest {
+    val fingerprintToDelete = FingerprintViewModel("A", 1, 10L)
+    fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToDelete)
+
+    underTest =
+      FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+          defaultUserId,
+          fakeFingerprintManagerInteractor,
+          backgroundDispatcher,
+          navigationViewModel,
         )
+        .create(FingerprintSettingsViewModel::class.java)
 
-        var nextStep: NextStepViewModel? = null
-        val job = launch {
-            underTest.nextStep.collect {
-                nextStep = it
-            }
-        }
+    var dialog: PreferenceViewModel? = null
+    val dialogJob = launch { underTest.isShowingDialog.collect { dialog = it } }
 
-        underTest.updateTokenAndChallenge(null, null)
-        underTest.onConfirmDevice(true, 10L)
-        underTest.onEnrollSuccess()
+    // Move to the ShowSettings state
+    navigationViewModel.onConfirmDevice(true, 10L)
+    runCurrent()
+    underTest.onDeleteClicked(fingerprintToDelete)
+    runCurrent()
 
-        runCurrent()
+    assertThat(dialog is PreferenceViewModel.DeleteDialog)
+    assertThat(dialog).isEqualTo(PreferenceViewModel.DeleteDialog(fingerprintToDelete))
 
-        assertThat(nextStep).isEqualTo(ShowSettings(defaultUserId))
-        job.cancel()
-    }
-}
\ No newline at end of file
+    underTest.deleteFingerprint(fingerprintToDelete)
+    underTest.onDeleteDialogFinished()
+    runCurrent()
+
+    assertThat(dialog).isNull()
+
+    dialogJob.cancel()
+  }
+}