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/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()
+  }
+}