Merge "Adding more tests for FingerprintSettingsV2" into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
12bf914caf
@@ -26,6 +26,7 @@ import kotlinx.coroutines.CoroutineDispatcher
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.last
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -49,7 +50,13 @@ class FingerprintSettingsNavigationViewModel(
|
|||||||
if (challengeInit == null || tokenInit == null) {
|
if (challengeInit == null || tokenInit == null) {
|
||||||
_nextStep.update { LaunchConfirmDeviceCredential(userId) }
|
_nextStep.update { LaunchConfirmDeviceCredential(userId) }
|
||||||
} else {
|
} else {
|
||||||
viewModelScope.launch { showSettingsHelper() }
|
viewModelScope.launch {
|
||||||
|
if (fingerprintManagerInteractor.enrolledFingerprints.last().isEmpty()) {
|
||||||
|
_nextStep.update { EnrollFirstFingerprint(userId, null, challenge, token) }
|
||||||
|
} else {
|
||||||
|
showSettingsHelper()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -17,8 +17,7 @@
|
|||||||
package com.android.settings.biometrics.fingerprint2.ui.viewmodel
|
package com.android.settings.biometrics.fingerprint2.ui.viewmodel
|
||||||
|
|
||||||
import android.hardware.fingerprint.FingerprintManager
|
import android.hardware.fingerprint.FingerprintManager
|
||||||
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
|
import android.hardware.fingerprint.FingerprintSensorProperties
|
||||||
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC
|
|
||||||
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
|
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
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?> =
|
private val _fingerprintStateViewModel: MutableStateFlow<FingerprintStateViewModel?> =
|
||||||
MutableStateFlow(null)
|
MutableStateFlow(null)
|
||||||
val fingerprintState: Flow<FingerprintStateViewModel?> =
|
val fingerprintState: Flow<FingerprintStateViewModel?> =
|
||||||
@@ -103,7 +84,6 @@ class FingerprintSettingsViewModel(
|
|||||||
MutableSharedFlow()
|
MutableSharedFlow()
|
||||||
|
|
||||||
private val attemptsSoFar: MutableStateFlow<Int> = MutableStateFlow(0)
|
private val attemptsSoFar: MutableStateFlow<Int> = MutableStateFlow(0)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a very tricky flow. The current fingerprint manager APIs are not robust, and a proper
|
* 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
|
* 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
|
return@combine false
|
||||||
}
|
}
|
||||||
val sensorType = sensorProps[0].sensorType
|
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
|
return@combine false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +168,24 @@ class FingerprintSettingsViewModel(
|
|||||||
}
|
}
|
||||||
.flowOn(backgroundDispatcher)
|
.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 */
|
/** The rename dialog has finished */
|
||||||
fun onRenameDialogFinished() {
|
fun onRenameDialogFinished() {
|
||||||
_isShowingDialog.update { null }
|
_isShowingDialog.update { null }
|
||||||
|
@@ -67,7 +67,11 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
|
|||||||
return enrolledFingerprintsInternal.remove(fp)
|
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 {
|
override suspend fun hasSideFps(): Boolean {
|
||||||
return sensorProps.any { it.isAnySidefpsType }
|
return sensorProps.any { it.isAnySidefpsType }
|
||||||
|
@@ -17,6 +17,7 @@
|
|||||||
package com.android.settings.fingerprint2.viewmodel
|
package com.android.settings.fingerprint2.viewmodel
|
||||||
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
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.EnrollFirstFingerprint
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
|
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.FingerprintViewModel
|
||||||
@@ -272,4 +273,97 @@ class FingerprintSettingsNavigationViewModelTest {
|
|||||||
assertThat(nextStep).isEqualTo(ShowSettings)
|
assertThat(nextStep).isEqualTo(ShowSettings)
|
||||||
job.cancel()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -213,7 +213,8 @@ class FingerprintSettingsViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun deleteDialog_showAndDismiss() = runTest {
|
fun deleteDialog_showAndDismiss() = runTest {
|
||||||
val fingerprintToDelete = FingerprintViewModel("A", 1, 10L)
|
val fingerprintToDelete = FingerprintViewModel("A", 1, 10L)
|
||||||
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToDelete)
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
|
||||||
|
mutableListOf(fingerprintToDelete)
|
||||||
|
|
||||||
underTest =
|
underTest =
|
||||||
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
|
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
|
||||||
@@ -244,4 +245,170 @@ class FingerprintSettingsViewModelTest {
|
|||||||
|
|
||||||
dialogJob.cancel()
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user