Merge "Adding more tests for FingerprintSettingsV2" into main

This commit is contained in:
Joshua Mccloskey
2023-08-15 17:30:37 +00:00
committed by Android (Google) Code Review
5 changed files with 301 additions and 25 deletions

View File

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

View File

@@ -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<FingerprintStateViewModel?> =
MutableStateFlow(null)
val fingerprintState: Flow<FingerprintStateViewModel?> =
@@ -103,7 +84,6 @@ class FingerprintSettingsViewModel(
MutableSharedFlow()
private val attemptsSoFar: MutableStateFlow<Int> = 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 }

View File

@@ -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 }

View File

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

View File

@@ -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<FingerprintViewModel> {
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
}
}