From 2200f55ffed1d0f1f7c81e9a1b646ac59641d3ba Mon Sep 17 00:00:00 2001 From: Hao Dong Date: Mon, 28 Aug 2023 17:41:45 +0000 Subject: [PATCH] Add enroll() in FingerprintManagerInteractor. Test: atest FingerprintManagerInteractorTest Bug: 295206773 Change-Id: If2fc46b1c952c3e55c698a18e125e194efe5ffb6 --- .../FingerprintManagerInteractor.kt | 61 +++++++++++++ .../FingerprintEnrollmentV2Activity.kt | 14 +-- ...FingerprintEnrollConfirmationV2Fragment.kt | 4 +- .../FingerprintEnrollEnrollingV2Fragment.kt | 4 +- .../FingerprintEnrollFindSensorV2Fragment.kt | 4 +- ...kt => FingerprintEnrollIntroV2Fragment.kt} | 8 +- .../ui/enrollment/viewmodel/EnrollReason.kt | 36 ++++++++ .../viewmodel/FingerEnrollStateViewModel.kt | 40 +++++++++ .../viewmodel/FingerprintEnrollViewModel.kt | 65 ++++++++++++-- ... FingerprintEnrolllNavigationViewModel.kt} | 8 +- .../FingerprintGatekeeperViewModel.kt | 2 + .../FakeFingerprintManagerInteractor.kt | 8 ++ .../FingerprintManagerInteractorTest.kt | 90 +++++++++++++++++-- 13 files changed, 307 insertions(+), 37 deletions(-) rename src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/{FingerprintEnrollmentIntroV2Fragment.kt => FingerprintEnrollIntroV2Fragment.kt} (97%) create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/EnrollReason.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerEnrollStateViewModel.kt rename src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/{FingerprintEnrolllmentNavigationViewModel.kt => FingerprintEnrolllNavigationViewModel.kt} (95%) 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 1f57198e9d6..2c8ee8f7a40 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt @@ -27,12 +27,18 @@ import android.util.Log import com.android.settings.biometrics.GatekeeperPasswordProvider import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.EnrollReason +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerEnrollStateViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.toOriginalReason import com.android.settings.password.ChooseLockSettingsHelper import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.onFailure import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @@ -66,6 +72,16 @@ interface FingerprintManagerInteractor { */ suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair + /** + * Runs [FingerprintManager.enroll] with the [hardwareAuthToken] and [EnrollReason] for this + * enrollment. Returning the [FingerEnrollStateViewModel] that represents this fingerprint + * enrollment state. + */ + suspend fun enroll( + hardwareAuthToken: ByteArray?, + enrollReason: EnrollReason, + ): Flow + /** * Removes the given fingerprint, returning true if it was successfully removed and false * otherwise @@ -133,6 +149,51 @@ class FingerprintManagerInteractorImpl( override val maxEnrollableFingerprints = flow { emit(maxFingerprints) } + override suspend fun enroll( + hardwareAuthToken: ByteArray?, + enrollReason: EnrollReason, + ): Flow = callbackFlow { + var streamEnded = false + val enrollmentCallback = + object : FingerprintManager.EnrollmentCallback() { + override fun onEnrollmentProgress(remaining: Int) { + trySend(FingerEnrollStateViewModel.EnrollProgress(remaining)).onFailure { error -> + Log.d(TAG, "onEnrollmentProgress($remaining) failed to send, due to $error") + } + if (remaining == 0) { + streamEnded = true + } + } + + override fun onEnrollmentHelp(helpMsgId: Int, helpString: CharSequence?) { + trySend(FingerEnrollStateViewModel.EnrollHelp(helpMsgId, helpString.toString())) + .onFailure { error -> Log.d(TAG, "onEnrollmentHelp failed to send, due to $error") } + } + + override fun onEnrollmentError(errMsgId: Int, errString: CharSequence?) { + trySend(FingerEnrollStateViewModel.EnrollError(errMsgId, errString.toString())) + .onFailure { error -> Log.d(TAG, "onEnrollmentError failed to send, due to $error") } + streamEnded = true + } + } + + val cancellationSignal = CancellationSignal() + fingerprintManager.enroll( + hardwareAuthToken, + cancellationSignal, + applicationContext.userId, + enrollmentCallback, + enrollReason.toOriginalReason() + ) + awaitClose { + // If the stream has not been ended, and the user has stopped collecting the flow + // before it was over, send cancel. + if (!streamEnded) { + cancellationSignal.cancel() + } + } + } + override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean = suspendCoroutine { val callback = object : RemovalCallback() { 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 f6d20ae0b8f..d43aebac239 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 @@ -43,12 +43,12 @@ import com.android.settings.biometrics.fingerprint2.domain.interactor.Fingerprin import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollConfirmationV2Fragment import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollEnrollingV2Fragment import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollFindSensorV2Fragment -import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollmentIntroV2Fragment +import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollIntroV2Fragment 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.FingerprintEnrollNavigationViewModel 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.Finish @@ -70,7 +70,7 @@ private const val TAG = "FingerprintEnrollmentV2Activity" * children fragments. */ class FingerprintEnrollmentV2Activity : FragmentActivity() { - private lateinit var navigationViewModel: FingerprintEnrollmentNavigationViewModel + private lateinit var navigationViewModel: FingerprintEnrollNavigationViewModel private lateinit var gatekeeperViewModel: FingerprintGatekeeperViewModel private val coroutineDispatcher = Dispatchers.Default @@ -170,18 +170,18 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { navigationViewModel = ViewModelProvider( this, - FingerprintEnrollmentNavigationViewModel.FingerprintEnrollmentNavigationViewModelFactory( + FingerprintEnrollNavigationViewModel.FingerprintEnrollNavigationViewModelFactory( backgroundDispatcher, interactor, gatekeeperViewModel, gatekeeperInfo is GatekeeperInfo.GatekeeperPasswordInfo, /* canSkipConfirm */ ) - )[FingerprintEnrollmentNavigationViewModel::class.java] + )[FingerprintEnrollNavigationViewModel::class.java] // Initialize FingerprintViewModel ViewModelProvider( this, - FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory(interactor) + FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory(interactor, backgroundDispatcher) )[FingerprintEnrollViewModel::class.java] // Initialize scroll view model @@ -198,7 +198,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { Confirmation -> FingerprintEnrollConfirmationV2Fragment::class.java as Class Education -> FingerprintEnrollFindSensorV2Fragment::class.java as Class Enrollment -> FingerprintEnrollEnrollingV2Fragment::class.java as Class - Intro -> FingerprintEnrollmentIntroV2Fragment::class.java as Class + Intro -> FingerprintEnrollIntroV2Fragment::class.java as Class else -> null } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollConfirmationV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollConfirmationV2Fragment.kt index df4cf725ca3..b12491f3afd 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollConfirmationV2Fragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollConfirmationV2Fragment.kt @@ -19,7 +19,7 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment import android.os.Bundle import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollmentNavigationViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel /** * A fragment to indicate that fingerprint enrollment has been completed. @@ -33,7 +33,7 @@ class FingerprintEnrollConfirmationV2Fragment : Fragment() { super.onCreate(savedInstanceState) if (savedInstanceState == null) { val navigationViewModel = - ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java] + ViewModelProvider(requireActivity())[FingerprintEnrollNavigationViewModel::class.java] } } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollEnrollingV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollEnrollingV2Fragment.kt index 915aa1f32fb..3f615ceb24b 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollEnrollingV2Fragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollEnrollingV2Fragment.kt @@ -19,7 +19,7 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment import android.os.Bundle import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollmentNavigationViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel /** A fragment that is responsible for enrolling a users fingerprint. */ class FingerprintEnrollEnrollingV2Fragment : Fragment() { @@ -28,7 +28,7 @@ class FingerprintEnrollEnrollingV2Fragment : Fragment() { super.onCreate(savedInstanceState) if (savedInstanceState == null) { val navigationViewModel = - ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java] + ViewModelProvider(requireActivity())[FingerprintEnrollNavigationViewModel::class.java] } } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt index e9e1db2b730..beb84e966ae 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt @@ -20,7 +20,7 @@ import android.os.Bundle import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import com.android.settings.R -import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollmentNavigationViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel /** * A fragment that is used to educate the user about the fingerprint sensor on this device. @@ -36,7 +36,7 @@ class FingerprintEnrollFindSensorV2Fragment : Fragment(R.layout.fingerprint_v2_e super.onCreate(savedInstanceState) if (savedInstanceState == null) { val navigationViewModel = - ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java] + ViewModelProvider(requireActivity())[FingerprintEnrollNavigationViewModel::class.java] } } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollmentIntroV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt similarity index 97% rename from src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollmentIntroV2Fragment.kt rename to src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt index f2f925b2107..03c7a5f46e7 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollmentIntroV2Fragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt @@ -33,8 +33,8 @@ 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.FingerprintEnrollNavigationViewModel 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.Unicorn @@ -72,10 +72,10 @@ private data class TextModel( * 2. How the data will be stored * 3. How the user can access and remove their data */ -class FingerprintEnrollmentIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll_introduction) { +class FingerprintEnrollIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll_introduction) { private lateinit var footerBarMixin: FooterBarMixin private lateinit var textModel: TextModel - private lateinit var navigationViewModel: FingerprintEnrollmentNavigationViewModel + private lateinit var navigationViewModel: FingerprintEnrollNavigationViewModel private lateinit var fingerprintEnrollViewModel: FingerprintEnrollViewModel private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel private lateinit var gateKeeperViewModel: FingerprintGatekeeperViewModel @@ -83,7 +83,7 @@ class FingerprintEnrollmentIntroV2Fragment : Fragment(R.layout.fingerprint_v2_en override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) navigationViewModel = - ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java] + ViewModelProvider(requireActivity())[FingerprintEnrollNavigationViewModel::class.java] fingerprintEnrollViewModel = ViewModelProvider(requireActivity())[FingerprintEnrollViewModel::class.java] fingerprintScrollViewModel = diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/EnrollReason.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/EnrollReason.kt new file mode 100644 index 00000000000..87deeb3f54f --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/EnrollReason.kt @@ -0,0 +1,36 @@ +/* + * 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.FingerprintManager + +/** + * The reason for enrollment. Represents [FingerprintManager.EnrollReason] + */ +enum class EnrollReason { + /** The enroll happens on education screen. */ + FindSensor, + /** The enroll happens on enrolling screen. */ + EnrollEnrolling +} + +/** Convert EnrollReason to original [FingerprintManager.EnrollReason]. */ +fun EnrollReason.toOriginalReason(): Int { + return when (this) { + EnrollReason.EnrollEnrolling -> FingerprintManager.ENROLL_ENROLL + EnrollReason.FindSensor -> FingerprintManager.ENROLL_FIND_SENSOR + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerEnrollStateViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerEnrollStateViewModel.kt new file mode 100644 index 00000000000..73343bdd048 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerEnrollStateViewModel.kt @@ -0,0 +1,40 @@ +/* + * 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.annotation.StringRes + +/** + * Represents a fingerprint enrollment state. See [FingerprintManager.EnrollmentCallback] for more + * information + */ +sealed class FingerEnrollStateViewModel { + /** Represents enrollment step progress. */ + data class EnrollProgress( + val remainingSteps: Int, + ) : FingerEnrollStateViewModel() + /** Represents that recoverable error has been encountered during enrollment. */ + data class EnrollHelp( + @StringRes val helpMsgId: Int, + val helpString: String, + ) : FingerEnrollStateViewModel() + /** Represents that an unrecoverable error has been encountered and the operation is complete. */ + data class EnrollError( + @StringRes val errMsgId: Int, + val errString: String, + ) : FingerEnrollStateViewModel() +} 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 index 31fa03d044b..cb1beb93cff 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt @@ -13,7 +13,6 @@ * 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 @@ -21,13 +20,24 @@ import androidx.lifecycle.ViewModelProvider import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.toSensorType +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.flow.update -/** Represents all of the fingerprint information needed for fingerprint enrollment. */ -class FingerprintEnrollViewModel(fingerprintManagerInteractor: FingerprintManagerInteractor) : - ViewModel() { +private const val TAG = "FingerprintEnrollViewModel" + +/** Represents all of the fingerprint information needed for a fingerprint enrollment process. */ +class FingerprintEnrollViewModel( + private val fingerprintManagerInteractor: FingerprintManagerInteractor, + backgroundDispatcher: CoroutineDispatcher, +) : ViewModel() { /** Represents the stream of [FingerprintSensorType] */ val sensorType: Flow = @@ -35,14 +45,55 @@ class FingerprintEnrollViewModel(fingerprintManagerInteractor: FingerprintManage it.sensorType.toSensorType() } - class FingerprintEnrollViewModelFactory(val interactor: FingerprintManagerInteractor) : - ViewModelProvider.Factory { + private var _enrollReason: MutableStateFlow = + MutableStateFlow(EnrollReason.FindSensor) + private var _hardwareAuthToken: MutableStateFlow = MutableStateFlow(null) + private var _consumerShouldEnroll: MutableStateFlow = MutableStateFlow(false) + /** + * A flow that contains a [FingerprintEnrollViewModel] which contains the relevant information for + * an enrollment process + */ + val enrollFlow: Flow = + combine(_consumerShouldEnroll, _hardwareAuthToken, _enrollReason) { + consumerShouldEnroll, + hardwareAuthToken, + enrollReason -> + Triple(consumerShouldEnroll, hardwareAuthToken, enrollReason) + } + .transformLatest { + // transformLatest() instead of transform() is used here for cancelling previous enroll() + // whenever |consumerShouldEnroll| is changed. Otherwise the latest value will be suspended + // since enroll() is an infinite callback flow. + (consumerShouldEnroll, hardwareAuthToken, enrollReason) -> + if (consumerShouldEnroll && hardwareAuthToken != null) { + fingerprintManagerInteractor.enroll(hardwareAuthToken, enrollReason).collect { emit(it) } + } + } + .flowOn(backgroundDispatcher) + + /** Used to indicate the consumer of the view model is ready for an enrollment. */ + fun startEnroll(hardwareAuthToken: ByteArray?, enrollReason: EnrollReason) { + _enrollReason.update { enrollReason } + _hardwareAuthToken.update { hardwareAuthToken } + // Update _consumerShouldEnroll after updating the other values. + _consumerShouldEnroll.update { true } + } + + /** Used to indicate to stop the enrollment. */ + fun stopEnroll() { + _consumerShouldEnroll.update { false } + } + + class FingerprintEnrollViewModelFactory( + val interactor: FingerprintManagerInteractor, + val backgroundDispatcher: CoroutineDispatcher + ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( modelClass: Class, ): T { - return FingerprintEnrollViewModel(interactor) as T + return FingerprintEnrollViewModel(interactor, backgroundDispatcher) as T } } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllmentNavigationViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt similarity index 95% rename from src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllmentNavigationViewModel.kt rename to src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt index 6a8a8c40acc..dafe545ed19 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllmentNavigationViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -const val TAG = "FingerprintEnrollmentNavigationViewModel" +private const val TAG = "FingerprintEnrollNavigationViewModel" /** Interface to validate a gatekeeper hat */ interface Validator { @@ -54,7 +54,7 @@ object Unicorn : EnrollType() * This class is responsible for sending a [NavigationStep] which indicates where the user is in the * Fingerprint Enrollment flow */ -class FingerprintEnrollmentNavigationViewModel( +class FingerprintEnrollNavigationViewModel( private val dispatcher: CoroutineDispatcher, private val validator: Validator, private val fingerprintManagerInteractor: FingerprintManagerInteractor, @@ -131,7 +131,7 @@ class FingerprintEnrollmentNavigationViewModel( } } - class FingerprintEnrollmentNavigationViewModelFactory( + class FingerprintEnrollNavigationViewModelFactory( private val backgroundDispatcher: CoroutineDispatcher, private val fingerprintManagerInteractor: FingerprintManagerInteractor, private val fingerprintGatekeeperViewModel: FingerprintGatekeeperViewModel, @@ -143,7 +143,7 @@ class FingerprintEnrollmentNavigationViewModel( modelClass: Class, ): T { - return FingerprintEnrollmentNavigationViewModel( + return FingerprintEnrollNavigationViewModel( backgroundDispatcher, object : Validator { override fun validateGateKeeper(challenge: Long?): Boolean { diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintGatekeeperViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintGatekeeperViewModel.kt index 5486e7ab37d..fa4463a0822 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintGatekeeperViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintGatekeeperViewModel.kt @@ -29,6 +29,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +private const val TAG = "FingerprintGatekeeperViewModel" + sealed interface GatekeeperInfo { object Invalid : GatekeeperInfo object Timeout : GatekeeperInfo 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 95150003ee9..f807f703014 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 @@ -22,6 +22,8 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal 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.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.EnrollReason +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerEnrollStateViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -32,6 +34,7 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor { var enrolledFingerprintsInternal: MutableList = mutableListOf() var challengeToGenerate: Pair = Pair(-1L, byteArrayOf()) var authenticateAttempt = FingerprintAuthAttemptViewModel.Success(1) + val enrollStateViewModel = FingerEnrollStateViewModel.EnrollProgress(1) var pressToAuthEnabled = true var sensorProps = @@ -68,6 +71,11 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor { override val maxEnrollableFingerprints: Flow = flow { emit(enrollableFingerprints) } + override suspend fun enroll( + hardwareAuthToken: ByteArray?, + enrollReason: EnrollReason + ): Flow = flow { emit(enrollStateViewModel) } + override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean { return enrolledFingerprintsInternal.remove(fp) } 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 70943f07e85..de2c494523f 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 @@ -30,6 +30,8 @@ import com.android.settings.biometrics.fingerprint2.domain.interactor.Fingerprin 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.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.EnrollReason.FindSensor +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerEnrollStateViewModel import com.android.settings.password.ChooseLockSettingsHelper import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.cancelAndJoin @@ -143,7 +145,7 @@ class FingerprintManagerInteractorTest { .thenReturn(byteArray) val generateChallengeCallback: ArgumentCaptor = - ArgumentCaptor.forClass(FingerprintManager.GenerateChallengeCallback::class.java) + argumentCaptor() var result: Pair? = null val job = testScope.launch { result = underTest.generateChallenge(1L) } @@ -165,8 +167,7 @@ class FingerprintManagerInteractorTest { val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L) val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L) - val removalCallback: ArgumentCaptor = - ArgumentCaptor.forClass(FingerprintManager.RemovalCallback::class.java) + val removalCallback: ArgumentCaptor = argumentCaptor() var result: Boolean? = null val job = @@ -189,8 +190,7 @@ class FingerprintManagerInteractorTest { val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L) val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L) - val removalCallback: ArgumentCaptor = - ArgumentCaptor.forClass(FingerprintManager.RemovalCallback::class.java) + val removalCallback: ArgumentCaptor = argumentCaptor() var result: Boolean? = null val job = @@ -229,8 +229,7 @@ class FingerprintManagerInteractorTest { var result: FingerprintAuthAttemptViewModel? = null val job = launch { result = underTest.authenticate() } - val authCallback: ArgumentCaptor = - ArgumentCaptor.forClass(FingerprintManager.AuthenticationCallback::class.java) + val authCallback: ArgumentCaptor = argumentCaptor() runCurrent() @@ -257,8 +256,7 @@ class FingerprintManagerInteractorTest { var result: FingerprintAuthAttemptViewModel? = null val job = launch { result = underTest.authenticate() } - val authCallback: ArgumentCaptor = - ArgumentCaptor.forClass(FingerprintManager.AuthenticationCallback::class.java) + val authCallback: ArgumentCaptor = argumentCaptor() runCurrent() @@ -280,8 +278,82 @@ class FingerprintManagerInteractorTest { ) } + @Test + fun testEnroll_progress() = + testScope.runTest { + val token = byteArrayOf(5, 3, 2) + var result: FingerEnrollStateViewModel? = null + val job = launch { underTest.enroll(token, FindSensor).collect { result = it } } + val enrollCallback: ArgumentCaptor = argumentCaptor() + runCurrent() + + verify(fingerprintManager) + .enroll( + eq(token), + any(CancellationSignal::class.java), + anyInt(), + capture(enrollCallback), + eq(FingerprintManager.ENROLL_FIND_SENSOR) + ) + enrollCallback.value.onEnrollmentProgress(1) + runCurrent() + job.cancelAndJoin() + + assertThat(result).isEqualTo(FingerEnrollStateViewModel.EnrollProgress(1)) + } + + @Test + fun testEnroll_help() = + testScope.runTest { + val token = byteArrayOf(5, 3, 2) + var result: FingerEnrollStateViewModel? = null + val job = launch { underTest.enroll(token, FindSensor).collect { result = it } } + val enrollCallback: ArgumentCaptor = argumentCaptor() + runCurrent() + + verify(fingerprintManager) + .enroll( + eq(token), + any(CancellationSignal::class.java), + anyInt(), + capture(enrollCallback), + eq(FingerprintManager.ENROLL_FIND_SENSOR) + ) + enrollCallback.value.onEnrollmentHelp(-1, "help") + runCurrent() + job.cancelAndJoin() + + assertThat(result).isEqualTo(FingerEnrollStateViewModel.EnrollHelp(-1, "help")) + } + + @Test + fun testEnroll_error() = + testScope.runTest { + val token = byteArrayOf(5, 3, 2) + var result: FingerEnrollStateViewModel? = null + val job = launch { underTest.enroll(token, FindSensor).collect { result = it } } + val enrollCallback: ArgumentCaptor = argumentCaptor() + runCurrent() + + verify(fingerprintManager) + .enroll( + eq(token), + any(CancellationSignal::class.java), + anyInt(), + capture(enrollCallback), + eq(FingerprintManager.ENROLL_FIND_SENSOR) + ) + enrollCallback.value.onEnrollmentError(-2, "error") + runCurrent() + job.cancelAndJoin() + + assertThat(result).isEqualTo(FingerEnrollStateViewModel.EnrollError(-2, "error")) + } + 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) + inline fun argumentCaptor(): ArgumentCaptor = + ArgumentCaptor.forClass(T::class.java) }