Remove FingerprintStateViewModel.

Since FingerprintStateViewModel is too general as a view model, this CL
removes it and adds more concrete flows in FingerprintSettingsViewModel
and FingerprintEnrollViewModel.

Test: atest FingerprintManagerInteractorTest
Test: atest FingerprintSettingsViewModelTest
Test: Verified enroll/deletion/renaming/authentication flows on Settings

Change-Id: I3a0662195c4989de0813b92bccda9d36a7f7e32a
This commit is contained in:
Hao Dong
2023-08-31 01:30:40 +00:00
parent 4fda9e8e42
commit 0978271198
11 changed files with 220 additions and 263 deletions

View File

@@ -47,6 +47,12 @@ interface FingerprintManagerInteractor {
/** Returns the max enrollable fingerprints, note during SUW this might be 1 */
val maxEnrollableFingerprints: Flow<Int>
/** Returns true if a user can enroll a fingerprint false otherwise. */
val canEnrollFingerprints: Flow<Boolean>
/** Retrieves the sensor properties of a device */
val sensorPropertiesInternal: Flow<FingerprintSensorPropertiesInternal?>
/** Runs [FingerprintManager.authenticate] */
suspend fun authenticate(): FingerprintAuthAttemptViewModel
@@ -60,9 +66,6 @@ interface FingerprintManagerInteractor {
*/
suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray>
/** Returns true if a user can enroll a fingerprint false otherwise. */
fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean>
/**
* Removes the given fingerprint, returning true if it was successfully removed and false
* otherwise
@@ -77,9 +80,6 @@ interface FingerprintManagerInteractor {
/** Indicates if the press to auth feature has been enabled */
suspend fun pressToAuthEnabled(): Boolean
/** Retrieves the sensor properties of a device */
suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal>
}
class FingerprintManagerInteractorImpl(
@@ -120,8 +120,15 @@ class FingerprintManagerInteractorImpl(
)
}
override fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean> = flow {
emit(numFingerprints < maxFingerprints)
override val canEnrollFingerprints: Flow<Boolean> = flow {
emit(
fingerprintManager.getEnrolledFingerprints(applicationContext.userId).size < maxFingerprints
)
}
override val sensorPropertiesInternal = flow {
val sensorPropertiesInternal = fingerprintManager.sensorPropertiesInternal
emit(if (sensorPropertiesInternal.isEmpty()) null else sensorPropertiesInternal.first())
}
override val maxEnrollableFingerprints = flow { emit(maxFingerprints) }
@@ -165,11 +172,6 @@ class FingerprintManagerInteractorImpl(
it.resume(pressToAuthProvider())
}
override suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal> =
suspendCancellableCoroutine {
it.resume(fingerprintManager.sensorPropertiesInternal)
}
override suspend fun authenticate(): FingerprintAuthAttemptViewModel =
suspendCancellableCoroutine { c: CancellableContinuation<FingerprintAuthAttemptViewModel> ->
val authenticationCallback =

View File

@@ -16,18 +16,6 @@
package com.android.settings.biometrics.fingerprint2.shared.model
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
/** Represents the fingerprint data nad the relevant state. */
data class FingerprintStateViewModel(
val fingerprintViewModels: List<FingerprintViewModel>,
val canEnroll: Boolean,
val maxFingerprints: Int,
val hasSideFps: Boolean,
val pressToAuth: Boolean,
val sensorProps: FingerprintSensorPropertiesInternal,
)
data class FingerprintViewModel(
val name: String,
val fingerId: Int,

View File

@@ -47,10 +47,10 @@ import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.Finge
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Confirmation
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Education
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Enrollment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollmentNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintGatekeeperViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Finish
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.GatekeeperInfo
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Intro
@@ -179,8 +179,10 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
)[FingerprintEnrollmentNavigationViewModel::class.java]
// Initialize FingerprintViewModel
ViewModelProvider(this, FingerprintViewModel.FingerprintViewModelFactory(interactor))[
FingerprintViewModel::class.java]
ViewModelProvider(
this,
FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory(interactor)
)[FingerprintEnrollViewModel::class.java]
// Initialize scroll view model
ViewModelProvider(this, FingerprintScrollViewModel.FingerprintScrollViewModelFactory())[

View File

@@ -34,10 +34,10 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.android.settings.R
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollmentNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintGatekeeperViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Unicorn
import com.google.android.setupcompat.template.FooterBarMixin
import com.google.android.setupcompat.template.FooterButton
@@ -76,7 +76,7 @@ class FingerprintEnrollmentIntroV2Fragment : Fragment(R.layout.fingerprint_v2_en
private lateinit var footerBarMixin: FooterBarMixin
private lateinit var textModel: TextModel
private lateinit var navigationViewModel: FingerprintEnrollmentNavigationViewModel
private lateinit var fingerprintStateViewModel: FingerprintViewModel
private lateinit var fingerprintEnrollViewModel: FingerprintEnrollViewModel
private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel
private lateinit var gateKeeperViewModel: FingerprintGatekeeperViewModel
@@ -84,8 +84,8 @@ class FingerprintEnrollmentIntroV2Fragment : Fragment(R.layout.fingerprint_v2_en
super.onCreate(savedInstanceState)
navigationViewModel =
ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java]
fingerprintStateViewModel =
ViewModelProvider(requireActivity())[FingerprintViewModel::class.java]
fingerprintEnrollViewModel =
ViewModelProvider(requireActivity())[FingerprintEnrollViewModel::class.java]
fingerprintScrollViewModel =
ViewModelProvider(requireActivity())[FingerprintScrollViewModel::class.java]
gateKeeperViewModel =
@@ -98,13 +98,11 @@ class FingerprintEnrollmentIntroV2Fragment : Fragment(R.layout.fingerprint_v2_en
lifecycleScope.launch {
combine(
navigationViewModel.enrollType,
fingerprintStateViewModel.fingerprintStateViewModel,
) { enrollType, fingerprintStateViewModel ->
Pair(enrollType, fingerprintStateViewModel)
fingerprintEnrollViewModel.sensorType,
) { enrollType, sensorType ->
Pair(enrollType, sensorType)
}
.collect { (enrollType, fingerprintStateViewModel) ->
val sensorProps = fingerprintStateViewModel?.sensorProps
.collect { (enrollType, sensorType) ->
textModel =
when (enrollType) {
Unicorn -> getUnicornTextModel()
@@ -145,7 +143,7 @@ class FingerprintEnrollmentIntroV2Fragment : Fragment(R.layout.fingerprint_v2_en
val iconShield: ImageView = view.requireViewById(R.id.icon_shield)
val footerMessage6: TextView = view.requireViewById(R.id.footer_message_6)
when (sensorProps?.sensorType) {
when (sensorType) {
FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC,
FingerprintSensorProperties.TYPE_UDFPS_OPTICAL -> {
footerMessage6.visibility = View.VISIBLE

View File

@@ -0,0 +1,44 @@
/*
* 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.biometrics.fingerprint2.ui.enrollment.viewmodel
import android.hardware.fingerprint.FingerprintSensorProperties
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.transform
/** Represents all of the fingerprint information needed for fingerprint enrollment. */
class FingerprintEnrollViewModel(fingerprintManagerInteractor: FingerprintManagerInteractor) :
ViewModel() {
/** Represents the stream of [FingerprintSensorProperties.SensorType] */
val sensorType: Flow<Int> =
fingerprintManagerInteractor.sensorPropertiesInternal.transform { it?.sensorType }
class FingerprintEnrollViewModelFactory(val interactor: FingerprintManagerInteractor) :
ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
): T {
return FingerprintEnrollViewModel(interactor) as T
}
}
}

View File

@@ -1,81 +0,0 @@
/*
* 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.biometrics.fingerprint2.ui.enrollment.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintStateViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/** Represents all of the fingerprint information needed for fingerprint enrollment. */
class FingerprintViewModel(fingerprintManagerInteractor: FingerprintManagerInteractor) :
ViewModel() {
private val _fingerprintViewModel: MutableStateFlow<FingerprintStateViewModel?> =
MutableStateFlow(null)
/**
* A flow that contains a [FingerprintStateViewModel] which contains the relevant information for
* enrollment
*/
val fingerprintStateViewModel: Flow<FingerprintStateViewModel?> =
_fingerprintViewModel.asStateFlow()
init {
viewModelScope.launch {
val enrolledFingerprints =
fingerprintManagerInteractor.enrolledFingerprints.last().map {
com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel(
it.name,
it.fingerId,
it.deviceId
)
}
val sensorProps = fingerprintManagerInteractor.sensorPropertiesInternal().first()
val maxFingerprints = 5
_fingerprintViewModel.update {
FingerprintStateViewModel(
enrolledFingerprints,
enrolledFingerprints.size < maxFingerprints,
maxFingerprints,
sensorProps.isAnySidefpsType,
false,
sensorProps,
)
}
}
}
class FingerprintViewModelFactory(val interactor: FingerprintManagerInteractor) :
ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
): T {
return FingerprintViewModel(interactor) as T
}
}
}

View File

@@ -20,7 +20,6 @@ import android.hardware.fingerprint.FingerprintManager
import android.util.Log
import androidx.lifecycle.LifecycleCoroutineScope
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintStateViewModel
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
import com.android.settings.biometrics.fingerprint2.ui.settings.binder.FingerprintSettingsViewBinder.FingerprintView
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.EnrollAdditionalFingerprint
@@ -35,6 +34,7 @@ import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.Prefer
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.ShowSettings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
@@ -70,7 +70,11 @@ object FingerprintSettingsViewBinder {
/** Indicates what result should be set for the returning callee */
fun setResultExternal(resultCode: Int)
/** Indicates the settings UI should be shown */
fun showSettings(state: FingerprintStateViewModel)
fun showSettings(enrolledFingerprints: List<FingerprintViewModel>)
/** Updates the add fingerprints preference */
fun updateAddFingerprintsPreference(canEnroll: Boolean, maxFingerprints: Int)
/** Updates the sfps fingerprints preference */
fun updateSfpsPreference(isSfpsPrefVisible: Boolean)
/** Indicates that a user has been locked out */
fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error)
/** Indicates a fingerprint preference should be highlighted */
@@ -93,9 +97,13 @@ object FingerprintSettingsViewBinder {
/** Result listener for launching enrollments **after** a user has reached the settings page. */
// Settings display flow
lifecycleScope.launch { viewModel.enrolledFingerprints.collect { view.showSettings(it) } }
lifecycleScope.launch {
viewModel.fingerprintState.filterNotNull().collect { view.showSettings(it) }
viewModel.addFingerprintPrefInfo.collect { (enablePref, maxFingerprints) ->
view.updateAddFingerprintsPreference(enablePref, maxFingerprints)
}
}
lifecycleScope.launch { viewModel.isSfpsPrefVisible.collect { view.updateSfpsPreference(it) } }
// Dialog flow
lifecycleScope.launch {

View File

@@ -47,7 +47,6 @@ import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintStateViewModel
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
import com.android.settings.biometrics.fingerprint2.ui.settings.binder.FingerprintSettingsViewBinder
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsNavigationViewModel
@@ -304,44 +303,52 @@ class FingerprintSettingsV2Fragment :
settingsViewModel.onDeleteClicked(fingerprintViewModel)
}
override fun showSettings(state: FingerprintStateViewModel) {
override fun showSettings(enrolledFingerprints: List<FingerprintViewModel>) {
val category =
this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)
as PreferenceCategory?
category?.removeAll()
state.fingerprintViewModels.forEach { fingerprint ->
enrolledFingerprints.forEach { fingerprint ->
category?.addPreference(
FingerprintSettingsPreference(
requireContext(),
fingerprint,
this@FingerprintSettingsV2Fragment,
state.fingerprintViewModels.size == 1,
enrolledFingerprints.size == 1,
)
)
}
category?.isVisible = true
createFingerprintsFooterPreference(state.canEnroll, state.maxFingerprints)
preferenceScreen.isVisible = true
addFooter()
}
override fun updateAddFingerprintsPreference(canEnroll: Boolean, maxFingerprints: Int) {
val pref = this@FingerprintSettingsV2Fragment.findPreference<Preference>(KEY_FINGERPRINT_ADD)
val maxSummary = context?.getString(R.string.fingerprint_add_max, maxFingerprints) ?: ""
pref?.summary = maxSummary
pref?.isEnabled = canEnroll
pref?.setOnPreferenceClickListener {
navigationViewModel.onAddFingerprintClicked()
true
}
pref?.isVisible = true
}
override fun updateSfpsPreference(isSfpsPrefVisible: Boolean) {
val sideFpsPref =
this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_SIDE_FPS_CATEGORY)
as PreferenceCategory?
sideFpsPref?.isVisible = false
if (state.hasSideFps) {
sideFpsPref?.isVisible = state.fingerprintViewModels.isNotEmpty()
val otherPref =
this@FingerprintSettingsV2Fragment.findPreference(
KEY_FINGERPRINT_SIDE_FPS_SCREEN_ON_TO_AUTH
) as Preference?
otherPref?.isVisible = state.fingerprintViewModels.isNotEmpty()
}
addFooter(state.hasSideFps)
sideFpsPref?.isVisible = isSfpsPrefVisible
val otherPref =
this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_SIDE_FPS_SCREEN_ON_TO_AUTH)
as Preference?
otherPref?.isVisible = isSfpsPrefVisible
}
private fun addFooter(hasSideFps: Boolean) {
private fun addFooter() {
val footer =
this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_FOOTER)
as PreferenceCategory?
@@ -380,10 +387,8 @@ class FingerprintSettingsV2Fragment :
footerColumns.add(column1)
val column2 = FooterColumn()
column2.title = getText(R.string.security_fingerprint_disclaimer_lockscreen_disabled_2)
if (hasSideFps) {
column2.learnMoreOverrideText =
getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
}
column2.learnMoreOverrideText =
getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
column2.learnMoreOnClickListener = learnMoreClickListener
footerColumns.add(column2)
} else {
@@ -394,10 +399,8 @@ class FingerprintSettingsV2Fragment :
DeviceHelper.getDeviceName(requireActivity())
)
column.learnMoreOnClickListener = learnMoreClickListener
if (hasSideFps) {
column.learnMoreOverrideText =
getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
}
column.learnMoreOverrideText =
getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
footerColumns.add(column)
}
@@ -550,18 +553,6 @@ class FingerprintSettingsV2Fragment :
}
}
private fun createFingerprintsFooterPreference(canEnroll: Boolean, maxFingerprints: Int) {
val pref = this@FingerprintSettingsV2Fragment.findPreference<Preference>(KEY_FINGERPRINT_ADD)
val maxSummary = context?.getString(R.string.fingerprint_add_max, maxFingerprints) ?: ""
pref?.summary = maxSummary
pref?.isEnabled = canEnroll
pref?.setOnPreferenceClickListener {
navigationViewModel.onAddFingerprintClicked()
true
}
pref?.isVisible = true
}
private fun fingerprintPreferences(): List<FingerprintSettingsPreference?> {
val category =
this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)

View File

@@ -18,26 +18,27 @@ package com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel
import android.hardware.fingerprint.FingerprintManager
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintStateViewModel
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -52,13 +53,30 @@ class FingerprintSettingsViewModel(
private val backgroundDispatcher: CoroutineDispatcher,
private val navigationViewModel: FingerprintSettingsNavigationViewModel,
) : ViewModel() {
private val _consumerShouldAuthenticate: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val fingerprintSensorPropertiesInternal:
MutableStateFlow<List<FingerprintSensorPropertiesInternal>?> =
private val _enrolledFingerprints: MutableStateFlow<List<FingerprintViewModel>?> =
MutableStateFlow(null)
/** Represents the stream of enrolled fingerprints. */
val enrolledFingerprints: Flow<List<FingerprintViewModel>> =
_enrolledFingerprints.asStateFlow().filterNotNull().filterOnlyWhenSettingsIsShown()
/** Represents the stream of the information of "Add Fingerprint" preference. */
val addFingerprintPrefInfo: Flow<Pair<Boolean, Int>> =
_enrolledFingerprints.filterOnlyWhenSettingsIsShown().transform {
emit(
Pair(
fingerprintManagerInteractor.canEnrollFingerprints.first(),
fingerprintManagerInteractor.maxEnrollableFingerprints.first()
)
)
}
/** Represents the stream of visibility of sfps preference. */
val isSfpsPrefVisible: Flow<Boolean> =
_enrolledFingerprints.filterOnlyWhenSettingsIsShown().transform {
emit(fingerprintManagerInteractor.hasSideFps() && !it.isNullOrEmpty())
}
private val _isShowingDialog: MutableStateFlow<PreferenceViewModel?> = MutableStateFlow(null)
val isShowingDialog =
_isShowingDialog.combine(navigationViewModel.nextStep) { dialogFlow, nextStep ->
@@ -69,16 +87,13 @@ class FingerprintSettingsViewModel(
}
}
private val _fingerprintStateViewModel: MutableStateFlow<FingerprintStateViewModel?> =
MutableStateFlow(null)
val fingerprintState: Flow<FingerprintStateViewModel?> =
_fingerprintStateViewModel.combineTransform(navigationViewModel.nextStep) {
settingsShowingViewModel,
currStep ->
if (currStep != null && currStep is ShowSettings) {
emit(settingsShowingViewModel)
}
}
private val _consumerShouldAuthenticate: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _fingerprintSensorType: Flow<Int> =
fingerprintManagerInteractor.sensorPropertiesInternal.transform { it?.sensorType }
private val _sensorNullOrEmpty: Flow<Boolean> =
fingerprintManagerInteractor.sensorPropertiesInternal.map{it ==null}
private val _isLockedOut: MutableStateFlow<FingerprintAuthAttemptViewModel.Error?> =
MutableStateFlow(null)
@@ -86,7 +101,7 @@ class FingerprintSettingsViewModel(
private val _authSucceeded: MutableSharedFlow<FingerprintAuthAttemptViewModel.Success?> =
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
* implementation would take quite a lot of code to implement, it might be easier to rewrite
@@ -101,11 +116,20 @@ class FingerprintSettingsViewModel(
_isShowingDialog,
navigationViewModel.nextStep,
_consumerShouldAuthenticate,
_fingerprintStateViewModel,
_enrolledFingerprints,
_isLockedOut,
attemptsSoFar,
fingerprintSensorPropertiesInternal
) { dialogShowing, step, resume, fingerprints, isLockedOut, attempts, sensorProps ->
_attemptsSoFar,
_fingerprintSensorType,
_sensorNullOrEmpty
) {
dialogShowing,
step,
resume,
fingerprints,
isLockedOut,
attempts,
sensorType,
sensorNullOrEmpty ->
if (DEBUG) {
Log.d(
TAG,
@@ -115,13 +139,13 @@ class FingerprintSettingsViewModel(
"fingerprints=${fingerprints}," +
"lockedOut=${isLockedOut}," +
"attempts=${attempts}," +
"sensorProps=${sensorProps}"
"sensorType=${sensorType}" +
"sensorNullOrEmpty=${sensorNullOrEmpty}"
)
}
if (sensorProps.isNullOrEmpty()) {
if (sensorNullOrEmpty) {
return@combine false
}
val sensorType = sensorProps[0].sensorType
if (
listOf(
FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
@@ -133,7 +157,7 @@ class FingerprintSettingsViewModel(
}
if (step != null && step is ShowSettings) {
if (fingerprints?.fingerprintViewModels?.isNotEmpty() == true) {
if (fingerprints?.isNotEmpty() == true) {
return@combine dialogShowing == null && isLockedOut == null && resume && attempts < 15
}
}
@@ -172,18 +196,12 @@ 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()
updateEnrolledFingerprints()
}
}
}
@@ -200,7 +218,7 @@ class FingerprintSettingsViewModel(
}
override fun toString(): String {
return "userId: $userId\n" + "fingerprintState: ${_fingerprintStateViewModel.value}\n"
return "userId: $userId\n" + "enrolledFingerprints: ${_enrolledFingerprints.value}\n"
}
/** The fingerprint delete button has been clicked. */
@@ -229,7 +247,7 @@ class FingerprintSettingsViewModel(
fun deleteFingerprint(fp: FingerprintViewModel) {
viewModelScope.launch(backgroundDispatcher) {
if (fingerprintManagerInteractor.removeFingerprint(fp)) {
updateSettingsData()
updateEnrolledFingerprints()
}
}
}
@@ -238,45 +256,25 @@ class FingerprintSettingsViewModel(
fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
viewModelScope.launch {
fingerprintManagerInteractor.renameFingerprint(fp, newName)
updateSettingsData()
updateEnrolledFingerprints()
}
}
private fun attemptingAuth() {
attemptsSoFar.update { it + 1 }
_attemptsSoFar.update { it + 1 }
}
private suspend fun onAuthSuccess(success: FingerprintAuthAttemptViewModel.Success) {
_authSucceeded.emit(success)
attemptsSoFar.update { 0 }
_attemptsSoFar.update { 0 }
}
private fun lockout(attemptViewModel: FingerprintAuthAttemptViewModel.Error) {
_isLockedOut.update { attemptViewModel }
}
/**
* This function is sort of a hack, it's used whenever we want to check for fingerprint state
* updates.
*/
private suspend fun updateSettingsData() {
Log.d(TAG, "update settings data called")
val fingerprints = fingerprintManagerInteractor.enrolledFingerprints.last()
val canEnrollFingerprint =
fingerprintManagerInteractor.canEnrollFingerprints(fingerprints.size).last()
val maxFingerprints = fingerprintManagerInteractor.maxEnrollableFingerprints.last()
val hasSideFps = fingerprintManagerInteractor.hasSideFps()
val pressToAuthEnabled = fingerprintManagerInteractor.pressToAuthEnabled()
_fingerprintStateViewModel.update {
FingerprintStateViewModel(
fingerprints,
canEnrollFingerprint,
maxFingerprints,
hasSideFps,
pressToAuthEnabled,
fingerprintManagerInteractor.sensorPropertiesInternal().first(),
)
}
private suspend fun updateEnrolledFingerprints() {
_enrolledFingerprints.update { fingerprintManagerInteractor.enrolledFingerprints.first() }
}
/** Used to indicate whether the consumer of the view model is ready for authentication. */
@@ -284,6 +282,13 @@ class FingerprintSettingsViewModel(
_consumerShouldAuthenticate.update { authenticate }
}
private fun <T> Flow<T>.filterOnlyWhenSettingsIsShown() =
combineTransform(navigationViewModel.nextStep) { value, currStep ->
if (currStep != null && currStep is ShowSettings) {
emit(value)
}
}
class FingerprintSettingsViewModelFactory(
private val userId: Int,
private val interactor: FingerprintManagerInteractor,
@@ -307,7 +312,7 @@ class FingerprintSettingsViewModel(
}
}
private inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
private inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine(
flow: Flow<T1>,
flow2: Flow<T2>,
flow3: Flow<T3>,
@@ -315,9 +320,10 @@ private inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
flow5: Flow<T5>,
flow6: Flow<T6>,
flow7: Flow<T7>,
crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
flow8: Flow<T8>,
crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R
): Flow<R> {
return combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> ->
return combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) { args: Array<*> ->
@Suppress("UNCHECKED_CAST")
transform(
args[0] as T1,
@@ -327,6 +333,7 @@ private inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
args[4] as T5,
args[5] as T6,
args[6] as T7,
args[7] as T8,
)
}
}

View File

@@ -23,7 +23,7 @@ import com.android.settings.biometrics.fingerprint2.domain.interactor.Fingerprin
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
/** Fake to be used by other classes to easily fake the FingerprintManager implementation. */
class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
@@ -53,15 +53,16 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> {
return challengeToGenerate
}
override val enrolledFingerprints: Flow<List<FingerprintViewModel>> = flow {
emit(enrolledFingerprintsInternal)
}
override val enrolledFingerprints: Flow<List<FingerprintViewModel>> =
flowOf(enrolledFingerprintsInternal)
override fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean> = flow {
emit(numFingerprints < enrollableFingerprints)
}
override val canEnrollFingerprints: Flow<Boolean> =
flowOf(enrolledFingerprintsInternal.size < enrollableFingerprints)
override val maxEnrollableFingerprints: Flow<Int> = flow { emit(enrollableFingerprints) }
override val sensorPropertiesInternal: Flow<FingerprintSensorPropertiesInternal?> =
flowOf(sensorProps.first())
override val maxEnrollableFingerprints: Flow<Int> = flowOf(enrollableFingerprints)
override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean {
return enrolledFingerprintsInternal.remove(fp)
@@ -80,7 +81,4 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
override suspend fun pressToAuthEnabled(): Boolean {
return pressToAuthEnabled
}
override suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal> =
sensorProps
}

View File

@@ -18,7 +18,6 @@ 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
@@ -51,8 +50,11 @@ import org.mockito.ArgumentMatchers.eq
import org.mockito.ArgumentMatchers.nullable
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.stubbing.OngoingStubbing
@RunWith(MockitoJUnitRunner::class)
class FingerprintManagerInteractorTest {
@@ -82,8 +84,7 @@ class FingerprintManagerInteractorTest {
@Test
fun testEmptyFingerprints() =
testScope.runTest {
Mockito.`when`(fingerprintManager.getEnrolledFingerprints(Mockito.anyInt()))
.thenReturn(emptyList())
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
val emptyFingerprintList: List<Fingerprint> = emptyList()
assertThat(underTest.enrolledFingerprints.last()).isEqualTo(emptyFingerprintList)
@@ -94,8 +95,7 @@ class FingerprintManagerInteractorTest {
testScope.runTest {
val expected = Fingerprint("Finger 1,", 2, 3L)
val fingerprintList: List<Fingerprint> = listOf(expected)
Mockito.`when`(fingerprintManager.getEnrolledFingerprints(Mockito.anyInt()))
.thenReturn(fingerprintList)
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(fingerprintList)
val list = underTest.enrolledFingerprints.last()
assertThat(list.size).isEqualTo(fingerprintList.size)
@@ -108,21 +108,22 @@ class FingerprintManagerInteractorTest {
@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,
val fingerprintList1: List<Fingerprint> =
listOf(
Fingerprint("Finger 1,", 2, 3L),
Fingerprint("Finger 2,", 3, 3L),
Fingerprint("Finger 3,", 4, 3L)
)
val fingerprintList2: List<Fingerprint> =
fingerprintList1.plus(
listOf(Fingerprint("Finger 4,", 5, 3L), Fingerprint("Finger 5,", 6, 3L))
)
whenever(fingerprintManager.getEnrolledFingerprints(anyInt()))
.thenReturn(fingerprintList1)
.thenReturn(fingerprintList2)
assertThat(underTest.canEnrollFingerprints(2).last()).isTrue()
assertThat(underTest.canEnrollFingerprints(3).last()).isFalse()
assertThat(underTest.canEnrollFingerprints.last()).isTrue()
assertThat(underTest.canEnrollFingerprints.last()).isFalse()
}
@Test
@@ -132,7 +133,7 @@ class FingerprintManagerInteractorTest {
val challenge = 100L
val intent = Intent()
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, challenge)
Mockito.`when`(
whenever(
gateKeeperPasswordProvider.requestGatekeeperHat(
any(Intent::class.java),
anyLong(),
@@ -148,8 +149,7 @@ class FingerprintManagerInteractorTest {
val job = testScope.launch { result = underTest.generateChallenge(1L) }
runCurrent()
Mockito.verify(fingerprintManager)
.generateChallenge(anyInt(), capture(generateChallengeCallback))
verify(fingerprintManager).generateChallenge(anyInt(), capture(generateChallengeCallback))
generateChallengeCallback.value.onChallengeGenerated(1, 2, challenge)
runCurrent()
@@ -173,7 +173,7 @@ class FingerprintManagerInteractorTest {
testScope.launch { result = underTest.removeFingerprint(fingerprintViewModelToRemove) }
runCurrent()
Mockito.verify(fingerprintManager)
verify(fingerprintManager)
.remove(any(Fingerprint::class.java), anyInt(), capture(removalCallback))
removalCallback.value.onRemovalSucceeded(fingerprintToRemove, 1)
@@ -197,7 +197,7 @@ class FingerprintManagerInteractorTest {
testScope.launch { result = underTest.removeFingerprint(fingerprintViewModelToRemove) }
runCurrent()
Mockito.verify(fingerprintManager)
verify(fingerprintManager)
.remove(any(Fingerprint::class.java), anyInt(), capture(removalCallback))
removalCallback.value.onRemovalError(
fingerprintToRemove,
@@ -218,8 +218,7 @@ class FingerprintManagerInteractorTest {
underTest.renameFingerprint(fingerprintToRename, "Woo")
Mockito.verify(fingerprintManager)
.rename(eq(fingerprintToRename.fingerId), anyInt(), safeEq("Woo"))
verify(fingerprintManager).rename(eq(fingerprintToRename.fingerId), anyInt(), safeEq("Woo"))
}
@Test
@@ -235,7 +234,7 @@ class FingerprintManagerInteractorTest {
runCurrent()
Mockito.verify(fingerprintManager)
verify(fingerprintManager)
.authenticate(
nullable(CryptoObject::class.java),
any(CancellationSignal::class.java),
@@ -263,7 +262,7 @@ class FingerprintManagerInteractorTest {
runCurrent()
Mockito.verify(fingerprintManager)
verify(fingerprintManager)
.authenticate(
nullable(CryptoObject::class.java),
any(CancellationSignal::class.java),
@@ -284,4 +283,5 @@ class FingerprintManagerInteractorTest {
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)
private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
}