From 76237177881a8740371fe41de9c68e4ab04b6226 Mon Sep 17 00:00:00 2001 From: Joshua McCloskey Date: Wed, 19 Jul 2023 21:37:10 +0000 Subject: [PATCH] Adding more tests for FingerprintSettingsV2 Test: atest FingerprintSettingsViewModelTest FingerprintSettingsNavigationModelTest Bug: 280862076 Change-Id: Ibb3d0112f394d6776fc1b346d226d9f7720cfed8 --- .../FingerprintSettingsNavigationViewModel.kt | 9 +- .../viewmodel/FingerprintSettingsViewModel.kt | 48 ++--- .../FakeFingerprintManagerInteractor.kt | 6 +- ...gerprintSettingsNavigationViewModelTest.kt | 94 ++++++++++ .../FingerprintSettingsViewModelTest.kt | 169 +++++++++++++++++- 5 files changed, 301 insertions(+), 25 deletions(-) diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt index a3a5d3c9746..a638806474b 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.last import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -49,7 +50,13 @@ class FingerprintSettingsNavigationViewModel( if (challengeInit == null || tokenInit == null) { _nextStep.update { LaunchConfirmDeviceCredential(userId) } } else { - viewModelScope.launch { showSettingsHelper() } + viewModelScope.launch { + if (fingerprintManagerInteractor.enrolledFingerprints.last().isEmpty()) { + _nextStep.update { EnrollFirstFingerprint(userId, null, challenge, token) } + } else { + showSettingsHelper() + } + } } } 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 554f336a7ff..0bae07533a1 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt @@ -17,8 +17,7 @@ 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.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.util.Log import androidx.lifecycle.ViewModel @@ -67,24 +66,6 @@ class FingerprintSettingsViewModel( } } - 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 = MutableStateFlow(null) val fingerprintState: Flow = @@ -103,7 +84,6 @@ class FingerprintSettingsViewModel( MutableSharedFlow() private val attemptsSoFar: MutableStateFlow = MutableStateFlow(0) - /** * 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 @@ -139,7 +119,13 @@ class FingerprintSettingsViewModel( return@combine false } val sensorType = sensorProps[0].sensorType - if (listOf(TYPE_UDFPS_OPTICAL, TYPE_UDFPS_ULTRASONIC).contains(sensorType)) { + if ( + listOf( + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, + FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC + ) + .contains(sensorType) + ) { return@combine false } @@ -182,6 +168,24 @@ class FingerprintSettingsViewModel( } .flowOn(backgroundDispatcher) + init { + viewModelScope.launch { + fingerprintSensorPropertiesInternal.update { + fingerprintManagerInteractor.sensorPropertiesInternal() + } + } + + viewModelScope.launch { + navigationViewModel.nextStep.filterNotNull().collect { + _isShowingDialog.update { null } + if (it is ShowSettings) { + // reset state + updateSettingsData() + } + } + } + } + /** The rename dialog has finished */ fun onRenameDialogFinished() { _isShowingDialog.update { null } 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 index 0509d8a91b0..1848c01c10b 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FakeFingerprintManagerInteractor.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FakeFingerprintManagerInteractor.kt @@ -67,7 +67,11 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor { return enrolledFingerprintsInternal.remove(fp) } - override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {} + override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) { + if (enrolledFingerprintsInternal.remove(fp)) { + enrolledFingerprintsInternal.add(FingerprintViewModel(newName, fp.fingerId, fp.deviceId)) + } + } override suspend fun hasSideFps(): Boolean { return sensorProps.any { it.isAnySidefpsType } diff --git a/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsNavigationViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsNavigationViewModelTest.kt index 4e1f6b191d4..9206afb44e6 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsNavigationViewModelTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsNavigationViewModelTest.kt @@ -17,6 +17,7 @@ package com.android.settings.fingerprint2.viewmodel import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.android.settings.biometrics.BiometricEnrollBase 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 @@ -272,4 +273,97 @@ class FingerprintSettingsNavigationViewModelTest { assertThat(nextStep).isEqualTo(ShowSettings) job.cancel() } + + @Test + fun enrollWithToken_andNoUsers_startsFingerprintEnrollment() = + testScope.runTest { + fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf() + + var nextStep: NextStepViewModel? = null + val job = launch { underTest.nextStep.collect { nextStep = it } } + + val token = byteArrayOf(1) + val challenge = 5L + + underTest = + FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory( + defaultUserId, + fakeFingerprintManagerInteractor, + backgroundDispatcher, + token, + challenge, + ) + .create(FingerprintSettingsNavigationViewModel::class.java) + + runCurrent() + + assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, null, challenge, token)) + job.cancel() + } + + @Test + fun enroll_shouldNotFinish() = + testScope.runTest { + fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf() + + var nextStep: NextStepViewModel? = null + val job = launch { underTest.nextStep.collect { nextStep = it } } + + val token = byteArrayOf(1) + val challenge = 5L + + underTest = + FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory( + defaultUserId, + fakeFingerprintManagerInteractor, + backgroundDispatcher, + token, + challenge, + ) + .create(FingerprintSettingsNavigationViewModel::class.java) + + runCurrent() + + assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, null, challenge, token)) + underTest.maybeFinishActivity(false) + + runCurrent() + assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, null, challenge, token)) + job.cancel() + } + + @Test + fun showSettings_shouldFinish() = + testScope.runTest { + fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = + mutableListOf(FingerprintViewModel("a", 1, 3L)) + + var nextStep: NextStepViewModel? = null + val job = launch { underTest.nextStep.collect { nextStep = it } } + + val token = byteArrayOf(1) + val challenge = 5L + + underTest = + FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory( + defaultUserId, + fakeFingerprintManagerInteractor, + backgroundDispatcher, + token, + challenge, + ) + .create(FingerprintSettingsNavigationViewModel::class.java) + + runCurrent() + assertThat(nextStep).isEqualTo(ShowSettings) + + underTest.maybeFinishActivity(false) + + runCurrent() + assertThat(nextStep) + .isEqualTo( + FinishSettingsWithResult(BiometricEnrollBase.RESULT_TIMEOUT, "onStop finishing settings") + ) + 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 d4308273458..8bd0b10560b 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsViewModelTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/viewmodel/FingerprintSettingsViewModelTest.kt @@ -213,7 +213,8 @@ class FingerprintSettingsViewModelTest { @Test fun deleteDialog_showAndDismiss() = runTest { val fingerprintToDelete = FingerprintViewModel("A", 1, 10L) - fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToDelete) + fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = + mutableListOf(fingerprintToDelete) underTest = FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( @@ -244,4 +245,170 @@ class FingerprintSettingsViewModelTest { dialogJob.cancel() } + + @Test + fun renameDialog_showAndDismiss() = runTest { + val fingerprintToRename = FingerprintViewModel("World", 1, 10L) + fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = + mutableListOf(fingerprintToRename) + + underTest = + FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( + defaultUserId, + fakeFingerprintManagerInteractor, + backgroundDispatcher, + navigationViewModel, + ) + .create(FingerprintSettingsViewModel::class.java) + + var dialog: PreferenceViewModel? = null + val dialogJob = launch { underTest.isShowingDialog.collect { dialog = it } } + + // Move to the ShowSettings state + navigationViewModel.onConfirmDevice(true, 10L) + runCurrent() + underTest.onPrefClicked(fingerprintToRename) + runCurrent() + + assertThat(dialog is PreferenceViewModel.DeleteDialog) + assertThat(dialog).isEqualTo(PreferenceViewModel.RenameDialog(fingerprintToRename)) + + underTest.renameFingerprint(fingerprintToRename, "Hello") + underTest.onRenameDialogFinished() + runCurrent() + + assertThat(dialog).isNull() + assertThat(fakeFingerprintManagerInteractor.enrolledFingerprintsInternal.first().name) + .isEqualTo("Hello") + + dialogJob.cancel() + } + + @Test + fun testTwoDialogsCannotShow_atSameTime() = runTest { + val fingerprintToDelete = FingerprintViewModel("A", 1, 10L) + fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = + mutableListOf(fingerprintToDelete) + + underTest = + FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( + defaultUserId, + fakeFingerprintManagerInteractor, + backgroundDispatcher, + navigationViewModel, + ) + .create(FingerprintSettingsViewModel::class.java) + + var dialog: PreferenceViewModel? = null + val dialogJob = launch { underTest.isShowingDialog.collect { dialog = it } } + + // Move to the ShowSettings state + navigationViewModel.onConfirmDevice(true, 10L) + runCurrent() + underTest.onDeleteClicked(fingerprintToDelete) + runCurrent() + + assertThat(dialog is PreferenceViewModel.DeleteDialog) + assertThat(dialog).isEqualTo(PreferenceViewModel.DeleteDialog(fingerprintToDelete)) + + underTest.onPrefClicked(fingerprintToDelete) + runCurrent() + assertThat(dialog is PreferenceViewModel.DeleteDialog) + assertThat(dialog).isEqualTo(PreferenceViewModel.DeleteDialog(fingerprintToDelete)) + + dialogJob.cancel() + } + + @Test + fun authenticatePauses_whenPaused() = + testScope.runTest { + val fingerprints = setupAuth() + val success = FingerprintAuthAttemptViewModel.Success(fingerprints.first().fingerId) + + var authAttempt: FingerprintAuthAttemptViewModel? = null + + val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } } + + underTest.shouldAuthenticate(true) + navigationViewModel.onConfirmDevice(true, 10L) + + advanceTimeBy(400) + runCurrent() + assertThat(authAttempt).isEqualTo(success) + + fakeFingerprintManagerInteractor.authenticateAttempt = + FingerprintAuthAttemptViewModel.Success(10) + underTest.shouldAuthenticate(false) + advanceTimeBy(400) + runCurrent() + + // The most recent auth attempt shouldn't have changed. + assertThat(authAttempt).isEqualTo(success) + job.cancel() + } + + @Test + fun dialog_pausesAuth() = + testScope.runTest { + val fingerprints = setupAuth() + + var authAttempt: FingerprintAuthAttemptViewModel? = null + val job = launch { underTest.authFlow.take(1).collectLatest { authAttempt = it } } + underTest.shouldAuthenticate(true) + navigationViewModel.onConfirmDevice(true, 10L) + + underTest.onPrefClicked(fingerprints[0]) + advanceTimeBy(400) + + job.cancel() + assertThat(authAttempt).isEqualTo(null) + } + + @Test + fun cannotAuth_when_notShowingSettings() = + testScope.runTest { + val fingerprints = setupAuth() + + var authAttempt: FingerprintAuthAttemptViewModel? = null + val job = launch { underTest.authFlow.take(1).collectLatest { authAttempt = it } } + underTest.shouldAuthenticate(true) + navigationViewModel.onConfirmDevice(true, 10L) + + // This should cause the state to change to FingerprintEnrolling + navigationViewModel.onAddFingerprintClicked() + advanceTimeBy(400) + + job.cancel() + assertThat(authAttempt).isEqualTo(null) + } + + private fun setupAuth(): MutableList { + fakeFingerprintManagerInteractor.sensorProps = + listOf( + FingerprintSensorPropertiesInternal( + 0 /* sensorId */, + SensorProperties.STRENGTH_STRONG, + 5 /* maxEnrollmentsPerUser */, + emptyList() /* ComponentInfoInternal */, + FingerprintSensorProperties.TYPE_POWER_BUTTON, + true /* resetLockoutRequiresHardwareAuthToken */ + ) + ) + val fingerprints = + mutableListOf(FingerprintViewModel("a", 1, 3L), FingerprintViewModel("b", 2, 5L)) + fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = fingerprints + val success = FingerprintAuthAttemptViewModel.Success(1) + fakeFingerprintManagerInteractor.authenticateAttempt = success + + underTest = + FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( + defaultUserId, + fakeFingerprintManagerInteractor, + backgroundDispatcher, + navigationViewModel, + ) + .create(FingerprintSettingsViewModel::class.java) + + return fingerprints + } }