Rear Fingerprint Enrollment

Bug: 297083009
Test: atest RFPSIconTouchViewModelTest FingerprintEnrollEnrollingViewModelTest FingerprintManagerInteractorTest
Change-Id: Icc072e7d7815070087ccb50ea5937c386b06fb11
This commit is contained in:
Joshua McCloskey
2023-10-02 18:20:17 +00:00
parent f29e758da9
commit a98dc8d4b5
41 changed files with 1935 additions and 378 deletions

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<com.google.android.setupdesign.GlifLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="?attr/fingerprint_layout_theme"
android:id="@+id/setup_wizard_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/SudContentFrame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<com.google.android.setupdesign.view.FillContentLayout
android:layout_width="@dimen/fingerprint_progress_bar_max_size"
android:layout_height="@dimen/fingerprint_progress_bar_max_size"
android:layout_marginVertical="24dp"
android:paddingTop="0dp"
android:paddingBottom="0dp">
<com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.RFPSProgressBar
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fingerprint_progress_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/fp_illustration"
android:minHeight="@dimen/fingerprint_progress_bar_min_size"
android:progress="0" />
</com.google.android.setupdesign.view.FillContentLayout>
<TextView
android:id="@+id/text"
style="@style/TextAppearance.ErrorText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:accessibilityLiveRegion="polite"
android:gravity="center"
android:visibility="invisible" />
</LinearLayout>
</LinearLayout>
</com.google.android.setupdesign.GlifLayout>

View File

@@ -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 {
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,
)
}
}

View File

@@ -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<Long, ByteArray> =
suspendCoroutine {
val callback = GenerateChallengeCallback { _, userId, challenge ->
@@ -75,11 +85,11 @@ class FingerprintManagerInteractorImpl(
fingerprintManager.generateChallenge(applicationContext.userId, callback)
}
override val enrolledFingerprints: Flow<List<FingerprintViewModel>> = flow {
override val enrolledFingerprints: Flow<List<FingerprintData>> = 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<FingerEnrollStateViewModel> = callbackFlow {
): Flow<FingerEnrollState> = 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<FingerprintAuthAttemptViewModel> ->
override suspend fun authenticate(): FingerprintAuthAttemptModel =
suspendCancellableCoroutine { c: CancellableContinuation<FingerprintAuthAttemptModel> ->
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))
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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<List<FingerprintViewModel>>
val enrolledFingerprints: Flow<List<FingerprintData>>
/** Returns the max enrollable fingerprints, note during SUW this might be 1 */
val maxEnrollableFingerprints: Flow<Int>
@@ -43,7 +43,7 @@ interface FingerprintManagerInteractor {
val sensorPropertiesInternal: Flow<FingerprintSensor?>
/** 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<FingerEnrollStateViewModel>
): Flow<FingerEnrollState>
/**
* 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

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()

View File

@@ -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,
GatekeeperPasswordProvider(LockPatternUtils(context)),
PressToAuthProviderImpl(context),
enrollType,
)
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
}
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,22 +244,38 @@ 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
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<Fragment>? =
when (currStep) {
Confirmation -> FingerprintEnrollConfirmationV2Fragment::class.java as Class<Fragment>
Education -> FingerprintEnrollFindSensorV2Fragment::class.java as Class<Fragment>
Enrollment -> FingerprintEnrollEnrollingV2Fragment::class.java as Class<Fragment>
is Enrollment -> {
when (sensorType) {
FingerprintSensorType.REAR -> RFPSEnrollFragment::class.java as Class<Fragment>
else -> FingerprintEnrollEnrollingV2Fragment::class.java as Class<Fragment>
}
}
Intro -> FingerprintEnrollIntroV2Fragment::class.java as Class<Fragment>
else -> null
}
@@ -261,6 +284,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
supportFragmentManager.fragments.onEach { fragment ->
supportFragmentManager.beginTransaction().remove(fragment).commit()
}
supportFragmentManager
.beginTransaction()
.setReorderingAllowed(true)

View File

@@ -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 =
private val viewModel: FingerprintEnrollFindSensorViewModel by lazy {
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
}
}
}
}
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()
}
}

View File

@@ -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)

View File

@@ -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
```

View File

@@ -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()
}
}

View File

@@ -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<Int> = 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<Boolean> =
_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 <T : ViewModel> create(modelClass: Class<T>): T {
return RFPSIconTouchViewModel() as T
}
}
}

View File

@@ -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<Boolean>(false)
val textViewIsVisible: Flow<Boolean> = _textViewIsVisible.asStateFlow()
/** Indicates if the icon should be animating or not */
val shouldAnimateIcon = fingerprintEnrollViewModel.enrollFlowShouldBeRunning
private val enrollFlow: Flow<FingerEnrollState?> = 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<FingerEnrollState.EnrollProgress?> =
enrollFlow
.filterIsInstance<FingerEnrollState.EnrollProgress>()
.shareIn(viewModelScope, SharingStarted.Eagerly, 1)
/** Clear help message on enroll progress */
val clearHelpMessage: Flow<Boolean> = progress.map { it != null }
/** Enroll help message that is only displayed once */
val helpMessage: Flow<FingerEnrollState.EnrollHelp?> =
enrollFlow
.filterIsInstance<FingerEnrollState.EnrollHelp>()
.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<FingerEnrollState.EnrollError?> =
enrollFlow
.filterIsInstance<FingerEnrollState.EnrollError>()
.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 <T : ViewModel> create(
modelClass: Class<T>,
): T {
return RFPSViewModel(fingerprintEnrollEnrollingViewModel) as T
}
}
}

View File

@@ -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())
}
}
}

View File

@@ -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())
}
}
}

View File

@@ -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()
}
}

View File

@@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
return BackgroundViewModel() as T
}
}
}

View File

@@ -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<Boolean> =
_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 <T : ViewModel> create(
modelClass: Class<T>,
): T {
return FingerprintEnrollEnrollingViewModel(fingerprintEnrollViewModel, backgroundViewModel)
as T
}
}
}

View File

@@ -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<Boolean> = MutableStateFlow(false)
/** Indicates if the education flow should be running. */
private val educationFlowShouldBeRunning: Flow<Boolean> =
_education.combine(backgroundViewModel.background) { shouldRunEducation, isInBackground ->
!isInBackground && shouldRunEducation
}
init {
// Start or end enroll flow
viewModelScope.launch {
@@ -107,39 +115,57 @@ class FingerprintEnrollFindSensorViewModel(
}
.collect { token ->
if (token != null) {
fingerprintEnrollViewModel.startEnroll(token, EnrollReason.FindSensor)
canStartEducation()
} else {
fingerprintEnrollViewModel.stopEnroll()
stopEducation()
}
}
}
// Enroll progress flow
viewModelScope.launch {
educationFlowShouldBeRunning.collect {
// Only collect the flow when we should be running.
if (it) {
combine(
navigationViewModel.enrollType,
fingerprintEnrollViewModel.enrollFlow.filterNotNull()
) { enrollType, enrollFlow ->
Pair(enrollType, enrollFlow)
navigationViewModel.fingerprintFlow,
fingerprintEnrollViewModel.educationEnrollFlow.filterNotNull(),
) { enrollType, educationFlow ->
Pair(enrollType, educationFlow)
}
.collect { (enrollType, enrollFlow) ->
when (enrollFlow) {
// TODO: Cancel the enroll() when EnrollProgress is received instead of proceeding to
.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 FingerEnrollStateViewModel.EnrollProgress -> proceedToEnrolling()
is FingerEnrollStateViewModel.EnrollError -> {
val errMsgId = enrollFlow.errMsgId
if (errMsgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
is FingerEnrollState.EnrollProgress -> proceedToEnrolling()
is FingerEnrollState.EnrollError -> {
if (educationFlow.isCancelled) {
proceedToEnrolling()
} else {
_showErrorDialog.update { Pair(errMsgId, enrollType == SetupWizard) }
_showErrorDialog.update { Pair(educationFlow.errString, enrollType == SetupWizard) }
}
}
is FingerEnrollStateViewModel.EnrollHelp -> {}
is FingerEnrollState.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() {
@@ -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

View File

@@ -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<EnrollReason> =
MutableStateFlow(EnrollReason.FindSensor)
private var _hardwareAuthToken: MutableStateFlow<ByteArray?> = MutableStateFlow(null)
private var _consumerShouldEnroll: MutableStateFlow<Boolean> = 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<EnrollReason?> =
navigationViewModel.navigationViewModel.map {
when (it.currStep) {
is Enrollment -> EnrollReason.EnrollEnrolling
is Education -> EnrollReason.FindSensor
else -> null
}
}
/** Represents the stream of [FingerprintSensorType] */
val sensorType: Flow<FingerprintSensorType> =
@@ -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<FingerEnrollStateViewModel> =
combine(_consumerShouldEnroll, _hardwareAuthToken, _enrollReason) {
consumerShouldEnroll,
hardwareAuthToken,
enrollReason ->
Triple(consumerShouldEnroll, hardwareAuthToken, enrollReason)
val _enrollFlow: Flow<FingerEnrollState> =
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<FingerEnrollState?> =
_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<FingerEnrollState?> =
_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 <T : ViewModel> create(
modelClass: Class<T>,
): T {
return FingerprintEnrollViewModel(interactor, backgroundDispatcher) as T
return FingerprintEnrollViewModel(
interactor,
gatekeeperViewModel,
navigationViewModel,
)
as T
}
}
}

View File

@@ -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<EnrollType?>(Default)
private var _fingerprintFlow = MutableStateFlow<FingerprintFlow?>(theFingerprintFlow)
/** A flow that indicates the [EnrollType] */
val enrollType: Flow<EnrollType?> = _enrollType.asStateFlow()
private var navState = NavState(canSkipConfirm)
/** A flow that indicates the [FingerprintFlow] */
val fingerprintFlow: Flow<FingerprintFlow?> = _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> = _navigationStep.asStateFlow()
/** This action indicates that the UI should actually update the navigation to the given step. */
val navigationAction: Flow<NavigationStep?> =
_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>,
): T {
val navState = NavState(canSkipConfirm)
return FingerprintEnrollNavigationViewModel(
backgroundDispatcher,
fingerprintManagerInteractor,
fingerprintGatekeeperViewModel,
canSkipConfirm,
Start.next(navState),
navState,
fingerprintFlow,
)
as T
}

View File

@@ -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
}

View File

@@ -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<FingerprintViewModel>)
fun showSettings(enrolledFingerprints: List<FingerprintData>)
/** 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, String>?
fingerprintViewModel: FingerprintData
): Pair<FingerprintData, String>?
}
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)
}

View File

@@ -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,7 +95,7 @@ class FingerprintDeletionDialog : InstrumentedDialogFragment() {
companion object {
private const val KEY_FINGERPRINT = "fingerprint"
suspend fun showInstance(
fp: FingerprintViewModel,
fp: FingerprintData,
lastFingerprint: Boolean,
target: FingerprintSettingsV2Fragment,
) = suspendCancellableCoroutine { continuation ->

View File

@@ -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
@@ -31,7 +31,7 @@ private const val TAG = "FingerprintSettingsPreference"
class FingerprintSettingsPreference(
context: Context,
val fingerprintViewModel: FingerprintViewModel,
val fingerprintViewModel: FingerprintData,
val fragment: FingerprintSettingsV2Fragment,
val isLastFingerprint: Boolean
) : TwoTargetPreference(context) {
@@ -79,7 +79,7 @@ class FingerprintSettingsPreference(
return FingerprintDeletionDialog.showInstance(fingerprintViewModel, isLastFingerprint, fragment)
}
suspend fun askUserToRenameDialog(): Pair<FingerprintViewModel, String>? {
suspend fun askUserToRenameDialog(): Pair<FingerprintData, String>? {
return FingerprintSettingsRenameDialog.showInstance(fingerprintViewModel, fragment)
}
}

View File

@@ -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 =

View File

@@ -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,14 +188,10 @@ 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 interactor =
FingerprintManagerInteractorImpl(
context.applicationContext,
backgroundDispatcher,
fingerprintManager,
GatekeeperPasswordProvider(LockPatternUtils(context.applicationContext))
) {
val pressToAuthProvider = {
var toReturn: Int =
Secure.getIntForUser(
context.contentResolver,
@@ -221,6 +219,16 @@ class FingerprintSettingsV2Fragment :
toReturn == 1
}
val interactor =
FingerprintManagerInteractorImpl(
context.applicationContext,
backgroundDispatcher,
fingerprintManager,
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<FingerprintViewModel>) {
override fun showSettings(enrolledFingerprints: List<FingerprintData>) {
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, String>? {
fingerprintViewModel: FingerprintData
): Pair<FingerprintData, String>? {
Log.d(TAG, "showing rename dialog for (${fingerprintViewModel})")
try {
val toReturn =

View File

@@ -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<List<FingerprintViewModel>?> =
private val _enrolledFingerprints: MutableStateFlow<List<FingerprintData>?> =
MutableStateFlow(null)
/** Represents the stream of enrolled fingerprints. */
val enrolledFingerprints: Flow<List<FingerprintViewModel>> =
val enrolledFingerprints: Flow<List<FingerprintData>> =
_enrolledFingerprints.asStateFlow().filterNotNull().filterOnlyWhenSettingsIsShown()
/** Represents the stream of the information of "Add Fingerprint" preference. */
@@ -95,10 +95,10 @@ class FingerprintSettingsViewModel(
private val _sensorNullOrEmpty: Flow<Boolean> =
fingerprintManagerInteractor.sensorPropertiesInternal.map { it == null }
private val _isLockedOut: MutableStateFlow<FingerprintAuthAttemptViewModel.Error?> =
private val _isLockedOut: MutableStateFlow<FingerprintAuthAttemptModel.Error?> =
MutableStateFlow(null)
private val _authSucceeded: MutableSharedFlow<FingerprintAuthAttemptViewModel.Success?> =
private val _authSucceeded: MutableSharedFlow<FingerprintAuthAttemptModel.Success?> =
MutableSharedFlow()
private val _attemptsSoFar: MutableStateFlow<Int> = MutableStateFlow(0)
@@ -164,7 +164,7 @@ class FingerprintSettingsViewModel(
.distinctUntilChanged()
/** Represents a consistent stream of authentication attempts. */
val authFlow: Flow<FingerprintAuthAttemptViewModel> =
val authFlow: Flow<FingerprintAuthAttemptModel> =
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 }
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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<FingerprintViewModel> = mutableListOf()
var enrolledFingerprintsInternal: MutableList<FingerprintData> = mutableListOf()
var challengeToGenerate: Pair<Long, ByteArray> = Pair(-1L, byteArrayOf())
var authenticateAttempt = FingerprintAuthAttemptViewModel.Success(1)
val enrollStateViewModel = FingerEnrollStateViewModel.EnrollProgress(1)
var authenticateAttempt = FingerprintAuthAttemptModel.Success(1)
var enrollStateViewModel: List<FingerEnrollState> =
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<List<FingerprintViewModel>> = flow {
override val enrolledFingerprints: Flow<List<FingerprintData>> = flow {
emit(enrolledFingerprintsInternal)
}
@@ -62,24 +63,22 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
emit(enrolledFingerprintsInternal.size < enrollableFingerprints)
}
override val sensorPropertiesInternal: Flow<FingerprintSensor?> = flow {
emit(sensorProp)
}
override val sensorPropertiesInternal: Flow<FingerprintSensor?> = flow { emit(sensorProp) }
override val maxEnrollableFingerprints: Flow<Int> = flow { emit(enrollableFingerprints) }
override suspend fun enroll(
hardwareAuthToken: ByteArray?,
enrollReason: EnrollReason
): Flow<FingerEnrollStateViewModel> = flowOf(enrollStateViewModel)
): Flow<FingerEnrollState> = 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))
}
}

View File

@@ -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<FingerprintManager.RemovalCallback> = 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<FingerprintManager.RemovalCallback> = 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<FingerprintManager.AuthenticationCallback> = 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<FingerprintManager.AuthenticationCallback> = 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<FingerprintManager.EnrollmentCallback> = 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<FingerprintManager.EnrollmentCallback> = 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<FingerprintManager.EnrollmentCallback> = 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 <T : Any> safeEq(value: T): T = eq(value) ?: value
private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
ArgumentCaptor.forClass(T::class.java)
}

View File

@@ -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()

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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 } }

View File

@@ -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<FingerprintViewModel> {
private fun setupAuth(): MutableList<FingerprintData> {
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 =