From a98dc8d4b566a56418fa374ee96c53b87eb55e28 Mon Sep 17 00:00:00 2001 From: Joshua McCloskey Date: Mon, 2 Oct 2023 18:20:17 +0000 Subject: [PATCH] Rear Fingerprint Enrollment Bug: 297083009 Test: atest RFPSIconTouchViewModelTest FingerprintEnrollEnrollingViewModelTest FingerprintManagerInteractorTest Change-Id: Icc072e7d7815070087ccb50ea5937c386b06fb11 --- .../fingerprint_v2_rfps_enroll_enrolling.xml | 75 ++++++ .../fingerprint2/conversion/Util.kt | 59 +++- .../FingerprintManagerInteractorImpl.kt | 70 +++-- .../repository/PressToAuthProviderImpl.kt | 51 ++++ .../data/repository/PressToAuthProvider.kt | 27 ++ .../FingerprintManagerInteractor.kt | 22 +- ...rollReasonViewModel.kt => EnrollReason.kt} | 0 ...StateViewModel.kt => FingerEnrollState.kt} | 23 +- ...erprintViewModel.kt => FingerprintData.kt} | 8 +- .../shared/model/FingerprintFlow.kt | 34 +++ .../FingerprintEnrollmentV2Activity.kt | 176 ++++++------ .../FingerprintEnrollFindSensorV2Fragment.kt | 35 ++- .../FingerprintEnrollIntroV2Fragment.kt | 4 +- .../ui/enrollment/modules/README.md | 26 ++ .../rfps/ui/fragment/RFPSEnrollFragment.kt | 252 ++++++++++++++++++ .../ui/viewmodel/RFPSIconTouchViewModel.kt | 61 +++++ .../rfps/ui/viewmodel/RFPSViewModel.kt | 102 +++++++ .../rfps/ui/widget/FingerprintErrorDialog.kt | 124 +++++++++ .../rfps/ui/widget/IconTouchDialog.kt | 73 +++++ .../rfps/ui/widget/RFPSProgressBar.kt | 107 ++++++++ .../viewmodel/BackgroundViewModel.kt | 48 ++++ .../FingerprintEnrollEnrollingViewModel.kt | 85 ++++++ .../FingerprintEnrollFindSensorViewModel.kt | 76 ++++-- .../viewmodel/FingerprintEnrollViewModel.kt | 106 +++++--- .../FingerprintEnrolllNavigationViewModel.kt | 48 ++-- .../enrollment/viewmodel/NextStepViewModel.kt | 8 +- .../binder/FingerprintSettingsViewBinder.kt | 18 +- .../fragment/FingerprintDeletionDialog.kt | 12 +- .../fragment/FingerprintSettingsPreference.kt | 12 +- .../FingerprintSettingsRenameDialog.kt | 6 +- .../fragment/FingerprintSettingsV2Fragment.kt | 82 +++--- .../viewmodel/FingerprintSettingsViewModel.kt | 30 +-- .../settings/viewmodel/PreferenceViewModel.kt | 6 +- .../FingerprintEnrollIntroFragmentTest.kt | 10 +- .../FakeFingerprintManagerInteractor.kt | 29 +- .../FingerprintManagerInteractorTest.kt | 50 ++-- ...gerprintEnrollFindSensorViewModelV2Test.kt | 13 +- .../viewmodel/RFPSIconTouchViewModelTest.kt | 142 ++++++++++ ...FingerprintEnrollEnrollingViewModelTest.kt | 153 +++++++++++ ...gerprintSettingsNavigationViewModelTest.kt | 10 +- .../FingerprintSettingsViewModelTest.kt | 40 +-- 41 files changed, 1935 insertions(+), 378 deletions(-) create mode 100644 res/layout/fingerprint_v2_rfps_enroll_enrolling.xml create mode 100644 src/com/android/settings/biometrics/fingerprint2/repository/PressToAuthProviderImpl.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/shared/data/repository/PressToAuthProvider.kt rename src/com/android/settings/biometrics/fingerprint2/shared/model/{EnrollReasonViewModel.kt => EnrollReason.kt} (100%) rename src/com/android/settings/biometrics/fingerprint2/shared/model/{FingerEnrollStateViewModel.kt => FingerEnrollState.kt} (73%) rename src/com/android/settings/biometrics/fingerprint2/shared/model/{FingerprintViewModel.kt => FingerprintData.kt} (84%) create mode 100644 src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintFlow.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/README.md create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSIconTouchViewModel.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/FingerprintErrorDialog.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/IconTouchDialog.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/RFPSProgressBar.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/BackgroundViewModel.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt create mode 100644 tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/modules/enrolling/rfps/viewmodel/RFPSIconTouchViewModelTest.kt create mode 100644 tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModelTest.kt diff --git a/res/layout/fingerprint_v2_rfps_enroll_enrolling.xml b/res/layout/fingerprint_v2_rfps_enroll_enrolling.xml new file mode 100644 index 00000000000..0b087d20b73 --- /dev/null +++ b/res/layout/fingerprint_v2_rfps_enroll_enrolling.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/biometrics/fingerprint2/conversion/Util.kt b/src/com/android/settings/biometrics/fingerprint2/conversion/Util.kt index 98b7ed07497..58ef5097834 100644 --- a/src/com/android/settings/biometrics/fingerprint2/conversion/Util.kt +++ b/src/com/android/settings/biometrics/fingerprint2/conversion/Util.kt @@ -16,14 +16,61 @@ package com.android.settings.biometrics.fingerprint2.conversion +import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_CANCELED +import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_UNABLE_TO_PROCESS import android.hardware.fingerprint.FingerprintManager +import com.android.settings.R import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason +import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState -class Util - -fun EnrollReason.toOriginalReason(): Int { - return when (this) { - EnrollReason.EnrollEnrolling -> FingerprintManager.ENROLL_ENROLL - EnrollReason.FindSensor -> FingerprintManager.ENROLL_FIND_SENSOR +object Util { + fun EnrollReason.toOriginalReason(): Int { + return when (this) { + EnrollReason.EnrollEnrolling -> FingerprintManager.ENROLL_ENROLL + EnrollReason.FindSensor -> FingerprintManager.ENROLL_FIND_SENSOR + } } + + fun Int.toEnrollError(isSetupWizard: Boolean): FingerEnrollState.EnrollError { + val errTitle = + when (this) { + FingerprintManager.FINGERPRINT_ERROR_TIMEOUT -> + R.string.security_settings_fingerprint_enroll_error_dialog_title + FingerprintManager.FINGERPRINT_ERROR_BAD_CALIBRATION -> + R.string.security_settings_fingerprint_bad_calibration_title + else -> R.string.security_settings_fingerprint_enroll_error_unable_to_process_dialog_title + } + val errString = + if (isSetupWizard) { + when (this) { + FingerprintManager.FINGERPRINT_ERROR_TIMEOUT -> + R.string.security_settings_fingerprint_enroll_error_dialog_title + FingerprintManager.FINGERPRINT_ERROR_BAD_CALIBRATION -> + R.string.security_settings_fingerprint_bad_calibration_title + else -> R.string.security_settings_fingerprint_enroll_error_unable_to_process_dialog_title + } + } else { + when (this) { + // This message happens when the underlying crypto layer + // decides to revoke the enrollment auth token + FingerprintManager.FINGERPRINT_ERROR_TIMEOUT -> + R.string.security_settings_fingerprint_enroll_error_timeout_dialog_message + FingerprintManager.FINGERPRINT_ERROR_BAD_CALIBRATION -> + R.string.security_settings_fingerprint_bad_calibration + FingerprintManager.FINGERPRINT_ERROR_UNABLE_TO_PROCESS -> + R.string.security_settings_fingerprint_enroll_error_unable_to_process_message + // There's nothing specific to tell the user about. Ask them to try again. + else -> R.string.security_settings_fingerprint_enroll_error_generic_dialog_message + } + } + + return FingerEnrollState.EnrollError( + errTitle, + errString, + this == FINGERPRINT_ERROR_UNABLE_TO_PROCESS, + this == FINGERPRINT_ERROR_CANCELED, + ) + } + } + diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt index 5c9232f4a96..984d04cb44e 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt @@ -24,12 +24,16 @@ import android.hardware.fingerprint.FingerprintManager.RemovalCallback import android.os.CancellationSignal import android.util.Log import com.android.settings.biometrics.GatekeeperPasswordProvider -import com.android.settings.biometrics.fingerprint2.conversion.toOriginalReason +import com.android.settings.biometrics.fingerprint2.conversion.Util.toEnrollError +import com.android.settings.biometrics.fingerprint2.conversion.Util.toOriginalReason +import com.android.settings.biometrics.fingerprint2.shared.data.repository.PressToAuthProvider import com.android.settings.biometrics.fingerprint2.shared.domain.interactor.FingerprintManagerInteractor import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason -import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollStateViewModel -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintFlow +import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData +import com.android.settings.biometrics.fingerprint2.shared.model.SetupWizard import com.android.settings.password.ChooseLockSettingsHelper import com.android.systemui.biometrics.shared.model.toFingerprintSensor import kotlin.coroutines.resume @@ -38,9 +42,12 @@ import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.onFailure +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @@ -51,7 +58,8 @@ class FingerprintManagerInteractorImpl( private val backgroundDispatcher: CoroutineDispatcher, private val fingerprintManager: FingerprintManager, private val gatekeeperPasswordProvider: GatekeeperPasswordProvider, - private val pressToAuthProvider: () -> Boolean, + private val pressToAuthProvider: PressToAuthProvider, + private val fingerprintFlow: FingerprintFlow, ) : FingerprintManagerInteractor { private val maxFingerprints = @@ -60,6 +68,8 @@ class FingerprintManagerInteractorImpl( ) private val applicationContext = applicationContext.applicationContext + private val enrollRequestOutstanding = MutableStateFlow(false) + override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair = suspendCoroutine { val callback = GenerateChallengeCallback { _, userId, challenge -> @@ -75,11 +85,11 @@ class FingerprintManagerInteractorImpl( fingerprintManager.generateChallenge(applicationContext.userId, callback) } - override val enrolledFingerprints: Flow> = flow { + override val enrolledFingerprints: Flow> = flow { emit( fingerprintManager .getEnrolledFingerprints(applicationContext.userId) - .map { (FingerprintViewModel(it.name.toString(), it.biometricId, it.deviceId)) } + .map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) } .toList() ) } @@ -103,28 +113,51 @@ class FingerprintManagerInteractorImpl( override suspend fun enroll( hardwareAuthToken: ByteArray?, enrollReason: EnrollReason, - ): Flow = callbackFlow { + ): Flow = callbackFlow { + // TODO (b/308456120) Improve this logic + if (enrollRequestOutstanding.value) { + Log.d(TAG, "Outstanding enroll request, waiting 150ms") + delay(150) + if (enrollRequestOutstanding.value) { + Log.e(TAG, "Request still present, continuing") + } + } + + enrollRequestOutstanding.update { true } + var streamEnded = false + var totalSteps: Int? = null val enrollmentCallback = object : FingerprintManager.EnrollmentCallback() { override fun onEnrollmentProgress(remaining: Int) { - trySend(FingerEnrollStateViewModel.EnrollProgress(remaining)).onFailure { error -> + // This is sort of an implementation detail, but unfortunately the API isn't + // very expressive. If anything we should look at changing the FingerprintManager API. + if (totalSteps == null) { + totalSteps = remaining + 1 + } + + trySend(FingerEnrollState.EnrollProgress(remaining, totalSteps!!)).onFailure { + error -> Log.d(TAG, "onEnrollmentProgress($remaining) failed to send, due to $error") } + if (remaining == 0) { streamEnded = true + enrollRequestOutstanding.update { false } } } override fun onEnrollmentHelp(helpMsgId: Int, helpString: CharSequence?) { - trySend(FingerEnrollStateViewModel.EnrollHelp(helpMsgId, helpString.toString())) + trySend(FingerEnrollState.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())) + trySend(errMsgId.toEnrollError(fingerprintFlow == SetupWizard)) .onFailure { error -> Log.d(TAG, "onEnrollmentError failed to send, due to $error") } + Log.d(TAG, "onEnrollmentError($errMsgId)") streamEnded = true + enrollRequestOutstanding.update { false } } } @@ -140,12 +173,13 @@ class FingerprintManagerInteractorImpl( // If the stream has not been ended, and the user has stopped collecting the flow // before it was over, send cancel. if (!streamEnded) { + Log.e(TAG, "Cancel is sent from settings for enroll()") cancellationSignal.cancel() } } } - override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean = suspendCoroutine { + override suspend fun removeFingerprint(fp: FingerprintData): Boolean = suspendCoroutine { val callback = object : RemovalCallback() { override fun onRemovalError( @@ -170,7 +204,7 @@ class FingerprintManagerInteractorImpl( ) } - override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) { + override suspend fun renameFingerprint(fp: FingerprintData, newName: String) { withContext(backgroundDispatcher) { fingerprintManager.rename(fp.fingerId, applicationContext.userId, newName) } @@ -181,11 +215,11 @@ class FingerprintManagerInteractorImpl( } override suspend fun pressToAuthEnabled(): Boolean = suspendCancellableCoroutine { - it.resume(pressToAuthProvider()) + it.resume(pressToAuthProvider.isEnabled) } - override suspend fun authenticate(): FingerprintAuthAttemptViewModel = - suspendCancellableCoroutine { c: CancellableContinuation -> + override suspend fun authenticate(): FingerprintAuthAttemptModel = + suspendCancellableCoroutine { c: CancellableContinuation -> val authenticationCallback = object : FingerprintManager.AuthenticationCallback() { @@ -195,7 +229,7 @@ class FingerprintManagerInteractorImpl( Log.d(TAG, "framework sent down onAuthError after finish") return } - c.resume(FingerprintAuthAttemptViewModel.Error(errorCode, errString.toString())) + c.resume(FingerprintAuthAttemptModel.Error(errorCode, errString.toString())) } override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) { @@ -204,7 +238,7 @@ class FingerprintManagerInteractorImpl( Log.d(TAG, "framework sent down onAuthError after finish") return } - c.resume(FingerprintAuthAttemptViewModel.Success(result.fingerprint?.biometricId ?: -1)) + c.resume(FingerprintAuthAttemptModel.Success(result.fingerprint?.biometricId ?: -1)) } } diff --git a/src/com/android/settings/biometrics/fingerprint2/repository/PressToAuthProviderImpl.kt b/src/com/android/settings/biometrics/fingerprint2/repository/PressToAuthProviderImpl.kt new file mode 100644 index 00000000000..38c5335ab0e --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/repository/PressToAuthProviderImpl.kt @@ -0,0 +1,51 @@ +/* + * 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.repository + +import android.content.Context +import android.provider.Settings +import com.android.settings.biometrics.fingerprint2.shared.data.repository.PressToAuthProvider + +class PressToAuthProviderImpl(val context: Context) : PressToAuthProvider { + override val isEnabled: Boolean + get() { + var toReturn: Int = + Settings.Secure.getIntForUser( + context.contentResolver, + Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED, + -1, + context.userId, + ) + if (toReturn == -1) { + toReturn = + if ( + context.resources.getBoolean(com.android.internal.R.bool.config_performantAuthDefault) + ) { + 1 + } else { + 0 + } + Settings.Secure.putIntForUser( + context.contentResolver, + Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED, + toReturn, + context.userId + ) + } + return (toReturn == 1) + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/data/repository/PressToAuthProvider.kt b/src/com/android/settings/biometrics/fingerprint2/shared/data/repository/PressToAuthProvider.kt new file mode 100644 index 00000000000..e776b9ab3e3 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/shared/data/repository/PressToAuthProvider.kt @@ -0,0 +1,27 @@ +/* + * 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.shared.data.repository + +/** + * Interface that indicates if press to auth is on or off. + */ +interface PressToAuthProvider { + /** + * Indicates true if the PressToAuth feature is enabled, false otherwise. + */ + val isEnabled: Boolean +} \ No newline at end of file diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/domain/interactor/FingerprintManagerInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/shared/domain/interactor/FingerprintManagerInteractor.kt index 72867155e5e..94afa49da99 100644 --- a/src/com/android/settings/biometrics/fingerprint2/shared/domain/interactor/FingerprintManagerInteractor.kt +++ b/src/com/android/settings/biometrics/fingerprint2/shared/domain/interactor/FingerprintManagerInteractor.kt @@ -17,9 +17,9 @@ package com.android.settings.biometrics.fingerprint2.shared.domain.interactor import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel -import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollStateViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData +import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState import com.android.systemui.biometrics.shared.model.FingerprintSensor import kotlinx.coroutines.flow.Flow @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.Flow */ interface FingerprintManagerInteractor { /** Returns the list of current fingerprints. */ - val enrolledFingerprints: Flow> + val enrolledFingerprints: Flow> /** Returns the max enrollable fingerprints, note during SUW this might be 1 */ val maxEnrollableFingerprints: Flow @@ -43,7 +43,7 @@ interface FingerprintManagerInteractor { val sensorPropertiesInternal: Flow /** Runs the authenticate flow */ - suspend fun authenticate(): FingerprintAuthAttemptViewModel + suspend fun authenticate(): FingerprintAuthAttemptModel /** * Generates a challenge with the provided [gateKeeperPasswordHandle] and on success returns a @@ -56,22 +56,22 @@ interface FingerprintManagerInteractor { /** * Runs [FingerprintManager.enroll] with the [hardwareAuthToken] and [EnrollReason] for this - * enrollment. Returning the [FingerEnrollStateViewModel] that represents this fingerprint + * enrollment. Returning the [FingerEnrollState] that represents this fingerprint * enrollment state. */ suspend fun enroll( - hardwareAuthToken: ByteArray?, - enrollReason: EnrollReason, - ): Flow + hardwareAuthToken: ByteArray?, + enrollReason: EnrollReason, + ): Flow /** * Removes the given fingerprint, returning true if it was successfully removed and false * otherwise */ - suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean + suspend fun removeFingerprint(fp: FingerprintData): Boolean /** Renames the given fingerprint if one exists */ - suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) + suspend fun renameFingerprint(fp: FingerprintData, newName: String) /** Indicates if the device has side fingerprint */ suspend fun hasSideFps(): Boolean diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/model/EnrollReasonViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/shared/model/EnrollReason.kt similarity index 100% rename from src/com/android/settings/biometrics/fingerprint2/shared/model/EnrollReasonViewModel.kt rename to src/com/android/settings/biometrics/fingerprint2/shared/model/EnrollReason.kt diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerEnrollStateViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerEnrollState.kt similarity index 73% rename from src/com/android/settings/biometrics/fingerprint2/shared/model/FingerEnrollStateViewModel.kt rename to src/com/android/settings/biometrics/fingerprint2/shared/model/FingerEnrollState.kt index 179ac603b72..4766d599814 100644 --- a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerEnrollStateViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerEnrollState.kt @@ -22,19 +22,28 @@ import android.annotation.StringRes * Represents a fingerprint enrollment state. See [FingerprintManager.EnrollmentCallback] for more * information */ -sealed class FingerEnrollStateViewModel { - /** Represents enrollment step progress. */ +sealed class FingerEnrollState { + /** + * Represents an enrollment step progress. + * + * Progress is obtained by (totalStepsRequired - remainingSteps) / totalStepsRequired + */ data class EnrollProgress( val remainingSteps: Int, - ) : FingerEnrollStateViewModel() + val totalStepsRequired: Int, + ) : FingerEnrollState() + /** Represents that recoverable error has been encountered during enrollment. */ data class EnrollHelp( @StringRes val helpMsgId: Int, val helpString: String, - ) : FingerEnrollStateViewModel() + ) : FingerEnrollState() + /** Represents that an unrecoverable error has been encountered and the operation is complete. */ data class EnrollError( - @StringRes val errMsgId: Int, - val errString: String, - ) : FingerEnrollStateViewModel() + @StringRes val errTitle: Int, + @StringRes val errString: Int, + val shouldRetryEnrollment: Boolean, + val isCancelled: Boolean, + ) : FingerEnrollState() } diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintData.kt similarity index 84% rename from src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt rename to src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintData.kt index db28e793533..b2aa25c56a8 100644 --- a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintData.kt @@ -16,19 +16,19 @@ package com.android.settings.biometrics.fingerprint2.shared.model -data class FingerprintViewModel( +data class FingerprintData( val name: String, val fingerId: Int, val deviceId: Long, ) -sealed class FingerprintAuthAttemptViewModel { +sealed class FingerprintAuthAttemptModel { data class Success( val fingerId: Int, - ) : FingerprintAuthAttemptViewModel() + ) : FingerprintAuthAttemptModel() data class Error( val error: Int, val message: String, - ) : FingerprintAuthAttemptViewModel() + ) : FingerprintAuthAttemptModel() } diff --git a/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintFlow.kt b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintFlow.kt new file mode 100644 index 00000000000..93c75770d63 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/shared/model/FingerprintFlow.kt @@ -0,0 +1,34 @@ +/* + * 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.shared.model + +/** + * The [FingerprintFlow] for fingerprint enrollment indicates information on how the flow should behave. + */ +sealed class FingerprintFlow + +/** The default enrollment experience, typically called from Settings */ +data object Default : FingerprintFlow() + +/** SetupWizard/Out of box experience (OOBE) enrollment type. */ +data object SetupWizard : FingerprintFlow() + +/** Unicorn enrollment type */ +data object Unicorn : FingerprintFlow() + +/** Flow to specify settings type */ +data object Settings : FingerprintFlow() 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 58fcea6961a..de2a1eef3c1 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 @@ -16,12 +16,9 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.activity -import android.annotation.ColorInt import android.app.Activity import android.content.Intent -import android.content.res.ColorStateList import android.content.res.Configuration -import android.graphics.Color import android.hardware.fingerprint.FingerprintManager import android.os.Bundle import android.provider.Settings @@ -35,22 +32,27 @@ import androidx.lifecycle.lifecycleScope import com.android.internal.widget.LockPatternUtils import com.android.settings.R import com.android.settings.SetupWizardUtils -import com.android.settings.Utils import com.android.settings.Utils.SETTINGS_PACKAGE_NAME import com.android.settings.biometrics.BiometricEnrollBase import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED import com.android.settings.biometrics.GatekeeperPasswordProvider import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.Default +import com.android.settings.biometrics.fingerprint2.shared.model.SetupWizard +import com.android.settings.biometrics.fingerprint2.repository.PressToAuthProviderImpl 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.FingerprintEnrollIntroV2Fragment +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.fragment.RFPSEnrollFragment +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.AccessibilityViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel 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.FingerprintEnrollEnrollingViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel @@ -65,8 +67,11 @@ import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Orie import com.android.settings.password.ChooseLockGeneric import com.android.settings.password.ChooseLockSettingsHelper import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.google.android.setupcompat.util.WizardManagerHelper import com.google.android.setupdesign.util.ThemeHelper import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -77,6 +82,7 @@ private const val TAG = "FingerprintEnrollmentV2Activity" * children fragments. */ class FingerprintEnrollmentV2Activity : FragmentActivity() { + private lateinit var fingerprintEnrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel private lateinit var navigationViewModel: FingerprintEnrollNavigationViewModel private lateinit var gatekeeperViewModel: FingerprintGatekeeperViewModel private lateinit var fingerprintEnrollViewModel: FingerprintEnrollViewModel @@ -84,6 +90,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { private lateinit var foldStateViewModel: FoldStateViewModel private lateinit var orientationStateViewModel: OrientationStateViewModel private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel + private lateinit var backgroundViewModel: BackgroundViewModel private val coroutineDispatcher = Dispatchers.Default /** Result listener for ChooseLock activity flow. */ @@ -101,23 +108,22 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { } } - override fun onAttachedToWindow() { - window.statusBarColor = getBackgroundColor() - super.onAttachedToWindow() + override fun onStop() { + super.onStop() + if (!isChangingConfigurations) { + backgroundViewModel.wentToBackground() + } } + override fun onResume() { + super.onResume() + backgroundViewModel.inForeground() + } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) foldStateViewModel.onConfigurationChange(newConfig) } - @ColorInt - private fun getBackgroundColor(): Int { - val stateList: ColorStateList? = - Utils.getColorAttr(applicationContext, android.R.attr.windowBackground) - return stateList?.defaultColor ?: Color.TRANSPARENT - } - private fun onConfirmDevice(resultCode: Int, data: Intent?) { val wasSuccessful = resultCode == RESULT_FINISHED || resultCode == Activity.RESULT_OK val gateKeeperPasswordHandle = data?.getExtra(EXTRA_KEY_GK_PW_HANDLE) as Long? @@ -137,39 +143,28 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { val context = applicationContext val fingerprintManager = context.getSystemService(FINGERPRINT_SERVICE) as FingerprintManager + val isAnySuw = WizardManagerHelper.isAnySetupWizard(intent) + val enrollType = + if (isAnySuw) { + SetupWizard + } else { + Default + } + + backgroundViewModel = + ViewModelProvider(this, BackgroundViewModel.BackgroundViewModelFactory())[ + BackgroundViewModel::class.java] + val interactor = FingerprintManagerInteractorImpl( context, backgroundDispatcher, fingerprintManager, - GatekeeperPasswordProvider(LockPatternUtils(context)) - ) { - var toReturn: Int = - Settings.Secure.getIntForUser( - context.contentResolver, - Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED, - -1, - context.userId, - ) - if (toReturn == -1) { - toReturn = - if ( - context.resources.getBoolean(com.android.internal.R.bool.config_performantAuthDefault) - ) { - 1 - } else { - 0 - } - Settings.Secure.putIntForUser( - context.contentResolver, - Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED, - toReturn, - context.userId - ) - } - toReturn == 1 - } + GatekeeperPasswordProvider(LockPatternUtils(context)), + PressToAuthProviderImpl(context), + enrollType, + ) var challenge: Long? = intent.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long? val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN) @@ -191,7 +186,8 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { backgroundDispatcher, interactor, gatekeeperViewModel, - gatekeeperInfo is GatekeeperInfo.GatekeeperPasswordInfo, /* canSkipConfirm */ + gatekeeperInfo is GatekeeperInfo.GatekeeperPasswordInfo, + enrollType, ) )[FingerprintEnrollNavigationViewModel::class.java] @@ -207,7 +203,8 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { this, FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory( interactor, - backgroundDispatcher + gatekeeperViewModel, + navigationViewModel, ) )[FingerprintEnrollViewModel::class.java] @@ -230,6 +227,16 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { ViewModelProvider(this, OrientationStateViewModel.OrientationViewModelFactory(context))[ OrientationStateViewModel::class.java] + // Initialize FingerprintEnrollEnrollingViewModel + fingerprintEnrollEnrollingViewModel = + ViewModelProvider( + this, + FingerprintEnrollEnrollingViewModel.FingerprintEnrollEnrollingViewModelFactory( + fingerprintEnrollViewModel, + backgroundViewModel + ) + )[FingerprintEnrollEnrollingViewModel::class.java] + // Initialize FingerprintEnrollFindSensorViewModel ViewModelProvider( this, @@ -237,48 +244,65 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { navigationViewModel, fingerprintEnrollViewModel, gatekeeperViewModel, + backgroundViewModel, accessibilityViewModel, foldStateViewModel, orientationStateViewModel ) )[FingerprintEnrollFindSensorViewModel::class.java] + // Initialize RFPS View Model + ViewModelProvider( + this, + RFPSViewModel.RFPSViewModelFactory(fingerprintEnrollEnrollingViewModel) + )[RFPSViewModel::class.java] + lifecycleScope.launch { - navigationViewModel.navigationViewModel.filterNotNull().collect { - Log.d(TAG, "navigationStep $it") - val isForward = it.forward - val currStep = it.currStep - val theClass: Class? = - when (currStep) { - Confirmation -> FingerprintEnrollConfirmationV2Fragment::class.java as Class - Education -> FingerprintEnrollFindSensorV2Fragment::class.java as Class - Enrollment -> FingerprintEnrollEnrollingV2Fragment::class.java as Class - Intro -> FingerprintEnrollIntroV2Fragment::class.java as Class - else -> null - } - - if (theClass != null) { - supportFragmentManager.fragments.onEach { fragment -> - supportFragmentManager.beginTransaction().remove(fragment).commit() - } - supportFragmentManager - .beginTransaction() - .setReorderingAllowed(true) - .add(R.id.fragment_container_view, theClass, null) - .commit() - } else { - - if (currStep is Finish) { - if (currStep.resultCode != null) { - finishActivity(currStep.resultCode) - } else { - finish() + navigationViewModel.navigationViewModel + .filterNotNull() + .combine(fingerprintEnrollViewModel.sensorType) { nav, sensorType -> Pair(nav, sensorType) } + .collect { (nav, sensorType) -> + Log.d(TAG, "navigationStep $nav") + fingerprintEnrollViewModel.sensorTypeCached = sensorType + val isForward = nav.forward + val currStep = nav.currStep + val theClass: Class? = + when (currStep) { + Confirmation -> FingerprintEnrollConfirmationV2Fragment::class.java as Class + Education -> FingerprintEnrollFindSensorV2Fragment::class.java as Class + is Enrollment -> { + when (sensorType) { + FingerprintSensorType.REAR -> RFPSEnrollFragment::class.java as Class + else -> FingerprintEnrollEnrollingV2Fragment::class.java as Class + } + } + Intro -> FingerprintEnrollIntroV2Fragment::class.java as Class + else -> null + } + + if (theClass != null) { + supportFragmentManager.fragments.onEach { fragment -> + supportFragmentManager.beginTransaction().remove(fragment).commit() + } + + supportFragmentManager + .beginTransaction() + .setReorderingAllowed(true) + .add(R.id.fragment_container_view, theClass, null) + .commit() + } else { + + if (currStep is Finish) { + if (currStep.resultCode != null) { + finishActivity(currStep.resultCode) + } else { + finish() + } + } else if (currStep == LaunchConfirmDeviceCredential) { + launchConfirmOrChooseLock(userId) } - } else if (currStep == LaunchConfirmDeviceCredential) { - launchConfirmOrChooseLock(userId) } } - } } val fromSettingsSummary = 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 0afa6134e2e..bfd426435a3 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 @@ -30,6 +30,7 @@ import com.android.settings.R import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog import com.android.settings.biometrics.fingerprint.FingerprintFindSensorAnimation import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.google.android.setupcompat.template.FooterBarMixin import com.google.android.setupcompat.template.FooterButton @@ -54,23 +55,8 @@ class FingerprintEnrollFindSensorV2Fragment : Fragment() { private var animation: FingerprintFindSensorAnimation? = null private var contentLayoutId: Int = -1 - private lateinit var viewModel: FingerprintEnrollFindSensorViewModel - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - viewModel = - ViewModelProvider(requireActivity())[FingerprintEnrollFindSensorViewModel::class.java] - lifecycleScope.launch { - viewModel.sensorType.collect { - contentLayoutId = - when (it) { - FingerprintSensorType.UDFPS_OPTICAL, - FingerprintSensorType.UDFPS_ULTRASONIC -> R.layout.udfps_enroll_find_sensor_layout - FingerprintSensorType.POWER_BUTTON -> R.layout.sfps_enroll_find_sensor_layout - else -> R.layout.fingerprint_v2_enroll_find_sensor - } - } - } + private val viewModel: FingerprintEnrollFindSensorViewModel by lazy { + ViewModelProvider(requireActivity())[FingerprintEnrollFindSensorViewModel::class.java] } override fun onCreateView( @@ -78,6 +64,18 @@ class FingerprintEnrollFindSensorV2Fragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View? { + + val sensorType = + ViewModelProvider(requireActivity())[FingerprintEnrollViewModel::class.java].sensorTypeCached + + contentLayoutId = + when (sensorType) { + FingerprintSensorType.UDFPS_OPTICAL, + FingerprintSensorType.UDFPS_ULTRASONIC -> R.layout.udfps_enroll_find_sensor_layout + FingerprintSensorType.POWER_BUTTON -> R.layout.sfps_enroll_find_sensor_layout + else -> R.layout.fingerprint_v2_enroll_find_sensor + } + return inflater.inflate(contentLayoutId, container, false).also { it -> val view = it!! as GlifLayout @@ -106,7 +104,8 @@ class FingerprintEnrollFindSensorV2Fragment : Fragment() { } lifecycleScope.launch { viewModel.showRfpsAnimation.collect { - animation = view.findViewById(R.id.fingerprint_sensor_location_animation) + animation = + view.findViewById(R.id.fingerprint_sensor_location_animation) animation!!.startAnimation() } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt index 898b158aa25..b1ab3014355 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt @@ -36,11 +36,11 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.android.settings.R +import com.android.settings.biometrics.fingerprint2.shared.model.Unicorn 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.FingerprintGatekeeperViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel -import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Unicorn import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.google.android.setupcompat.template.FooterBarMixin import com.google.android.setupcompat.template.FooterButton @@ -120,7 +120,7 @@ class FingerprintEnrollIntroV2Fragment() : Fragment(R.layout.fingerprint_v2_enro viewLifecycleOwner.lifecycleScope.launch { combine( - navigationViewModel.enrollType, + navigationViewModel.fingerprintFlow, fingerprintViewModel.sensorType, ) { enrollType, sensorType -> Pair(enrollType, sensorType) diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/README.md b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/README.md new file mode 100644 index 00000000000..dfb95982000 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/README.md @@ -0,0 +1,26 @@ +# Module enrollment + +### Fingerprint Settings Enrollment Modules + +This directory is responsible for containing the enrollment modules, each enrollment module is +responsible for the actual enrolling portion of FingerprintEnrollment. +The modules should be split out into udfps, rfps, and sfps. + +[comment]: <> This file structure print out has been generated with the tree command. + +``` +├── enrolling +│   └── rfps +│   ├── data +│   ├── domain +│   │   └── RFPSInteractor.kt +│   ├── README.md +│   └── ui +│   ├── fragment +│   │   └── RFPSEnrollFragment.kt +│   ├── viewmodel +│   │   └── RFPSViewModel.kt +│   └── widget +│   └── RFPSProgressIndicator.kt +└── README.md +``` \ No newline at end of file diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt new file mode 100644 index 00000000000..d8c2f5a2a03 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt @@ -0,0 +1,252 @@ +/* + * 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.modules.enrolling.rfps.ui.fragment + +import android.graphics.Color +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.animation.AnimationUtils +import android.view.animation.Interpolator +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.settings.R +import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSIconTouchViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.FingerprintErrorDialog +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.IconTouchDialog +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.RFPSProgressBar +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.OrientationStateViewModel +import com.android.settings.core.instrumentation.InstrumentedDialogFragment +import com.google.android.setupcompat.template.FooterBarMixin +import com.google.android.setupcompat.template.FooterButton +import com.google.android.setupdesign.GlifLayout +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch + +private const val TAG = "RFPSEnrollFragment" + +/** This fragment is responsible for taking care of rear fingerprint enrollment. */ +class RFPSEnrollFragment : Fragment(R.layout.fingerprint_v2_rfps_enroll_enrolling) { + + private lateinit var linearOutSlowInInterpolator: Interpolator + private lateinit var fastOutLinearInInterpolator: Interpolator + private lateinit var textView: TextView + private lateinit var progressBar: RFPSProgressBar + + private val iconTouchViewModel: RFPSIconTouchViewModel by lazy { + ViewModelProvider(requireActivity())[RFPSIconTouchViewModel::class.java] + } + + private val orientationViewModel: OrientationStateViewModel by lazy { + ViewModelProvider(requireActivity())[OrientationStateViewModel::class.java] + } + + private val rfpsViewModel: RFPSViewModel by lazy { + ViewModelProvider(requireActivity())[RFPSViewModel::class.java] + } + + private val backgroundViewModel: BackgroundViewModel by lazy { + ViewModelProvider(requireActivity())[BackgroundViewModel::class.java] + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = super.onCreateView(inflater, container, savedInstanceState)!! + val fragment = this + val context = requireContext() + val glifLayout = view.requireViewById(R.id.setup_wizard_layout) as GlifLayout + glifLayout.setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message) + glifLayout.setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title) + + fastOutLinearInInterpolator = + AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_linear_in) + linearOutSlowInInterpolator = + AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in) + + textView = view.requireViewById(R.id.text) as TextView + progressBar = view.requireViewById(R.id.fingerprint_progress_bar) as RFPSProgressBar + + val footerBarMixin = glifLayout.getMixin(FooterBarMixin::class.java) + footerBarMixin.secondaryButton = + FooterButton.Builder(context) + .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) + .setListener { Log.e(TAG, "skip enrollment!") } + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary) + .build() + footerBarMixin.buttonContainer.setBackgroundColor(Color.TRANSPARENT) + + progressBar.setOnTouchListener { _, motionEvent -> + if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { + iconTouchViewModel.userTouchedFingerprintIcon() + } + true + } + + // On any orientation event, dismiss dialogs. + viewLifecycleOwner.lifecycleScope.launch { + orientationViewModel.orientation.collect { dismissDialogs() } + } + + // Signal we are ready for enrollment. + rfpsViewModel.readyForEnrollment() + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + // Icon animation update + viewLifecycleOwner.lifecycleScope.launch { + rfpsViewModel.shouldAnimateIcon.collect { animate -> + progressBar.updateIconAnimation(animate) + } + } + + // Flow to show a dialog. + viewLifecycleOwner.lifecycleScope.launch { + iconTouchViewModel.shouldShowDialog.collectLatest { showDialog -> + if (showDialog) { + try { + IconTouchDialog.showInstance(fragment) + } catch (exception: Exception) { + Log.d(TAG, "Dialog dismissed due to $exception") + } + } + } + } + } + } + + // If we go to the background, then finish enrollment. This should be permanent finish, + // and shouldn't be reset until we explicitly tell the view model we want to retry + // enrollment. + viewLifecycleOwner.lifecycleScope.launch { + backgroundViewModel.background + .filter { inBackground -> inBackground } + .collect { rfpsViewModel.stopEnrollment() } + } + + viewLifecycleOwner.lifecycleScope.launch { + rfpsViewModel.progress.filterNotNull().collect { progress -> handleEnrollProgress(progress) } + } + + viewLifecycleOwner.lifecycleScope.launch { + rfpsViewModel.helpMessage.filterNotNull().collect { help -> + textView.text = help.helpString + textView.visibility = View.VISIBLE + textView.translationY = + resources.getDimensionPixelSize(R.dimen.fingerprint_error_text_appear_distance).toFloat() + textView.alpha = 0f + textView + .animate() + .alpha(1f) + .translationY(0f) + .setDuration(200) + .setInterpolator(linearOutSlowInInterpolator) + .start() + + } + } + + viewLifecycleOwner.lifecycleScope.launch { + rfpsViewModel.errorMessage.filterNotNull().collect { error -> handleEnrollError(error) } + } + viewLifecycleOwner.lifecycleScope.launch { + rfpsViewModel.textViewIsVisible.collect { + textView.visibility = if (it) View.VISIBLE else View.INVISIBLE + } + } + + viewLifecycleOwner.lifecycleScope.launch { + rfpsViewModel.clearHelpMessage.collect { + textView + .animate() + .alpha(0f) + .translationY( + resources + .getDimensionPixelSize(R.dimen.fingerprint_error_text_disappear_distance) + .toFloat() + ) + .setDuration(100) + .setInterpolator(fastOutLinearInInterpolator) + .withEndAction { rfpsViewModel.setVisibility(false) } + .start() + } + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.DESTROYED) { + rfpsViewModel.stopEnrollment() + dismissDialogs() + } + } + return view + } + + private fun handleEnrollError(error: FingerEnrollState.EnrollError) { + val fragment = this + viewLifecycleOwner.lifecycleScope.launch { + try { + val shouldRestartEnrollment = FingerprintErrorDialog.showInstance(error, fragment) + } catch (exception: Exception) { + Log.e(TAG, "Exception occurred $exception") + } + onEnrollmentFailed() + } + } + + private fun onEnrollmentFailed() { + rfpsViewModel.stopEnrollment() + } + + private fun handleEnrollProgress(progress: FingerEnrollState.EnrollProgress) { + progressBar.updateProgress( + progress.remainingSteps.toFloat() / progress.totalStepsRequired.toFloat() + ) + + if (progress.remainingSteps == 0) { + performNextStepSuccess() + } + } + + private fun performNextStepSuccess() {} + + private fun dismissDialogs() { + val transaction = parentFragmentManager.beginTransaction() + for (frag in parentFragmentManager.fragments) { + if (frag is InstrumentedDialogFragment) { + Log.d(TAG, "removing dialog settings fragment $frag") + frag.dismiss() + transaction.remove(frag) + } + } + transaction.commitAllowingStateLoss() + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSIconTouchViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSIconTouchViewModel.kt new file mode 100644 index 00000000000..c16e65cc9f2 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSIconTouchViewModel.kt @@ -0,0 +1,61 @@ +/* + * 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.modules.enrolling.rfps.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.flow.update + +private const val touchesToShowDialog = 3 +/** + * This class is responsible for counting the number of touches on the fingerprint icon, and if this + * number reaches a threshold it will produce an action via [shouldShowDialog] to indicate the ui + * should show a dialog. + */ +class RFPSIconTouchViewModel : ViewModel() { + + /** Keeps the number of times a user has touches the fingerprint icon. */ + private val _touches: MutableStateFlow = MutableStateFlow(0) + + /** + * Whether or not the UI should be showing the dialog. By making this SharingStarted.Eagerly + * the first event 0 % 3 == 0 will fire as soon as this view model is created, so it should + * be ignored and work as intended. + */ + val shouldShowDialog: Flow = + _touches + .transform { numTouches -> emit((numTouches % touchesToShowDialog) == 0) } + .shareIn(viewModelScope, SharingStarted.Eagerly, 0) + + /** Indicates a user has tapped on the fingerprint icon. */ + fun userTouchedFingerprintIcon() { + _touches.update { _touches.value + 1 } + } + + class RFPSIconTouchViewModelFactory : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return RFPSIconTouchViewModel() as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt new file mode 100644 index 00000000000..58d604e4407 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt @@ -0,0 +1,102 @@ +/* + * 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.modules.enrolling.rfps.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.flow.update + +/** View Model used by the rear fingerprint enrollment fragment. */ +class RFPSViewModel( + private val fingerprintEnrollViewModel: FingerprintEnrollEnrollingViewModel, +) : ViewModel() { + + /** Value to indicate if the text view is visible or not **/ + private val _textViewIsVisible = MutableStateFlow(false) + val textViewIsVisible: Flow = _textViewIsVisible.asStateFlow() + + /** Indicates if the icon should be animating or not */ + val shouldAnimateIcon = fingerprintEnrollViewModel.enrollFlowShouldBeRunning + + private val enrollFlow: Flow = fingerprintEnrollViewModel.enrollFLow + + /** + * Enroll progress message with a replay of size 1 allowing for new subscribers to get the most + * recent state (this is useful for things like screen rotation) + */ + val progress: Flow = + enrollFlow + .filterIsInstance() + .shareIn(viewModelScope, SharingStarted.Eagerly, 1) + + /** Clear help message on enroll progress */ + val clearHelpMessage: Flow = progress.map { it != null } + + /** Enroll help message that is only displayed once */ + val helpMessage: Flow = + enrollFlow + .filterIsInstance() + .shareIn(viewModelScope, SharingStarted.Eagerly, 0).transform { + _textViewIsVisible.update { true } + } + + /** + * The error message should only be shown once, for scenarios like screen rotations, we don't want + * to re-show the error message. + */ + val errorMessage: Flow = + enrollFlow + .filterIsInstance() + .shareIn(viewModelScope, SharingStarted.Eagerly, 0) + + /** Indicates if the consumer is ready for enrollment */ + fun readyForEnrollment() { + fingerprintEnrollViewModel.canEnroll() + } + + /** Indicates if enrollment should stop */ + fun stopEnrollment() { + fingerprintEnrollViewModel.stopEnroll() + } + + fun setVisibility(isVisible: Boolean) { + _textViewIsVisible.update { isVisible } + } + + class RFPSViewModelFactory( + private val fingerprintEnrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel, + ) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + ): T { + return RFPSViewModel(fingerprintEnrollEnrollingViewModel) as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/FingerprintErrorDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/FingerprintErrorDialog.kt new file mode 100644 index 00000000000..b9c628ed412 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/FingerprintErrorDialog.kt @@ -0,0 +1,124 @@ +/* + * 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.modules.enrolling.rfps.ui.widget + +import android.app.AlertDialog +import android.app.Dialog +import android.app.settings.SettingsEnums +import android.content.DialogInterface +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import com.android.settings.R +import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState +import com.android.settings.core.instrumentation.InstrumentedDialogFragment +import kotlin.coroutines.resume +import kotlinx.coroutines.suspendCancellableCoroutine + +private const val TAG = "FingerprintErrorDialog" + +/** A Dialog used for fingerprint enrollment when an error occurs. */ +class FingerprintErrorDialog : InstrumentedDialogFragment() { + private lateinit var onContinue: DialogInterface.OnClickListener + private lateinit var onTryAgain: DialogInterface.OnClickListener + private lateinit var onCancelListener: DialogInterface.OnCancelListener + + override fun onCancel(dialog: DialogInterface) { + Log.d(TAG, "onCancel $dialog") + onCancelListener.onCancel(dialog) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + Log.d(TAG, "onCreateDialog $this") + val errorString = requireArguments().getInt(KEY_MESSAGE) + val errorTitle = requireArguments().getInt(KEY_TITLE) + val builder = AlertDialog.Builder(requireContext()) + val shouldShowTryAgain = requireArguments().getBoolean(KEY_SHOULD_TRY_AGAIN) + builder.setTitle(errorTitle).setMessage(errorString).setCancelable(false) + + if (shouldShowTryAgain) { + builder + .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_try_again) { + dialog, + which -> + dialog.dismiss() + onTryAgain.onClick(dialog, which) + } + .setNegativeButton(R.string.security_settings_fingerprint_enroll_dialog_ok) { dialog, which + -> + dialog.dismiss() + onContinue.onClick(dialog, which) + } + } else { + builder.setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok) { + dialog, + which -> + dialog.dismiss() + onContinue.onClick(dialog, which) + } + } + + val dialog = builder.create() + dialog.setCanceledOnTouchOutside(false) + return dialog + } + + override fun getMetricsCategory(): Int { + return SettingsEnums.DIALOG_FINGERPINT_ERROR + } + + companion object { + private const val KEY_MESSAGE = "fingerprint_message" + private const val KEY_TITLE = "fingerprint_title" + private const val KEY_SHOULD_TRY_AGAIN = "should_try_again" + + suspend fun showInstance( + error: FingerEnrollState.EnrollError, + fragment: Fragment, + ) = suspendCancellableCoroutine { continuation -> + val dialog = FingerprintErrorDialog() + dialog.onTryAgain = DialogInterface.OnClickListener { _, _ -> continuation.resume(true) } + + dialog.onContinue = DialogInterface.OnClickListener { _, _ -> continuation.resume(false) } + + dialog.onCancelListener = + DialogInterface.OnCancelListener { + Log.d(TAG, "onCancelListener clicked $dialog") + continuation.resume(null) + } + + continuation.invokeOnCancellation { Log.d(TAG, "invokeOnCancellation $dialog") } + + val bundle = Bundle() + bundle.putInt( + KEY_TITLE, + error.errTitle, + ) + bundle.putInt( + KEY_MESSAGE, + error.errString, + ) + bundle.putBoolean( + KEY_SHOULD_TRY_AGAIN, + error.shouldRetryEnrollment, + ) + dialog.arguments = bundle + Log.d(TAG, "showing dialog $dialog") + dialog.show(fragment.parentFragmentManager, FingerprintErrorDialog::class.java.toString()) + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/IconTouchDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/IconTouchDialog.kt new file mode 100644 index 00000000000..c0863432afc --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/IconTouchDialog.kt @@ -0,0 +1,73 @@ +/* + * 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.modules.enrolling.rfps.ui.widget + +import android.app.AlertDialog +import android.app.Dialog +import android.app.settings.SettingsEnums +import android.content.DialogInterface +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import com.android.settings.R +import com.android.settings.core.instrumentation.InstrumentedDialogFragment +import kotlin.coroutines.resume +import kotlinx.coroutines.suspendCancellableCoroutine + +private const val TAG = "IconTouchDialog" + +/** Dialog shown when the user taps the Progress bar a certain amount of times. */ +class IconTouchDialog : InstrumentedDialogFragment() { + lateinit var onDismissListener: DialogInterface.OnClickListener + lateinit var onCancelListener: DialogInterface.OnCancelListener + + override fun onCancel(dialog: DialogInterface) { + Log.d(TAG, "onCancel $dialog") + onCancelListener.onCancel(dialog) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val builder: AlertDialog.Builder = AlertDialog.Builder(activity, R.style.Theme_AlertDialog) + builder + .setTitle(R.string.security_settings_fingerprint_enroll_touch_dialog_title) + .setMessage(R.string.security_settings_fingerprint_enroll_touch_dialog_message) + .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok) { dialog, which -> + dialog.dismiss() + onDismissListener.onClick(dialog, which) + } + .setOnCancelListener { onCancelListener.onCancel(it) } + return builder.create() + } + + override fun getMetricsCategory(): Int { + return SettingsEnums.DIALOG_FINGERPRINT_ICON_TOUCH + } + + companion object { + suspend fun showInstance(fragment: Fragment) = suspendCancellableCoroutine { continuation -> + val dialog = IconTouchDialog() + dialog.onDismissListener = + DialogInterface.OnClickListener { _, _ -> continuation.resume("Done") } + dialog.onCancelListener = + DialogInterface.OnCancelListener { _ -> continuation.resume("OnCancel") } + + continuation.invokeOnCancellation { Log.d(TAG, "invokeOnCancellation $dialog") } + + dialog.show(fragment.parentFragmentManager, IconTouchDialog::class.java.toString()) + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/RFPSProgressBar.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/RFPSProgressBar.kt new file mode 100644 index 00000000000..fe6268107d3 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/RFPSProgressBar.kt @@ -0,0 +1,107 @@ +/* + * 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.modules.enrolling.rfps.ui.widget + +import android.animation.ObjectAnimator +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.drawable.Animatable2 +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.util.AttributeSet +import android.view.animation.AnimationUtils +import android.view.animation.Interpolator +import com.android.settings.R +import com.android.settings.widget.RingProgressBar + +/** Progress bar for rear fingerprint enrollment. */ +class RFPSProgressBar(context: Context, attributeSet: AttributeSet) : + RingProgressBar(context, attributeSet) { + + private val fastOutSlowInInterpolator: Interpolator + + private val iconAnimationDrawable: AnimatedVectorDrawable + private val iconBackgroundBlinksDrawable: AnimatedVectorDrawable + + private val maxProgress: Int + + private var progressAnimation: ObjectAnimator? = null + + private var shouldAnimateInternal: Boolean = true + + init { + val fingerprintDrawable = background as LayerDrawable + iconAnimationDrawable = + fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation) + as AnimatedVectorDrawable + iconBackgroundBlinksDrawable = + fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background) + as AnimatedVectorDrawable + + fastOutSlowInInterpolator = + AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in) + + iconAnimationDrawable.registerAnimationCallback( + object : Animatable2.AnimationCallback() { + override fun onAnimationEnd(drawable: Drawable?) { + super.onAnimationEnd(drawable) + if (shouldAnimateInternal) { + animateIconAnimationInternal() + } + } + } + ) + animateIconAnimationInternal() + + progressBackgroundTintMode = PorterDuff.Mode.SRC + + val attributes = + context.obtainStyledAttributes(R.style.RingProgressBarStyle, intArrayOf(android.R.attr.max)) + + maxProgress = attributes.getInt(0, -1) + + attributes.recycle() + } + + /** Indicates if the progress animation should be running */ + fun updateIconAnimation(shouldAnimate: Boolean) { + if (shouldAnimate && !shouldAnimateInternal) { + animateIconAnimationInternal() + } + + shouldAnimateInternal = shouldAnimate + } + + /** This function should only be called when actual progress has been made. */ + fun updateProgress(percentComplete: Float) { + val progress = maxProgress - (percentComplete.coerceIn(0.0f, 100.0f) * maxProgress).toInt() + iconBackgroundBlinksDrawable.start() + + progressAnimation?.isRunning?.let { progressAnimation!!.cancel() } + + progressAnimation = ObjectAnimator.ofInt(this, "progress", getProgress(), progress) + + progressAnimation?.interpolator = fastOutSlowInInterpolator + progressAnimation?.setDuration(250) + progressAnimation?.start() + } + + private fun animateIconAnimationInternal() { + iconAnimationDrawable.start() + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/BackgroundViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/BackgroundViewModel.kt new file mode 100644 index 00000000000..2b53a530f4b --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/BackgroundViewModel.kt @@ -0,0 +1,48 @@ +/* + * 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 kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +/** A class for determining if the application is in the background or not. */ +class BackgroundViewModel : ViewModel() { + + private val _background = MutableStateFlow(false) + /** When true, the application is in background, else false */ + val background = _background.asStateFlow() + + /** Indicates that the application has been put in the background. */ + fun wentToBackground() { + _background.update { true } + } + + /** Indicates that the application has been brought to the foreground. */ + fun inForeground() { + _background.update { false } + } + + class BackgroundViewModelFactory : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return BackgroundViewModel() as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt new file mode 100644 index 00000000000..7ab315e7201 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt @@ -0,0 +1,85 @@ +/* + * 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 kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.flow.update + +/** + * This class is a wrapper around the [FingerprintEnrollViewModel] and decides when + * the user should or should not be enrolling. + */ +class FingerprintEnrollEnrollingViewModel( + private val fingerprintEnrollViewModel: FingerprintEnrollViewModel, + backgroundViewModel: BackgroundViewModel, +) : ViewModel() { + + private val _didTryEnrollment = MutableStateFlow(false) + private val _userDidEnroll = MutableStateFlow(false) + /** Indicates if the enrollment flow should be running. */ + val enrollFlowShouldBeRunning: Flow = + _userDidEnroll.combine(backgroundViewModel.background) { shouldEnroll, isInBackground -> + if (isInBackground) { + false + } else { + shouldEnroll + } + } + + /** + * Used to indicate the consumer of the view model is ready for an enrollment. Note that this does + * not necessarily try an enrollment. + */ + fun canEnroll() { + // Update _consumerShouldEnroll after updating the other values. + if (!_didTryEnrollment.value) { + _didTryEnrollment.update { true } + _userDidEnroll.update { true } + } + } + + /** Used to indicate to stop the enrollment. */ + fun stopEnroll() { + _userDidEnroll.update { false } + } + + /** Collects the enrollment flow based on [enrollFlowShouldBeRunning] */ + val enrollFLow = + enrollFlowShouldBeRunning.transformLatest { + if (it) { + fingerprintEnrollViewModel.enrollFlow.collect { event -> emit(event) } + } + } + + class FingerprintEnrollEnrollingViewModelFactory( + private val fingerprintEnrollViewModel: FingerprintEnrollViewModel, + private val backgroundViewModel: BackgroundViewModel + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + ): T { + return FingerprintEnrollEnrollingViewModel(fingerprintEnrollViewModel, backgroundViewModel) + as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt index 90aefc81085..7722a46fc7c 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt @@ -16,12 +16,11 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel -import android.hardware.fingerprint.FingerprintManager import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason -import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollStateViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState +import com.android.settings.biometrics.fingerprint2.shared.model.SetupWizard import com.android.systemui.biometrics.shared.model.FingerprintSensorType import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -40,6 +39,7 @@ class FingerprintEnrollFindSensorViewModel( private val navigationViewModel: FingerprintEnrollNavigationViewModel, private val fingerprintEnrollViewModel: FingerprintEnrollViewModel, private val gatekeeperViewModel: FingerprintGatekeeperViewModel, + backgroundViewModel: BackgroundViewModel, accessibilityViewModel: AccessibilityViewModel, foldStateViewModel: FoldStateViewModel, orientationStateViewModel: OrientationStateViewModel @@ -88,6 +88,14 @@ class FingerprintEnrollFindSensorViewModel( /** Represents the stream of showing error dialog. */ val showErrorDialog = _showErrorDialog.filterNotNull() + private var _didTryEducation = false + private var _education: MutableStateFlow = MutableStateFlow(false) + /** Indicates if the education flow should be running. */ + private val educationFlowShouldBeRunning: Flow = + _education.combine(backgroundViewModel.background) { shouldRunEducation, isInBackground -> + !isInBackground && shouldRunEducation + } + init { // Start or end enroll flow viewModelScope.launch { @@ -107,40 +115,58 @@ class FingerprintEnrollFindSensorViewModel( } .collect { token -> if (token != null) { - fingerprintEnrollViewModel.startEnroll(token, EnrollReason.FindSensor) + canStartEducation() } else { - fingerprintEnrollViewModel.stopEnroll() + stopEducation() } } } // Enroll progress flow viewModelScope.launch { - combine( - navigationViewModel.enrollType, - fingerprintEnrollViewModel.enrollFlow.filterNotNull() - ) { enrollType, enrollFlow -> - Pair(enrollType, enrollFlow) - } - .collect { (enrollType, enrollFlow) -> - when (enrollFlow) { - // TODO: Cancel the enroll() when EnrollProgress is received instead of proceeding to - // Enrolling page. Otherwise Enrolling page will receive the EnrollError. - is FingerEnrollStateViewModel.EnrollProgress -> proceedToEnrolling() - is FingerEnrollStateViewModel.EnrollError -> { - val errMsgId = enrollFlow.errMsgId - if (errMsgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) { - proceedToEnrolling() - } else { - _showErrorDialog.update { Pair(errMsgId, enrollType == SetupWizard) } + educationFlowShouldBeRunning.collect { + // Only collect the flow when we should be running. + if (it) { + combine( + navigationViewModel.fingerprintFlow, + fingerprintEnrollViewModel.educationEnrollFlow.filterNotNull(), + ) { enrollType, educationFlow -> + Pair(enrollType, educationFlow) + } + .collect { (enrollType, educationFlow) -> + when (educationFlow) { + // TODO: Cancel the enroll() when EnrollProgress is received instead of proceeding + // to + // Enrolling page. Otherwise Enrolling page will receive the EnrollError. + is FingerEnrollState.EnrollProgress -> proceedToEnrolling() + is FingerEnrollState.EnrollError -> { + if (educationFlow.isCancelled) { + proceedToEnrolling() + } else { + _showErrorDialog.update { Pair(educationFlow.errString, enrollType == SetupWizard) } + } + } + is FingerEnrollState.EnrollHelp -> {} } } - is FingerEnrollStateViewModel.EnrollHelp -> {} - } } + } } } + /** Indicates if education can begin */ + private fun canStartEducation() { + if (!_didTryEducation) { + _didTryEducation = true + _education.update { true } + } + } + + /** Indicates that education has finished */ + private fun stopEducation() { + _education.update { false } + } + /** Proceed to EnrollEnrolling page. */ fun proceedToEnrolling() { navigationViewModel.nextStep() @@ -150,6 +176,7 @@ class FingerprintEnrollFindSensorViewModel( private val navigationViewModel: FingerprintEnrollNavigationViewModel, private val fingerprintEnrollViewModel: FingerprintEnrollViewModel, private val gatekeeperViewModel: FingerprintGatekeeperViewModel, + private val backgroundViewModel: BackgroundViewModel, private val accessibilityViewModel: AccessibilityViewModel, private val foldStateViewModel: FoldStateViewModel, private val orientationStateViewModel: OrientationStateViewModel @@ -160,6 +187,7 @@ class FingerprintEnrollFindSensorViewModel( navigationViewModel, fingerprintEnrollViewModel, gatekeeperViewModel, + backgroundViewModel, accessibilityViewModel, foldStateViewModel, orientationStateViewModel 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 392d205d203..c7a1071c6a6 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 @@ -17,32 +17,41 @@ 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.shared.domain.interactor.FingerprintManagerInteractor import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason -import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollStateViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState import com.android.systemui.biometrics.shared.model.FingerprintSensorType -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.transformLatest -import kotlinx.coroutines.flow.update - -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, + gatekeeperViewModel: FingerprintGatekeeperViewModel, + navigationViewModel: FingerprintEnrollNavigationViewModel, ) : ViewModel() { - private var _enrollReason: MutableStateFlow = - MutableStateFlow(EnrollReason.FindSensor) - private var _hardwareAuthToken: MutableStateFlow = MutableStateFlow(null) - private var _consumerShouldEnroll: MutableStateFlow = MutableStateFlow(false) + /** + * Cached value of [FingerprintSensorType] + * + * This is typically used by fragments that change their layout/behavior based on this + * information. This value should be set before any fragment is created. + */ + var sensorTypeCached: FingerprintSensorType? = null + private var _enrollReason: Flow = + navigationViewModel.navigationViewModel.map { + when (it.currStep) { + is Enrollment -> EnrollReason.EnrollEnrolling + is Education -> EnrollReason.FindSensor + else -> null + } + } /** Represents the stream of [FingerprintSensorType] */ val sensorType: Flow = @@ -51,47 +60,68 @@ class FingerprintEnrollViewModel( /** * A flow that contains a [FingerprintEnrollViewModel] which contains the relevant information for * an enrollment process + * + * This flow should be the only flow which calls enroll(). */ - val enrollFlow: Flow = - combine(_consumerShouldEnroll, _hardwareAuthToken, _enrollReason) { - consumerShouldEnroll, - hardwareAuthToken, - enrollReason -> - Triple(consumerShouldEnroll, hardwareAuthToken, enrollReason) + val _enrollFlow: Flow = + combine(gatekeeperViewModel.gatekeeperInfo, _enrollReason) { hardwareAuthToken, enrollReason, + -> + Pair(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) } + /** [transformLatest] is used as we want to make sure to cancel previous API call. */ + (hardwareAuthToken, enrollReason) -> + if (hardwareAuthToken is GatekeeperInfo.GatekeeperPasswordInfo && enrollReason != null) { + fingerprintManagerInteractor.enroll(hardwareAuthToken.token, enrollReason).collect { + emit(it) + } } } - .flowOn(backgroundDispatcher) + .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 0) - /** 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 } - } + /** + * This flow will kick off education when + * 1) There is an active subscriber to this flow + * 2) shouldEnroll is true and we are on the FindSensor step + */ + val educationEnrollFlow: Flow = + _enrollReason.filterNotNull().transformLatest { enrollReason -> + if (enrollReason == EnrollReason.FindSensor) { + _enrollFlow.collect { event -> emit(event) } + } else { + emit(null) + } + } - /** Used to indicate to stop the enrollment. */ - fun stopEnroll() { - _consumerShouldEnroll.update { false } - } + /** + * This flow will kick off enrollment when + * 1) There is an active subscriber to this flow + * 2) shouldEnroll is true and we are on the EnrollEnrolling step + */ + val enrollFlow: Flow = + _enrollReason.filterNotNull().transformLatest { enrollReason -> + if (enrollReason == EnrollReason.EnrollEnrolling) { + _enrollFlow.collect { event -> emit(event) } + } else { + emit(null) + } + } class FingerprintEnrollViewModelFactory( val interactor: FingerprintManagerInteractor, - val backgroundDispatcher: CoroutineDispatcher + val gatekeeperViewModel: FingerprintGatekeeperViewModel, + val navigationViewModel: FingerprintEnrollNavigationViewModel, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( modelClass: Class, ): T { - return FingerprintEnrollViewModel(interactor, backgroundDispatcher) as T + return FingerprintEnrollViewModel( + interactor, + gatekeeperViewModel, + navigationViewModel, + ) + as T } } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt index 97c8271a73d..2e5dce0939e 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrolllNavigationViewModel.kt @@ -21,30 +21,19 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.android.settings.biometrics.fingerprint2.shared.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintFlow import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch private const val TAG = "FingerprintEnrollNavigationViewModel" -/** - * The [EnrollType] for fingerprint enrollment indicates information on how the flow should behave. - */ -sealed class EnrollType - -/** The default enrollment experience, typically called from Settings */ -object Default : EnrollType() - -/** SetupWizard/Out of box experience (OOBE) enrollment type. */ -object SetupWizard : EnrollType() - -/** Unicorn enrollment type */ -object Unicorn : EnrollType() - /** * This class is responsible for sending a [NavigationStep] which indicates where the user is in the * Fingerprint Enrollment flow @@ -53,31 +42,26 @@ class FingerprintEnrollNavigationViewModel( private val dispatcher: CoroutineDispatcher, private val fingerprintManagerInteractor: FingerprintManagerInteractor, private val gatekeeperViewModel: FingerprintGatekeeperViewModel, - private val canSkipConfirm: Boolean + private val firstStep: NextStepViewModel, + private val navState: NavState, + private val theFingerprintFlow: FingerprintFlow, ) : ViewModel() { private class InternalNavigationStep( lastStep: NextStepViewModel, nextStep: NextStepViewModel, forward: Boolean, - var canNavigate: Boolean + var canNavigate: Boolean, ) : NavigationStep(lastStep, nextStep, forward) - private var _enrollType = MutableStateFlow(Default) + private var _fingerprintFlow = MutableStateFlow(theFingerprintFlow) - /** A flow that indicates the [EnrollType] */ - val enrollType: Flow = _enrollType.asStateFlow() - - private var navState = NavState(canSkipConfirm) + /** A flow that indicates the [FingerprintFlow] */ + val fingerprintFlow: Flow = _fingerprintFlow.asStateFlow() private val _navigationStep = MutableStateFlow( - InternalNavigationStep( - PlaceHolderState, - Start.next(navState), - forward = false, - canNavigate = true - ) + InternalNavigationStep(PlaceHolderState, firstStep, forward = false, canNavigate = true) ) init { @@ -96,6 +80,10 @@ class FingerprintEnrollNavigationViewModel( */ val navigationViewModel: Flow = _navigationStep.asStateFlow() + /** This action indicates that the UI should actually update the navigation to the given step. */ + val navigationAction: Flow = + _navigationStep.shareIn(viewModelScope, SharingStarted.Lazily, 0) + /** Used to start the next step of Fingerprint Enrollment. */ fun nextStep() { viewModelScope.launch { @@ -130,6 +118,7 @@ class FingerprintEnrollNavigationViewModel( private val fingerprintManagerInteractor: FingerprintManagerInteractor, private val fingerprintGatekeeperViewModel: FingerprintGatekeeperViewModel, private val canSkipConfirm: Boolean, + private val fingerprintFlow: FingerprintFlow, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") @@ -137,11 +126,14 @@ class FingerprintEnrollNavigationViewModel( modelClass: Class, ): T { + val navState = NavState(canSkipConfirm) return FingerprintEnrollNavigationViewModel( backgroundDispatcher, fingerprintManagerInteractor, fingerprintGatekeeperViewModel, - canSkipConfirm, + Start.next(navState), + navState, + fingerprintFlow, ) as T } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/NextStepViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/NextStepViewModel.kt index e99b8f91e6c..b68f6d63abb 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/NextStepViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/NextStepViewModel.kt @@ -57,7 +57,7 @@ object PlaceHolderState : NextStepViewModel() { * This state is the initial state for the current step, and will be used to determine if the user * needs to [LaunchConfirmDeviceCredential] if not, it will go to [Intro] */ -object Start : NextStepViewModel() { +data object Start : NextStepViewModel() { override fun next(state: NavState): NextStepViewModel = if (state.confirmedDevice) Intro else LaunchConfirmDeviceCredential @@ -71,19 +71,19 @@ class Finish(val resultCode: Int?) : NextStepViewModel() { } /** State for the FingerprintEnrollment introduction */ -object Intro : NextStepViewModel() { +data object Intro : NextStepViewModel() { override fun next(state: NavState): NextStepViewModel = Education override fun prev(state: NavState): NextStepViewModel = Finish(null) } /** State for the FingerprintEnrollment education */ -object Education : NextStepViewModel() { +data object Education : NextStepViewModel() { override fun next(state: NavState): NextStepViewModel = Enrollment override fun prev(state: NavState): NextStepViewModel = Intro } /** State for the FingerprintEnrollment enrollment */ -object Enrollment : NextStepViewModel() { +data object Enrollment : NextStepViewModel() { override fun next(state: NavState): NextStepViewModel = Confirmation override fun prev(state: NavState): NextStepViewModel = Education } 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 e66b4cde4b8..debdfb8da63 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 @@ -19,8 +19,8 @@ package com.android.settings.biometrics.fingerprint2.ui.settings.binder 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.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData import com.android.settings.biometrics.fingerprint2.ui.settings.binder.FingerprintSettingsViewBinder.FingerprintView import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.EnrollAdditionalFingerprint import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.EnrollFirstFingerprint @@ -66,21 +66,21 @@ 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(enrolledFingerprints: List) + 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) + fun userLockout(authAttemptViewModel: FingerprintAuthAttemptModel.Error) /** Indicates a fingerprint preference should be highlighted */ suspend fun highlightPref(fingerId: Int) /** Indicates a user should be prompted to delete a fingerprint */ - suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintViewModel): Boolean + suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintData): Boolean /** Indicates a user should be asked to renae ma dialog */ suspend fun askUserToRenameDialog( - fingerprintViewModel: FingerprintViewModel - ): Pair? + fingerprintViewModel: FingerprintData + ): Pair? } fun bind( @@ -131,10 +131,10 @@ object FingerprintSettingsViewBinder { lifecycleScope.launch { viewModel.authFlow.filterNotNull().collect { when (it) { - is FingerprintAuthAttemptViewModel.Success -> { + is FingerprintAuthAttemptModel.Success -> { view.highlightPref(it.fingerId) } - is FingerprintAuthAttemptViewModel.Error -> { + is FingerprintAuthAttemptModel.Error -> { if (it.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) { view.userLockout(it) } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintDeletionDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintDeletionDialog.kt index 32b50c5747b..71a22eb765d 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintDeletionDialog.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintDeletionDialog.kt @@ -26,7 +26,7 @@ import android.os.Bundle import android.os.UserManager import androidx.appcompat.app.AlertDialog import com.android.settings.R -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData import com.android.settings.core.instrumentation.InstrumentedDialogFragment import kotlin.coroutines.resume import kotlinx.coroutines.suspendCancellableCoroutine @@ -34,7 +34,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine private const val KEY_IS_LAST_FINGERPRINT = "IS_LAST_FINGERPRINT" class FingerprintDeletionDialog : InstrumentedDialogFragment() { - private lateinit var fingerprintViewModel: FingerprintViewModel + private lateinit var fingerprintViewModel: FingerprintData private var isLastFingerprint: Boolean = false private lateinit var alertDialog: AlertDialog lateinit var onClickListener: DialogInterface.OnClickListener @@ -51,7 +51,7 @@ class FingerprintDeletionDialog : InstrumentedDialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val fp = requireArguments().get(KEY_FINGERPRINT) as android.hardware.fingerprint.Fingerprint - fingerprintViewModel = FingerprintViewModel(fp.name.toString(), fp.biometricId, fp.deviceId) + fingerprintViewModel = FingerprintData(fp.name.toString(), fp.biometricId, fp.deviceId) isLastFingerprint = requireArguments().getBoolean(KEY_IS_LAST_FINGERPRINT) val title = getString(R.string.fingerprint_delete_title, fingerprintViewModel.name) var message = getString(R.string.fingerprint_v2_delete_message, fingerprintViewModel.name) @@ -95,9 +95,9 @@ class FingerprintDeletionDialog : InstrumentedDialogFragment() { companion object { private const val KEY_FINGERPRINT = "fingerprint" suspend fun showInstance( - fp: FingerprintViewModel, - lastFingerprint: Boolean, - target: FingerprintSettingsV2Fragment, + fp: FingerprintData, + lastFingerprint: Boolean, + target: FingerprintSettingsV2Fragment, ) = suspendCancellableCoroutine { continuation -> val dialog = FingerprintDeletionDialog() dialog.onClickListener = DialogInterface.OnClickListener { _, _ -> continuation.resume(true) } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsPreference.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsPreference.kt index b1e5097c565..ea26946a482 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsPreference.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsPreference.kt @@ -22,7 +22,7 @@ import android.view.View import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceViewHolder import com.android.settings.R -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData import com.android.settingslib.widget.TwoTargetPreference import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -30,10 +30,10 @@ import kotlinx.coroutines.launch private const val TAG = "FingerprintSettingsPreference" class FingerprintSettingsPreference( - context: Context, - val fingerprintViewModel: FingerprintViewModel, - val fragment: FingerprintSettingsV2Fragment, - val isLastFingerprint: Boolean + context: Context, + val fingerprintViewModel: FingerprintData, + val fragment: FingerprintSettingsV2Fragment, + val isLastFingerprint: Boolean ) : TwoTargetPreference(context) { private lateinit var myView: View @@ -79,7 +79,7 @@ class FingerprintSettingsPreference( return FingerprintDeletionDialog.showInstance(fingerprintViewModel, isLastFingerprint, fragment) } - suspend fun askUserToRenameDialog(): Pair? { + suspend fun askUserToRenameDialog(): Pair? { return FingerprintSettingsRenameDialog.showInstance(fingerprintViewModel, fragment) } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsRenameDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsRenameDialog.kt index 9bde0b062b0..ff469f18b2d 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsRenameDialog.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsRenameDialog.kt @@ -27,7 +27,7 @@ import android.util.Log import android.widget.ImeAwareEditText import androidx.appcompat.app.AlertDialog import com.android.settings.R -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData import com.android.settings.core.instrumentation.InstrumentedDialogFragment import kotlin.coroutines.resume import kotlinx.coroutines.suspendCancellableCoroutine @@ -46,7 +46,7 @@ class FingerprintSettingsRenameDialog : InstrumentedDialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { Log.d(TAG, "onCreateDialog $this") val fp = requireArguments().get(KEY_FINGERPRINT) as android.hardware.fingerprint.Fingerprint - val fingerprintViewModel = FingerprintViewModel(fp.name.toString(), fp.biometricId, fp.deviceId) + val fingerprintViewModel = FingerprintData(fp.name.toString(), fp.biometricId, fp.deviceId) val context = requireContext() val alertDialog = @@ -101,7 +101,7 @@ class FingerprintSettingsRenameDialog : InstrumentedDialogFragment() { companion object { private const val KEY_FINGERPRINT = "fingerprint" - suspend fun showInstance(fp: FingerprintViewModel, target: FingerprintSettingsV2Fragment) = + suspend fun showInstance(fp: FingerprintData, target: FingerprintSettingsV2Fragment) = suspendCancellableCoroutine { continuation -> val dialog = FingerprintSettingsRenameDialog() val onClick = 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 c818566ae32..c22a5a73bfb 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 @@ -46,8 +46,10 @@ import com.android.settings.biometrics.GatekeeperPasswordProvider 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.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.repository.PressToAuthProviderImpl +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData +import com.android.settings.biometrics.fingerprint2.shared.model.Settings import com.android.settings.biometrics.fingerprint2.ui.settings.binder.FingerprintSettingsViewBinder import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsNavigationViewModel import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsViewModel @@ -142,7 +144,7 @@ class FingerprintSettingsV2Fragment : } } - override fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error) { + override fun userLockout(authAttemptViewModel: FingerprintAuthAttemptModel.Error) { Toast.makeText(activity, authAttemptViewModel.message, Toast.LENGTH_SHORT).show() } @@ -186,40 +188,46 @@ class FingerprintSettingsV2Fragment : val backgroundDispatcher = Dispatchers.IO val activity = requireActivity() val userHandle = activity.user.identifier + // Note that SUW should not be launching FingerprintSettings + val isAnySuw = Settings + + val pressToAuthProvider = { + var toReturn: Int = + Secure.getIntForUser( + context.contentResolver, + Secure.SFPS_PERFORMANT_AUTH_ENABLED, + -1, + userHandle, + ) + if (toReturn == -1) { + toReturn = + if ( + context.resources.getBoolean(com.android.internal.R.bool.config_performantAuthDefault) + ) { + 1 + } else { + 0 + } + Secure.putIntForUser( + context.contentResolver, + Secure.SFPS_PERFORMANT_AUTH_ENABLED, + toReturn, + userHandle + ) + } + + toReturn == 1 + } val interactor = FingerprintManagerInteractorImpl( context.applicationContext, backgroundDispatcher, fingerprintManager, - GatekeeperPasswordProvider(LockPatternUtils(context.applicationContext)) - ) { - var toReturn: Int = - Secure.getIntForUser( - context.contentResolver, - Secure.SFPS_PERFORMANT_AUTH_ENABLED, - -1, - userHandle, - ) - if (toReturn == -1) { - toReturn = - if ( - context.resources.getBoolean(com.android.internal.R.bool.config_performantAuthDefault) - ) { - 1 - } else { - 0 - } - Secure.putIntForUser( - context.contentResolver, - Secure.SFPS_PERFORMANT_AUTH_ENABLED, - toReturn, - userHandle - ) - } - - toReturn == 1 - } + GatekeeperPasswordProvider(LockPatternUtils(context.applicationContext)), + PressToAuthProviderImpl(context), + isAnySuw + ) val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN) val challenge = intent.getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L) @@ -292,18 +300,18 @@ class FingerprintSettingsV2Fragment : } /** Used to indicate that preference has been clicked */ - fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) { + fun onPrefClicked(fingerprintViewModel: FingerprintData) { Log.d(TAG, "onPrefClicked(${fingerprintViewModel})") settingsViewModel.onPrefClicked(fingerprintViewModel) } /** Used to indicate that a delete pref has been clicked */ - fun onDeletePrefClicked(fingerprintViewModel: FingerprintViewModel) { + fun onDeletePrefClicked(fingerprintViewModel: FingerprintData) { Log.d(TAG, "onDeletePrefClicked(${fingerprintViewModel})") settingsViewModel.onDeleteClicked(fingerprintViewModel) } - override fun showSettings(enrolledFingerprints: List) { + override fun showSettings(enrolledFingerprints: List) { val category = this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY) as PreferenceCategory? @@ -422,7 +430,7 @@ class FingerprintSettingsV2Fragment : } } - override suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintViewModel): Boolean { + override suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintData): Boolean { Log.d(TAG, "showing delete dialog for (${fingerprintViewModel})") try { @@ -446,8 +454,8 @@ class FingerprintSettingsV2Fragment : } override suspend fun askUserToRenameDialog( - fingerprintViewModel: FingerprintViewModel - ): Pair? { + fingerprintViewModel: FingerprintData + ): Pair? { Log.d(TAG, "showing rename dialog for (${fingerprintViewModel})") try { val toReturn = 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 fa1e5e11feb..164f79fed58 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 @@ -22,8 +22,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.android.settings.biometrics.fingerprint2.shared.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.shared.model.FingerprintAuthAttemptModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData import com.android.systemui.biometrics.shared.model.FingerprintSensorType import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -53,11 +53,11 @@ class FingerprintSettingsViewModel( private val backgroundDispatcher: CoroutineDispatcher, private val navigationViewModel: FingerprintSettingsNavigationViewModel, ) : ViewModel() { - private val _enrolledFingerprints: MutableStateFlow?> = + private val _enrolledFingerprints: MutableStateFlow?> = MutableStateFlow(null) /** Represents the stream of enrolled fingerprints. */ - val enrolledFingerprints: Flow> = + val enrolledFingerprints: Flow> = _enrolledFingerprints.asStateFlow().filterNotNull().filterOnlyWhenSettingsIsShown() /** Represents the stream of the information of "Add Fingerprint" preference. */ @@ -95,10 +95,10 @@ class FingerprintSettingsViewModel( private val _sensorNullOrEmpty: Flow = fingerprintManagerInteractor.sensorPropertiesInternal.map { it == null } - private val _isLockedOut: MutableStateFlow = + private val _isLockedOut: MutableStateFlow = MutableStateFlow(null) - private val _authSucceeded: MutableSharedFlow = + private val _authSucceeded: MutableSharedFlow = MutableSharedFlow() private val _attemptsSoFar: MutableStateFlow = MutableStateFlow(0) @@ -164,7 +164,7 @@ class FingerprintSettingsViewModel( .distinctUntilChanged() /** Represents a consistent stream of authentication attempts. */ - val authFlow: Flow = + val authFlow: Flow = canAuthenticate .transformLatest { try { @@ -173,11 +173,11 @@ class FingerprintSettingsViewModel( Log.d(TAG, "canAuthenticate authing") attemptingAuth() when (val authAttempt = fingerprintManagerInteractor.authenticate()) { - is FingerprintAuthAttemptViewModel.Success -> { + is FingerprintAuthAttemptModel.Success -> { onAuthSuccess(authAttempt) emit(authAttempt) } - is FingerprintAuthAttemptViewModel.Error -> { + is FingerprintAuthAttemptModel.Error -> { if (authAttempt.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) { lockout(authAttempt) emit(authAttempt) @@ -219,7 +219,7 @@ class FingerprintSettingsViewModel( } /** The fingerprint delete button has been clicked. */ - fun onDeleteClicked(fingerprintViewModel: FingerprintViewModel) { + fun onDeleteClicked(fingerprintViewModel: FingerprintData) { viewModelScope.launch { if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) { _isShowingDialog.tryEmit(PreferenceViewModel.DeleteDialog(fingerprintViewModel)) @@ -230,7 +230,7 @@ class FingerprintSettingsViewModel( } /** The rename fingerprint dialog has been clicked. */ - fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) { + fun onPrefClicked(fingerprintViewModel: FingerprintData) { viewModelScope.launch { if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) { _isShowingDialog.tryEmit(PreferenceViewModel.RenameDialog(fingerprintViewModel)) @@ -241,7 +241,7 @@ class FingerprintSettingsViewModel( } /** A request to delete a fingerprint */ - fun deleteFingerprint(fp: FingerprintViewModel) { + fun deleteFingerprint(fp: FingerprintData) { viewModelScope.launch(backgroundDispatcher) { if (fingerprintManagerInteractor.removeFingerprint(fp)) { updateEnrolledFingerprints() @@ -250,7 +250,7 @@ class FingerprintSettingsViewModel( } /** A request to rename a fingerprint */ - fun renameFingerprint(fp: FingerprintViewModel, newName: String) { + fun renameFingerprint(fp: FingerprintData, newName: String) { viewModelScope.launch { fingerprintManagerInteractor.renameFingerprint(fp, newName) updateEnrolledFingerprints() @@ -261,12 +261,12 @@ class FingerprintSettingsViewModel( _attemptsSoFar.update { it + 1 } } - private suspend fun onAuthSuccess(success: FingerprintAuthAttemptViewModel.Success) { + private suspend fun onAuthSuccess(success: FingerprintAuthAttemptModel.Success) { _authSucceeded.emit(success) _attemptsSoFar.update { 0 } } - private fun lockout(attemptViewModel: FingerprintAuthAttemptViewModel.Error) { + private fun lockout(attemptViewModel: FingerprintAuthAttemptModel.Error) { _isLockedOut.update { attemptViewModel } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/PreferenceViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/PreferenceViewModel.kt index 4c33f7f0415..181da4e85f6 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/PreferenceViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/PreferenceViewModel.kt @@ -16,15 +16,15 @@ package com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData /** Classed use to represent a Dialogs state. */ sealed class PreferenceViewModel { data class RenameDialog( - val fingerprintViewModel: FingerprintViewModel, + val fingerprintViewModel: FingerprintData, ) : PreferenceViewModel() data class DeleteDialog( - val fingerprintViewModel: FingerprintViewModel, + val fingerprintViewModel: FingerprintData, ) : PreferenceViewModel() } diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt b/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt index cea6676bc20..024f346909f 100644 --- a/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt @@ -33,12 +33,15 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.runner.AndroidJUnit4 import com.android.settings.R +import com.android.settings.biometrics.fingerprint2.shared.model.Default import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollIntroV2Fragment 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.FingerprintGatekeeperViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.GatekeeperInfo +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Intro +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.NavState import com.android.settings.testutils2.FakeFingerprintManagerInteractor import com.google.android.setupdesign.GlifLayout import com.google.android.setupdesign.template.RequireScrollMixin @@ -65,9 +68,12 @@ class FingerprintEnrollIntroFragmentTest { backgroundDispatcher, interactor, gatekeeperViewModel, - canSkipConfirm = true, + Intro, + NavState(true), + Default, ) - private var fingerprintViewModel = FingerprintEnrollViewModel(interactor, backgroundDispatcher) + private var fingerprintViewModel = + FingerprintEnrollViewModel(interactor, gatekeeperViewModel, navigationViewModel) private var fingerprintScrollViewModel = FingerprintScrollViewModel() @Before diff --git a/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt b/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt index ad943f210a3..dd8658cf96a 100644 --- a/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt +++ b/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt @@ -18,9 +18,9 @@ package com.android.settings.testutils2 import com.android.settings.biometrics.fingerprint2.shared.domain.interactor.FingerprintManagerInteractor import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason -import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollStateViewModel -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData import com.android.systemui.biometrics.shared.model.FingerprintSensor import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength @@ -32,10 +32,11 @@ import kotlinx.coroutines.flow.flowOf class FakeFingerprintManagerInteractor : FingerprintManagerInteractor { var enrollableFingerprints: Int = 5 - var enrolledFingerprintsInternal: MutableList = mutableListOf() + var enrolledFingerprintsInternal: MutableList = mutableListOf() var challengeToGenerate: Pair = Pair(-1L, byteArrayOf()) - var authenticateAttempt = FingerprintAuthAttemptViewModel.Success(1) - val enrollStateViewModel = FingerEnrollStateViewModel.EnrollProgress(1) + var authenticateAttempt = FingerprintAuthAttemptModel.Success(1) + var enrollStateViewModel: List = + listOf(FingerEnrollState.EnrollProgress(5, 5)) var pressToAuthEnabled = true var sensorProp = @@ -46,7 +47,7 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor { FingerprintSensorType.POWER_BUTTON ) - override suspend fun authenticate(): FingerprintAuthAttemptViewModel { + override suspend fun authenticate(): FingerprintAuthAttemptModel { return authenticateAttempt } @@ -54,7 +55,7 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor { return challengeToGenerate } - override val enrolledFingerprints: Flow> = flow { + override val enrolledFingerprints: Flow> = flow { emit(enrolledFingerprintsInternal) } @@ -62,24 +63,22 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor { emit(enrolledFingerprintsInternal.size < enrollableFingerprints) } - override val sensorPropertiesInternal: Flow = flow { - emit(sensorProp) - } + override val sensorPropertiesInternal: Flow = flow { emit(sensorProp) } override val maxEnrollableFingerprints: Flow = flow { emit(enrollableFingerprints) } override suspend fun enroll( hardwareAuthToken: ByteArray?, enrollReason: EnrollReason - ): Flow = flowOf(enrollStateViewModel) + ): Flow = flowOf(*enrollStateViewModel.toTypedArray()) - override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean { + override suspend fun removeFingerprint(fp: FingerprintData): Boolean { return enrolledFingerprintsInternal.remove(fp) } - override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) { + override suspend fun renameFingerprint(fp: FingerprintData, newName: String) { if (enrolledFingerprintsInternal.remove(fp)) { - enrolledFingerprintsInternal.add(FingerprintViewModel(newName, fp.fingerId, fp.deviceId)) + enrolledFingerprintsInternal.add(FingerprintData(newName, fp.fingerId, fp.deviceId)) } } 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 f0d0a0a4ca0..3440d2ac2d4 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 @@ -26,12 +26,14 @@ import android.os.CancellationSignal import android.os.Handler import androidx.test.core.app.ApplicationProvider import com.android.settings.biometrics.GatekeeperPasswordProvider -import com.android.settings.biometrics.fingerprint2.shared.domain.interactor.FingerprintManagerInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl +import com.android.settings.biometrics.fingerprint2.shared.data.repository.PressToAuthProvider +import com.android.settings.biometrics.fingerprint2.shared.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.shared.model.Default import com.android.settings.biometrics.fingerprint2.shared.model.EnrollReason -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel -import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollStateViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerEnrollState +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData import com.android.settings.password.ChooseLockSettingsHelper import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.cancelAndJoin @@ -69,7 +71,11 @@ class FingerprintManagerInteractorTest { @Mock private lateinit var gateKeeperPasswordProvider: GatekeeperPasswordProvider private var testScope = TestScope(backgroundDispatcher) - private var pressToAuthProvider = { true } + private var pressToAuthProvider = + object : PressToAuthProvider { + override val isEnabled: Boolean + get() = false + } @Before fun setup() { @@ -80,6 +86,7 @@ class FingerprintManagerInteractorTest { fingerprintManager, gateKeeperPasswordProvider, pressToAuthProvider, + Default, ) } @@ -164,7 +171,7 @@ class FingerprintManagerInteractorTest { @Test fun testRemoveFingerprint_succeeds() = testScope.runTest { - val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L) + val fingerprintViewModelToRemove = FingerprintData("Finger 2", 1, 2L) val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L) val removalCallback: ArgumentCaptor = argumentCaptor() @@ -187,7 +194,7 @@ class FingerprintManagerInteractorTest { @Test fun testRemoveFingerprint_fails() = testScope.runTest { - val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L) + val fingerprintViewModelToRemove = FingerprintData("Finger 2", 1, 2L) val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L) val removalCallback: ArgumentCaptor = argumentCaptor() @@ -214,7 +221,7 @@ class FingerprintManagerInteractorTest { @Test fun testRenameFingerprint_succeeds() = testScope.runTest { - val fingerprintToRename = FingerprintViewModel("Finger 2", 1, 2L) + val fingerprintToRename = FingerprintData("Finger 2", 1, 2L) underTest.renameFingerprint(fingerprintToRename, "Woo") @@ -226,7 +233,7 @@ class FingerprintManagerInteractorTest { testScope.runTest { val fingerprint = Fingerprint("Woooo", 100, 101L) - var result: FingerprintAuthAttemptViewModel? = null + var result: FingerprintAuthAttemptModel? = null val job = launch { result = underTest.authenticate() } val authCallback: ArgumentCaptor = argumentCaptor() @@ -247,13 +254,13 @@ class FingerprintManagerInteractorTest { runCurrent() job.cancelAndJoin() - assertThat(result).isEqualTo(FingerprintAuthAttemptViewModel.Success(fingerprint.biometricId)) + assertThat(result).isEqualTo(FingerprintAuthAttemptModel.Success(fingerprint.biometricId)) } @Test fun testAuth_lockout() = testScope.runTest { - var result: FingerprintAuthAttemptViewModel? = null + var result: FingerprintAuthAttemptModel? = null val job = launch { result = underTest.authenticate() } val authCallback: ArgumentCaptor = argumentCaptor() @@ -274,7 +281,7 @@ class FingerprintManagerInteractorTest { job.cancelAndJoin() assertThat(result) .isEqualTo( - FingerprintAuthAttemptViewModel.Error(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!") + FingerprintAuthAttemptModel.Error(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!") ) } @@ -282,7 +289,7 @@ class FingerprintManagerInteractorTest { fun testEnroll_progress() = testScope.runTest { val token = byteArrayOf(5, 3, 2) - var result: FingerEnrollStateViewModel? = null + var result: FingerEnrollState? = null val job = launch { underTest.enroll(token, EnrollReason.FindSensor).collect { result = it } } val enrollCallback: ArgumentCaptor = argumentCaptor() runCurrent() @@ -299,14 +306,14 @@ class FingerprintManagerInteractorTest { runCurrent() job.cancelAndJoin() - assertThat(result).isEqualTo(FingerEnrollStateViewModel.EnrollProgress(1)) + assertThat(result).isEqualTo(FingerEnrollState.EnrollProgress(1, 2)) } @Test fun testEnroll_help() = testScope.runTest { val token = byteArrayOf(5, 3, 2) - var result: FingerEnrollStateViewModel? = null + var result: FingerEnrollState? = null val job = launch { underTest.enroll(token, EnrollReason.FindSensor).collect { result = it } } val enrollCallback: ArgumentCaptor = argumentCaptor() runCurrent() @@ -323,14 +330,14 @@ class FingerprintManagerInteractorTest { runCurrent() job.cancelAndJoin() - assertThat(result).isEqualTo(FingerEnrollStateViewModel.EnrollHelp(-1, "help")) + assertThat(result).isEqualTo(FingerEnrollState.EnrollHelp(-1, "help")) } @Test fun testEnroll_error() = testScope.runTest { val token = byteArrayOf(5, 3, 2) - var result: FingerEnrollStateViewModel? = null + var result: FingerEnrollState? = null val job = launch { underTest.enroll(token, EnrollReason.FindSensor).collect { result = it } } val enrollCallback: ArgumentCaptor = argumentCaptor() runCurrent() @@ -343,17 +350,20 @@ class FingerprintManagerInteractorTest { capture(enrollCallback), eq(FingerprintManager.ENROLL_FIND_SENSOR) ) - enrollCallback.value.onEnrollmentError(-2, "error") + enrollCallback.value.onEnrollmentError(-1, "error") runCurrent() job.cancelAndJoin() - - assertThat(result).isEqualTo(FingerEnrollStateViewModel.EnrollError(-2, "error")) + assertThat(result).isInstanceOf(FingerEnrollState.EnrollError::class.java) } 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) } diff --git a/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt b/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt index 509b0edb22d..bd94cba3ec5 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt @@ -21,7 +21,9 @@ import android.content.res.Configuration import android.view.accessibility.AccessibilityManager import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider +import com.android.settings.biometrics.fingerprint2.shared.model.Default import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.AccessibilityViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Education import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel @@ -70,6 +72,7 @@ class FingerprintEnrollFindSensorViewModelV2Test { private lateinit var foldStateViewModel: FoldStateViewModel private lateinit var orientationStateViewModel: OrientationStateViewModel private lateinit var underTest: FingerprintEnrollFindSensorViewModel + private lateinit var backgroundViewModel: BackgroundViewModel private val context: Context = ApplicationProvider.getApplicationContext() private val accessibilityManager: AccessibilityManager = context.getSystemService(AccessibilityManager::class.java)!! @@ -93,12 +96,18 @@ class FingerprintEnrollFindSensorViewModelV2Test { fakeFingerprintManagerInteractor, gatekeeperViewModel, canSkipConfirm = true, + Default, ) .create(FingerprintEnrollNavigationViewModel::class.java) + + backgroundViewModel = + BackgroundViewModel.BackgroundViewModelFactory().create(BackgroundViewModel::class.java) + backgroundViewModel.inForeground() enrollViewModel = FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory( fakeFingerprintManagerInteractor, - backgroundDispatcher + gatekeeperViewModel, + navigationViewModel, ) .create(FingerprintEnrollViewModel::class.java) accessibilityViewModel = @@ -114,6 +123,7 @@ class FingerprintEnrollFindSensorViewModelV2Test { navigationViewModel, enrollViewModel, gatekeeperViewModel, + backgroundViewModel, accessibilityViewModel, foldStateViewModel, orientationStateViewModel @@ -123,6 +133,7 @@ class FingerprintEnrollFindSensorViewModelV2Test { // Navigate to Education page navigationViewModel.nextStep() } + @After fun tearDown() { Dispatchers.resetMain() diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/modules/enrolling/rfps/viewmodel/RFPSIconTouchViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/modules/enrolling/rfps/viewmodel/RFPSIconTouchViewModelTest.kt new file mode 100644 index 00000000000..46e883af33d --- /dev/null +++ b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/modules/enrolling/rfps/viewmodel/RFPSIconTouchViewModelTest.kt @@ -0,0 +1,142 @@ +/* + * 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.fingerprint2.ui.enrollment.modules.enrolling.rfps.viewmodel + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSIconTouchViewModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class RFPSIconTouchViewModelTest { + @JvmField @Rule var rule = MockitoJUnit.rule() + + @get:Rule val instantTaskRule = InstantTaskExecutorRule() + + private var backgroundDispatcher = StandardTestDispatcher() + private var testScope = TestScope(backgroundDispatcher) + private lateinit var rfpsIconTouchViewModel: RFPSIconTouchViewModel + + @Before + fun setup() { + Dispatchers.setMain(backgroundDispatcher) + testScope = TestScope(backgroundDispatcher) + rfpsIconTouchViewModel = + RFPSIconTouchViewModel() + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun initShouldNotShowDialog() = + testScope.runTest { + var shouldShowDialog = false + + val job = launch { rfpsIconTouchViewModel.shouldShowDialog.collect { shouldShowDialog = it } } + + runCurrent() + + assertThat(shouldShowDialog).isFalse() + job.cancel() + } + + @Test + fun shouldShowDialogTest() = + testScope.runTest { + var shouldShowDialog = false + + val job = launch { rfpsIconTouchViewModel.shouldShowDialog.collect { shouldShowDialog = it } } + + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + + runCurrent() + + assertThat(shouldShowDialog).isTrue() + job.cancel() + } + + @Test + fun stateShouldBeFalseAfterReset() = + testScope.runTest { + var shouldShowDialog = false + + val job = launch { rfpsIconTouchViewModel.shouldShowDialog.collect { shouldShowDialog = it } } + + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + + runCurrent() + + assertThat(shouldShowDialog).isTrue() + + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + runCurrent() + + assertThat(shouldShowDialog).isFalse() + + job.cancel() + } + + @Test + fun toggleMultipleTimes() = + testScope.runTest { + var shouldShowDialog = false + + val job = launch { rfpsIconTouchViewModel.shouldShowDialog.collect { shouldShowDialog = it } } + + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + + runCurrent() + + assertThat(shouldShowDialog).isTrue() + + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + runCurrent() + + assertThat(shouldShowDialog).isFalse() + + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + rfpsIconTouchViewModel.userTouchedFingerprintIcon() + + runCurrent() + assertThat(shouldShowDialog).isTrue() + + job.cancel() + } +} diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModelTest.kt new file mode 100644 index 00000000000..efb4a07f15b --- /dev/null +++ b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModelTest.kt @@ -0,0 +1,153 @@ +/* + * 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.fingerprint2.ui.enrollment.viewmodel + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.android.settings.biometrics.fingerprint2.shared.model.Default +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Enrollment +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel +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.FingerprintGatekeeperViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.GatekeeperInfo +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.NavState +import com.android.settings.testutils2.FakeFingerprintManagerInteractor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class FingerprintEnrollEnrollingViewModelTest { + @JvmField @Rule var rule = MockitoJUnit.rule() + + @get:Rule val instantTaskRule = InstantTaskExecutorRule() + + private var backgroundDispatcher = StandardTestDispatcher() + private lateinit var enrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel + private lateinit var backgroundViewModel: BackgroundViewModel + private lateinit var gateKeeperViewModel: FingerprintGatekeeperViewModel + private lateinit var navigationViewModel: FingerprintEnrollNavigationViewModel + private val defaultGatekeeperInfo = GatekeeperInfo.GatekeeperPasswordInfo(byteArrayOf(1, 3), 3) + private var testScope = TestScope(backgroundDispatcher) + + private lateinit var fakeFingerprintManagerInteractor: FakeFingerprintManagerInteractor + + private fun initialize(gatekeeperInfo: GatekeeperInfo = defaultGatekeeperInfo) { + fakeFingerprintManagerInteractor = FakeFingerprintManagerInteractor() + gateKeeperViewModel = + FingerprintGatekeeperViewModel.FingerprintGatekeeperViewModelFactory( + gatekeeperInfo, + fakeFingerprintManagerInteractor + ) + .create(FingerprintGatekeeperViewModel::class.java) + + navigationViewModel = + FingerprintEnrollNavigationViewModel( + backgroundDispatcher, + fakeFingerprintManagerInteractor, + gateKeeperViewModel, + Enrollment, + NavState(true), + Default, + ) + + backgroundViewModel = + BackgroundViewModel.BackgroundViewModelFactory().create(BackgroundViewModel::class.java) + backgroundViewModel.inForeground() + val fingerprintEnrollViewModel = + FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory( + fakeFingerprintManagerInteractor, + gateKeeperViewModel, + navigationViewModel, + ) + .create(FingerprintEnrollViewModel::class.java) + enrollEnrollingViewModel = + FingerprintEnrollEnrollingViewModel.FingerprintEnrollEnrollingViewModelFactory( + fingerprintEnrollViewModel, + backgroundViewModel, + ) + .create(FingerprintEnrollEnrollingViewModel::class.java) + } + + @Before + fun setup() { + Dispatchers.setMain(backgroundDispatcher) + initialize() + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun testEnrollShouldBeFalse() = + testScope.runTest { + var shouldEnroll = false + + val job = launch { + enrollEnrollingViewModel.enrollFlowShouldBeRunning.collect { shouldEnroll = it } + } + + assertThat(shouldEnroll).isFalse() + runCurrent() + + enrollEnrollingViewModel.canEnroll() + runCurrent() + + assertThat(shouldEnroll).isTrue() + job.cancel() + } + + @Test + fun testEnrollShouldBeFalseWhenBackground() = + testScope.runTest { + var shouldEnroll = false + + val job = launch { + enrollEnrollingViewModel.enrollFlowShouldBeRunning.collect { shouldEnroll = it } + } + + assertThat(shouldEnroll).isFalse() + runCurrent() + + enrollEnrollingViewModel.canEnroll() + runCurrent() + + assertThat(shouldEnroll).isTrue() + + backgroundViewModel.wentToBackground() + runCurrent() + assertThat(shouldEnroll).isFalse() + + job.cancel() + } +} diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsNavigationViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsNavigationViewModelTest.kt index d4dbec5cdfd..064e087544d 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsNavigationViewModelTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsNavigationViewModelTest.kt @@ -18,7 +18,7 @@ package com.android.settings.fingerprint2.ui.settings import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.android.settings.biometrics.BiometricEnrollBase -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.EnrollFirstFingerprint import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsNavigationViewModel import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FinishSettings @@ -208,7 +208,7 @@ class FingerprintSettingsNavigationViewModelTest { fun enrollAdditionalFingerprints_fails() = testScope.runTest { fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = - mutableListOf(FingerprintViewModel("a", 1, 3L)) + mutableListOf(FingerprintData("a", 1, 3L)) fakeFingerprintManagerInteractor.challengeToGenerate = Pair(4L, byteArrayOf(3, 3, 1)) var nextStep: NextStepViewModel? = null @@ -227,7 +227,7 @@ class FingerprintSettingsNavigationViewModelTest { fun enrollAdditional_success() = testScope.runTest { fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = - mutableListOf(FingerprintViewModel("a", 1, 3L)) + mutableListOf(FingerprintData("a", 1, 3L)) var nextStep: NextStepViewModel? = null val job = launch { underTest.nextStep.collect { nextStep = it } } @@ -245,7 +245,7 @@ class FingerprintSettingsNavigationViewModelTest { fun confirmDeviceCredential_withEnrolledFingerprint_showsSettings() = testScope.runTest { fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = - mutableListOf(FingerprintViewModel("a", 1, 3L)) + mutableListOf(FingerprintData("a", 1, 3L)) fakeFingerprintManagerInteractor.challengeToGenerate = Pair(10L, byteArrayOf(1, 2, 3)) var nextStep: NextStepViewModel? = null @@ -320,7 +320,7 @@ class FingerprintSettingsNavigationViewModelTest { fun showSettings_shouldFinish() = testScope.runTest { fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = - mutableListOf(FingerprintViewModel("a", 1, 3L)) + mutableListOf(FingerprintData("a", 1, 3L)) var nextStep: NextStepViewModel? = null val job = launch { underTest.nextStep.collect { nextStep = it } } diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsViewModelTest.kt index d25ced011a9..4bd912144a6 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsViewModelTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsViewModelTest.kt @@ -17,8 +17,8 @@ package com.android.settings.fingerprint2.ui.settings import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel -import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptModel +import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintData import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsNavigationViewModel import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsViewModel import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.PreferenceViewModel @@ -103,7 +103,7 @@ class FingerprintSettingsViewModelTest { FingerprintSensorType.UDFPS_OPTICAL, ) fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = - mutableListOf(FingerprintViewModel("a", 1, 3L)) + mutableListOf(FingerprintData("a", 1, 3L)) underTest = FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( @@ -114,7 +114,7 @@ class FingerprintSettingsViewModelTest { ) .create(FingerprintSettingsViewModel::class.java) - var authAttempt: FingerprintAuthAttemptViewModel? = null + var authAttempt: FingerprintAuthAttemptModel? = null val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } } underTest.shouldAuthenticate(true) @@ -139,7 +139,7 @@ class FingerprintSettingsViewModelTest { FingerprintSensorType.UDFPS_ULTRASONIC, ) fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = - mutableListOf(FingerprintViewModel("a", 1, 3L)) + mutableListOf(FingerprintData("a", 1, 3L)) underTest = FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( @@ -150,7 +150,7 @@ class FingerprintSettingsViewModelTest { ) .create(FingerprintSettingsViewModel::class.java) - var authAttempt: FingerprintAuthAttemptViewModel? = null + var authAttempt: FingerprintAuthAttemptModel? = null val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } } underTest.shouldAuthenticate(true) @@ -173,8 +173,8 @@ class FingerprintSettingsViewModelTest { FingerprintSensorType.POWER_BUTTON ) fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = - mutableListOf(FingerprintViewModel("a", 1, 3L)) - val success = FingerprintAuthAttemptViewModel.Success(1) + mutableListOf(FingerprintData("a", 1, 3L)) + val success = FingerprintAuthAttemptModel.Success(1) fakeFingerprintManagerInteractor.authenticateAttempt = success underTest = @@ -186,7 +186,7 @@ class FingerprintSettingsViewModelTest { ) .create(FingerprintSettingsViewModel::class.java) - var authAttempt: FingerprintAuthAttemptViewModel? = null + var authAttempt: FingerprintAuthAttemptModel? = null val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } } underTest.shouldAuthenticate(true) @@ -200,7 +200,7 @@ class FingerprintSettingsViewModelTest { @Test fun deleteDialog_showAndDismiss() = runTest { - val fingerprintToDelete = FingerprintViewModel("A", 1, 10L) + val fingerprintToDelete = FingerprintData("A", 1, 10L) fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToDelete) @@ -236,7 +236,7 @@ class FingerprintSettingsViewModelTest { @Test fun renameDialog_showAndDismiss() = runTest { - val fingerprintToRename = FingerprintViewModel("World", 1, 10L) + val fingerprintToRename = FingerprintData("World", 1, 10L) fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToRename) @@ -274,7 +274,7 @@ class FingerprintSettingsViewModelTest { @Test fun testTwoDialogsCannotShow_atSameTime() = runTest { - val fingerprintToDelete = FingerprintViewModel("A", 1, 10L) + val fingerprintToDelete = FingerprintData("A", 1, 10L) fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToDelete) @@ -311,9 +311,9 @@ class FingerprintSettingsViewModelTest { fun authenticatePauses_whenPaused() = testScope.runTest { val fingerprints = setupAuth() - val success = FingerprintAuthAttemptViewModel.Success(fingerprints.first().fingerId) + val success = FingerprintAuthAttemptModel.Success(fingerprints.first().fingerId) - var authAttempt: FingerprintAuthAttemptViewModel? = null + var authAttempt: FingerprintAuthAttemptModel? = null val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } } @@ -325,7 +325,7 @@ class FingerprintSettingsViewModelTest { assertThat(authAttempt).isEqualTo(success) fakeFingerprintManagerInteractor.authenticateAttempt = - FingerprintAuthAttemptViewModel.Success(10) + FingerprintAuthAttemptModel.Success(10) underTest.shouldAuthenticate(false) advanceTimeBy(400) runCurrent() @@ -340,7 +340,7 @@ class FingerprintSettingsViewModelTest { testScope.runTest { val fingerprints = setupAuth() - var authAttempt: FingerprintAuthAttemptViewModel? = null + var authAttempt: FingerprintAuthAttemptModel? = null val job = launch { underTest.authFlow.take(1).collectLatest { authAttempt = it } } underTest.shouldAuthenticate(true) navigationViewModel.onConfirmDevice(true, 10L) @@ -357,7 +357,7 @@ class FingerprintSettingsViewModelTest { testScope.runTest { val fingerprints = setupAuth() - var authAttempt: FingerprintAuthAttemptViewModel? = null + var authAttempt: FingerprintAuthAttemptModel? = null val job = launch { underTest.authFlow.take(1).collectLatest { authAttempt = it } } underTest.shouldAuthenticate(true) navigationViewModel.onConfirmDevice(true, 10L) @@ -370,7 +370,7 @@ class FingerprintSettingsViewModelTest { assertThat(authAttempt).isEqualTo(null) } - private fun setupAuth(): MutableList { + private fun setupAuth(): MutableList { fakeFingerprintManagerInteractor.sensorProp = FingerprintSensor( 0 /* sensorId */, @@ -379,9 +379,9 @@ class FingerprintSettingsViewModelTest { FingerprintSensorType.POWER_BUTTON ) val fingerprints = - mutableListOf(FingerprintViewModel("a", 1, 3L), FingerprintViewModel("b", 2, 5L)) + mutableListOf(FingerprintData("a", 1, 3L), FingerprintData("b", 2, 5L)) fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = fingerprints - val success = FingerprintAuthAttemptViewModel.Success(1) + val success = FingerprintAuthAttemptModel.Success(1) fakeFingerprintManagerInteractor.authenticateAttempt = success underTest =