diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt index e7c458dc9af..1f57198e9d6 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt @@ -47,6 +47,12 @@ interface FingerprintManagerInteractor { /** Returns the max enrollable fingerprints, note during SUW this might be 1 */ val maxEnrollableFingerprints: Flow + /** Returns true if a user can enroll a fingerprint false otherwise. */ + val canEnrollFingerprints: Flow + + /** Retrieves the sensor properties of a device */ + val sensorPropertiesInternal: Flow + /** Runs [FingerprintManager.authenticate] */ suspend fun authenticate(): FingerprintAuthAttemptViewModel @@ -60,9 +66,6 @@ interface FingerprintManagerInteractor { */ suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair - /** Returns true if a user can enroll a fingerprint false otherwise. */ - fun canEnrollFingerprints(numFingerprints: Int): Flow - /** * 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 } class FingerprintManagerInteractorImpl( @@ -120,8 +120,15 @@ class FingerprintManagerInteractorImpl( ) } - override fun canEnrollFingerprints(numFingerprints: Int): Flow = flow { - emit(numFingerprints < maxFingerprints) + override val canEnrollFingerprints: Flow = 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 = - suspendCancellableCoroutine { - it.resume(fingerprintManager.sensorPropertiesInternal) - } - override suspend fun authenticate(): FingerprintAuthAttemptViewModel = suspendCancellableCoroutine { c: CancellableContinuation -> val authenticationCallback = diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt index 36bdf8ddb6e..db28e793533 100644 --- a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt @@ -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, - val canEnroll: Boolean, - val maxFingerprints: Int, - val hasSideFps: Boolean, - val pressToAuth: Boolean, - val sensorProps: FingerprintSensorPropertiesInternal, -) - data class FingerprintViewModel( val name: String, val fingerId: Int, diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt index d497d464257..f6d20ae0b8f 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt @@ -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())[ diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollmentIntroV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollmentIntroV2Fragment.kt index c7fcb669c54..ebbdef854fd 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollmentIntroV2Fragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollmentIntroV2Fragment.kt @@ -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 diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt new file mode 100644 index 00000000000..879f425eb7c --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt @@ -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 = + fingerprintManagerInteractor.sensorPropertiesInternal.transform { it?.sensorType } + + class FingerprintEnrollViewModelFactory(val interactor: FingerprintManagerInteractor) : + ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + ): T { + return FingerprintEnrollViewModel(interactor) as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintStateViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintStateViewModel.kt deleted file mode 100644 index 20e3a0a90a6..00000000000 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintStateViewModel.kt +++ /dev/null @@ -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 = - MutableStateFlow(null) - - /** - * A flow that contains a [FingerprintStateViewModel] which contains the relevant information for - * enrollment - */ - val fingerprintStateViewModel: Flow = - _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 create( - modelClass: Class, - ): T { - - return FingerprintViewModel(interactor) as T - } - } -} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/binder/FingerprintSettingsViewBinder.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/binder/FingerprintSettingsViewBinder.kt index 6a4463038dd..9f42d819b56 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/binder/FingerprintSettingsViewBinder.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/binder/FingerprintSettingsViewBinder.kt @@ -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) + /** 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 { diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt index 7dcf46aea41..c818566ae32 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt @@ -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) { 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(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(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 { val category = this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY) diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt index fbd0f1d251e..64d8a12958e 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt @@ -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 = MutableStateFlow(false) - - private val fingerprintSensorPropertiesInternal: - MutableStateFlow?> = + private val _enrolledFingerprints: MutableStateFlow?> = MutableStateFlow(null) + /** Represents the stream of enrolled fingerprints. */ + val enrolledFingerprints: Flow> = + _enrolledFingerprints.asStateFlow().filterNotNull().filterOnlyWhenSettingsIsShown() + + /** Represents the stream of the information of "Add Fingerprint" preference. */ + val addFingerprintPrefInfo: Flow> = + _enrolledFingerprints.filterOnlyWhenSettingsIsShown().transform { + emit( + Pair( + fingerprintManagerInteractor.canEnrollFingerprints.first(), + fingerprintManagerInteractor.maxEnrollableFingerprints.first() + ) + ) + } + + /** Represents the stream of visibility of sfps preference. */ + val isSfpsPrefVisible: Flow = + _enrolledFingerprints.filterOnlyWhenSettingsIsShown().transform { + emit(fingerprintManagerInteractor.hasSideFps() && !it.isNullOrEmpty()) + } + private val _isShowingDialog: MutableStateFlow = MutableStateFlow(null) val isShowingDialog = _isShowingDialog.combine(navigationViewModel.nextStep) { dialogFlow, nextStep -> @@ -69,16 +87,13 @@ class FingerprintSettingsViewModel( } } - private val _fingerprintStateViewModel: MutableStateFlow = - MutableStateFlow(null) - val fingerprintState: Flow = - _fingerprintStateViewModel.combineTransform(navigationViewModel.nextStep) { - settingsShowingViewModel, - currStep -> - if (currStep != null && currStep is ShowSettings) { - emit(settingsShowingViewModel) - } - } + private val _consumerShouldAuthenticate: MutableStateFlow = MutableStateFlow(false) + + private val _fingerprintSensorType: Flow = + fingerprintManagerInteractor.sensorPropertiesInternal.transform { it?.sensorType } + + private val _sensorNullOrEmpty: Flow = + fingerprintManagerInteractor.sensorPropertiesInternal.map{it ==null} private val _isLockedOut: MutableStateFlow = MutableStateFlow(null) @@ -86,7 +101,7 @@ class FingerprintSettingsViewModel( private val _authSucceeded: MutableSharedFlow = MutableSharedFlow() - private val attemptsSoFar: MutableStateFlow = MutableStateFlow(0) + 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 @@ -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 Flow.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 combine( +private inline fun combine( flow: Flow, flow2: Flow, flow3: Flow, @@ -315,9 +320,10 @@ private inline fun combine( flow5: Flow, flow6: Flow, flow7: Flow, - crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R + flow8: Flow, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R ): Flow { - 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 combine( args[4] as T5, args[5] as T6, args[6] as T7, + args[7] as T8, ) } } 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 759306e6bb0..e2bdd17344c 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 @@ -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 { return challengeToGenerate } - override val enrolledFingerprints: Flow> = flow { - emit(enrolledFingerprintsInternal) - } + override val enrolledFingerprints: Flow> = + flowOf(enrolledFingerprintsInternal) - override fun canEnrollFingerprints(numFingerprints: Int): Flow = flow { - emit(numFingerprints < enrollableFingerprints) - } + override val canEnrollFingerprints: Flow = + flowOf(enrolledFingerprintsInternal.size < enrollableFingerprints) - override val maxEnrollableFingerprints: Flow = flow { emit(enrollableFingerprints) } + override val sensorPropertiesInternal: Flow = + flowOf(sensorProps.first()) + + override val maxEnrollableFingerprints: Flow = 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 = - 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 index cc6f42af2aa..70943f07e85 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt @@ -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 = 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 = 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 = + listOf( + Fingerprint("Finger 1,", 2, 3L), + Fingerprint("Finger 2,", 3, 3L), + Fingerprint("Finger 3,", 4, 3L) ) + val fingerprintList2: List = + 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 safeEq(value: T): T = eq(value) ?: value private fun capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture() private fun any(type: Class): T = Mockito.any(type) + private fun whenever(methodCall: T): OngoingStubbing = `when`(methodCall) }