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