Add enroll() in FingerprintManagerInteractor.

Test: atest FingerprintManagerInteractorTest
Bug: 295206773

Change-Id: If2fc46b1c952c3e55c698a18e125e194efe5ffb6
This commit is contained in:
Hao Dong
2023-08-28 17:41:45 +00:00
parent 26cd19552b
commit 2200f55ffe
13 changed files with 307 additions and 37 deletions

View File

@@ -27,12 +27,18 @@ import android.util.Log
import com.android.settings.biometrics.GatekeeperPasswordProvider import com.android.settings.biometrics.GatekeeperPasswordProvider
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel 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.FingerprintViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.EnrollReason
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerEnrollStateViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.toOriginalReason
import com.android.settings.password.ChooseLockSettingsHelper import com.android.settings.password.ChooseLockSettingsHelper
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -66,6 +72,16 @@ interface FingerprintManagerInteractor {
*/ */
suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray>
/**
* Runs [FingerprintManager.enroll] with the [hardwareAuthToken] and [EnrollReason] for this
* enrollment. Returning the [FingerEnrollStateViewModel] that represents this fingerprint
* enrollment state.
*/
suspend fun enroll(
hardwareAuthToken: ByteArray?,
enrollReason: EnrollReason,
): Flow<FingerEnrollStateViewModel>
/** /**
* Removes the given fingerprint, returning true if it was successfully removed and false * Removes the given fingerprint, returning true if it was successfully removed and false
* otherwise * otherwise
@@ -133,6 +149,51 @@ class FingerprintManagerInteractorImpl(
override val maxEnrollableFingerprints = flow { emit(maxFingerprints) } override val maxEnrollableFingerprints = flow { emit(maxFingerprints) }
override suspend fun enroll(
hardwareAuthToken: ByteArray?,
enrollReason: EnrollReason,
): Flow<FingerEnrollStateViewModel> = callbackFlow {
var streamEnded = false
val enrollmentCallback =
object : FingerprintManager.EnrollmentCallback() {
override fun onEnrollmentProgress(remaining: Int) {
trySend(FingerEnrollStateViewModel.EnrollProgress(remaining)).onFailure { error ->
Log.d(TAG, "onEnrollmentProgress($remaining) failed to send, due to $error")
}
if (remaining == 0) {
streamEnded = true
}
}
override fun onEnrollmentHelp(helpMsgId: Int, helpString: CharSequence?) {
trySend(FingerEnrollStateViewModel.EnrollHelp(helpMsgId, helpString.toString()))
.onFailure { error -> Log.d(TAG, "onEnrollmentHelp failed to send, due to $error") }
}
override fun onEnrollmentError(errMsgId: Int, errString: CharSequence?) {
trySend(FingerEnrollStateViewModel.EnrollError(errMsgId, errString.toString()))
.onFailure { error -> Log.d(TAG, "onEnrollmentError failed to send, due to $error") }
streamEnded = true
}
}
val cancellationSignal = CancellationSignal()
fingerprintManager.enroll(
hardwareAuthToken,
cancellationSignal,
applicationContext.userId,
enrollmentCallback,
enrollReason.toOriginalReason()
)
awaitClose {
// If the stream has not been ended, and the user has stopped collecting the flow
// before it was over, send cancel.
if (!streamEnded) {
cancellationSignal.cancel()
}
}
}
override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean = suspendCoroutine { override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean = suspendCoroutine {
val callback = val callback =
object : RemovalCallback() { object : RemovalCallback() {

View File

@@ -43,12 +43,12 @@ import com.android.settings.biometrics.fingerprint2.domain.interactor.Fingerprin
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollConfirmationV2Fragment import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollConfirmationV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollEnrollingV2Fragment 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.FingerprintEnrollFindSensorV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollmentIntroV2Fragment import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollIntroV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Confirmation import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Confirmation
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Education 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.Enrollment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollmentNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintGatekeeperViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintGatekeeperViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Finish import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Finish
@@ -70,7 +70,7 @@ private const val TAG = "FingerprintEnrollmentV2Activity"
* children fragments. * children fragments.
*/ */
class FingerprintEnrollmentV2Activity : FragmentActivity() { class FingerprintEnrollmentV2Activity : FragmentActivity() {
private lateinit var navigationViewModel: FingerprintEnrollmentNavigationViewModel private lateinit var navigationViewModel: FingerprintEnrollNavigationViewModel
private lateinit var gatekeeperViewModel: FingerprintGatekeeperViewModel private lateinit var gatekeeperViewModel: FingerprintGatekeeperViewModel
private val coroutineDispatcher = Dispatchers.Default private val coroutineDispatcher = Dispatchers.Default
@@ -170,18 +170,18 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
navigationViewModel = navigationViewModel =
ViewModelProvider( ViewModelProvider(
this, this,
FingerprintEnrollmentNavigationViewModel.FingerprintEnrollmentNavigationViewModelFactory( FingerprintEnrollNavigationViewModel.FingerprintEnrollNavigationViewModelFactory(
backgroundDispatcher, backgroundDispatcher,
interactor, interactor,
gatekeeperViewModel, gatekeeperViewModel,
gatekeeperInfo is GatekeeperInfo.GatekeeperPasswordInfo, /* canSkipConfirm */ gatekeeperInfo is GatekeeperInfo.GatekeeperPasswordInfo, /* canSkipConfirm */
) )
)[FingerprintEnrollmentNavigationViewModel::class.java] )[FingerprintEnrollNavigationViewModel::class.java]
// Initialize FingerprintViewModel // Initialize FingerprintViewModel
ViewModelProvider( ViewModelProvider(
this, this,
FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory(interactor) FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory(interactor, backgroundDispatcher)
)[FingerprintEnrollViewModel::class.java] )[FingerprintEnrollViewModel::class.java]
// Initialize scroll view model // Initialize scroll view model
@@ -198,7 +198,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
Confirmation -> FingerprintEnrollConfirmationV2Fragment::class.java as Class<Fragment> Confirmation -> FingerprintEnrollConfirmationV2Fragment::class.java as Class<Fragment>
Education -> FingerprintEnrollFindSensorV2Fragment::class.java as Class<Fragment> Education -> FingerprintEnrollFindSensorV2Fragment::class.java as Class<Fragment>
Enrollment -> FingerprintEnrollEnrollingV2Fragment::class.java as Class<Fragment> Enrollment -> FingerprintEnrollEnrollingV2Fragment::class.java as Class<Fragment>
Intro -> FingerprintEnrollmentIntroV2Fragment::class.java as Class<Fragment> Intro -> FingerprintEnrollIntroV2Fragment::class.java as Class<Fragment>
else -> null else -> null
} }

View File

@@ -19,7 +19,7 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollmentNavigationViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel
/** /**
* A fragment to indicate that fingerprint enrollment has been completed. * A fragment to indicate that fingerprint enrollment has been completed.
@@ -33,7 +33,7 @@ class FingerprintEnrollConfirmationV2Fragment : Fragment() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (savedInstanceState == null) { if (savedInstanceState == null) {
val navigationViewModel = val navigationViewModel =
ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java] ViewModelProvider(requireActivity())[FingerprintEnrollNavigationViewModel::class.java]
} }
} }
} }

View File

@@ -19,7 +19,7 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollmentNavigationViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel
/** A fragment that is responsible for enrolling a users fingerprint. */ /** A fragment that is responsible for enrolling a users fingerprint. */
class FingerprintEnrollEnrollingV2Fragment : Fragment() { class FingerprintEnrollEnrollingV2Fragment : Fragment() {
@@ -28,7 +28,7 @@ class FingerprintEnrollEnrollingV2Fragment : Fragment() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (savedInstanceState == null) { if (savedInstanceState == null) {
val navigationViewModel = val navigationViewModel =
ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java] ViewModelProvider(requireActivity())[FingerprintEnrollNavigationViewModel::class.java]
} }
} }
} }

View File

@@ -20,7 +20,7 @@ import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.android.settings.R import com.android.settings.R
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollmentNavigationViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel
/** /**
* A fragment that is used to educate the user about the fingerprint sensor on this device. * A fragment that is used to educate the user about the fingerprint sensor on this device.
@@ -36,7 +36,7 @@ class FingerprintEnrollFindSensorV2Fragment : Fragment(R.layout.fingerprint_v2_e
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (savedInstanceState == null) { if (savedInstanceState == null) {
val navigationViewModel = val navigationViewModel =
ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java] ViewModelProvider(requireActivity())[FingerprintEnrollNavigationViewModel::class.java]
} }
} }
} }

View File

@@ -33,8 +33,8 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.android.settings.R import com.android.settings.R
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollmentNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintGatekeeperViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintGatekeeperViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Unicorn import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Unicorn
@@ -72,10 +72,10 @@ private data class TextModel(
* 2. How the data will be stored * 2. How the data will be stored
* 3. How the user can access and remove their data * 3. How the user can access and remove their data
*/ */
class FingerprintEnrollmentIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll_introduction) { class FingerprintEnrollIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll_introduction) {
private lateinit var footerBarMixin: FooterBarMixin private lateinit var footerBarMixin: FooterBarMixin
private lateinit var textModel: TextModel private lateinit var textModel: TextModel
private lateinit var navigationViewModel: FingerprintEnrollmentNavigationViewModel private lateinit var navigationViewModel: FingerprintEnrollNavigationViewModel
private lateinit var fingerprintEnrollViewModel: FingerprintEnrollViewModel private lateinit var fingerprintEnrollViewModel: FingerprintEnrollViewModel
private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel
private lateinit var gateKeeperViewModel: FingerprintGatekeeperViewModel private lateinit var gateKeeperViewModel: FingerprintGatekeeperViewModel
@@ -83,7 +83,7 @@ class FingerprintEnrollmentIntroV2Fragment : Fragment(R.layout.fingerprint_v2_en
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
navigationViewModel = navigationViewModel =
ViewModelProvider(requireActivity())[FingerprintEnrollmentNavigationViewModel::class.java] ViewModelProvider(requireActivity())[FingerprintEnrollNavigationViewModel::class.java]
fingerprintEnrollViewModel = fingerprintEnrollViewModel =
ViewModelProvider(requireActivity())[FingerprintEnrollViewModel::class.java] ViewModelProvider(requireActivity())[FingerprintEnrollViewModel::class.java]
fingerprintScrollViewModel = fingerprintScrollViewModel =

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel
import android.hardware.fingerprint.FingerprintManager
/**
* The reason for enrollment. Represents [FingerprintManager.EnrollReason]
*/
enum class EnrollReason {
/** The enroll happens on education screen. */
FindSensor,
/** The enroll happens on enrolling screen. */
EnrollEnrolling
}
/** Convert EnrollReason to original [FingerprintManager.EnrollReason]. */
fun EnrollReason.toOriginalReason(): Int {
return when (this) {
EnrollReason.EnrollEnrolling -> FingerprintManager.ENROLL_ENROLL
EnrollReason.FindSensor -> FingerprintManager.ENROLL_FIND_SENSOR
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel
import android.annotation.StringRes
/**
* Represents a fingerprint enrollment state. See [FingerprintManager.EnrollmentCallback] for more
* information
*/
sealed class FingerEnrollStateViewModel {
/** Represents enrollment step progress. */
data class EnrollProgress(
val remainingSteps: Int,
) : FingerEnrollStateViewModel()
/** Represents that recoverable error has been encountered during enrollment. */
data class EnrollHelp(
@StringRes val helpMsgId: Int,
val helpString: String,
) : FingerEnrollStateViewModel()
/** Represents that an unrecoverable error has been encountered and the operation is complete. */
data class EnrollError(
@StringRes val errMsgId: Int,
val errString: String,
) : FingerEnrollStateViewModel()
}

View File

@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel package com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -21,13 +20,24 @@ import androidx.lifecycle.ViewModelProvider
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.toSensorType import com.android.systemui.biometrics.shared.model.toSensorType
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
/** Represents all of the fingerprint information needed for fingerprint enrollment. */ private const val TAG = "FingerprintEnrollViewModel"
class FingerprintEnrollViewModel(fingerprintManagerInteractor: FingerprintManagerInteractor) :
ViewModel() { /** Represents all of the fingerprint information needed for a fingerprint enrollment process. */
class FingerprintEnrollViewModel(
private val fingerprintManagerInteractor: FingerprintManagerInteractor,
backgroundDispatcher: CoroutineDispatcher,
) : ViewModel() {
/** Represents the stream of [FingerprintSensorType] */ /** Represents the stream of [FingerprintSensorType] */
val sensorType: Flow<FingerprintSensorType> = val sensorType: Flow<FingerprintSensorType> =
@@ -35,14 +45,55 @@ class FingerprintEnrollViewModel(fingerprintManagerInteractor: FingerprintManage
it.sensorType.toSensorType() it.sensorType.toSensorType()
} }
class FingerprintEnrollViewModelFactory(val interactor: FingerprintManagerInteractor) : private var _enrollReason: MutableStateFlow<EnrollReason> =
ViewModelProvider.Factory { MutableStateFlow(EnrollReason.FindSensor)
private var _hardwareAuthToken: MutableStateFlow<ByteArray?> = MutableStateFlow(null)
private var _consumerShouldEnroll: MutableStateFlow<Boolean> = MutableStateFlow(false)
/**
* A flow that contains a [FingerprintEnrollViewModel] which contains the relevant information for
* an enrollment process
*/
val enrollFlow: Flow<FingerEnrollStateViewModel> =
combine(_consumerShouldEnroll, _hardwareAuthToken, _enrollReason) {
consumerShouldEnroll,
hardwareAuthToken,
enrollReason ->
Triple(consumerShouldEnroll, hardwareAuthToken, enrollReason)
}
.transformLatest {
// transformLatest() instead of transform() is used here for cancelling previous enroll()
// whenever |consumerShouldEnroll| is changed. Otherwise the latest value will be suspended
// since enroll() is an infinite callback flow.
(consumerShouldEnroll, hardwareAuthToken, enrollReason) ->
if (consumerShouldEnroll && hardwareAuthToken != null) {
fingerprintManagerInteractor.enroll(hardwareAuthToken, enrollReason).collect { emit(it) }
}
}
.flowOn(backgroundDispatcher)
/** Used to indicate the consumer of the view model is ready for an enrollment. */
fun startEnroll(hardwareAuthToken: ByteArray?, enrollReason: EnrollReason) {
_enrollReason.update { enrollReason }
_hardwareAuthToken.update { hardwareAuthToken }
// Update _consumerShouldEnroll after updating the other values.
_consumerShouldEnroll.update { true }
}
/** Used to indicate to stop the enrollment. */
fun stopEnroll() {
_consumerShouldEnroll.update { false }
}
class FingerprintEnrollViewModelFactory(
val interactor: FingerprintManagerInteractor,
val backgroundDispatcher: CoroutineDispatcher
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create( override fun <T : ViewModel> create(
modelClass: Class<T>, modelClass: Class<T>,
): T { ): T {
return FingerprintEnrollViewModel(interactor) as T return FingerprintEnrollViewModel(interactor, backgroundDispatcher) as T
} }
} }
} }

View File

@@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
const val TAG = "FingerprintEnrollmentNavigationViewModel" private const val TAG = "FingerprintEnrollNavigationViewModel"
/** Interface to validate a gatekeeper hat */ /** Interface to validate a gatekeeper hat */
interface Validator { interface Validator {
@@ -54,7 +54,7 @@ object Unicorn : EnrollType()
* This class is responsible for sending a [NavigationStep] which indicates where the user is in the * This class is responsible for sending a [NavigationStep] which indicates where the user is in the
* Fingerprint Enrollment flow * Fingerprint Enrollment flow
*/ */
class FingerprintEnrollmentNavigationViewModel( class FingerprintEnrollNavigationViewModel(
private val dispatcher: CoroutineDispatcher, private val dispatcher: CoroutineDispatcher,
private val validator: Validator, private val validator: Validator,
private val fingerprintManagerInteractor: FingerprintManagerInteractor, private val fingerprintManagerInteractor: FingerprintManagerInteractor,
@@ -131,7 +131,7 @@ class FingerprintEnrollmentNavigationViewModel(
} }
} }
class FingerprintEnrollmentNavigationViewModelFactory( class FingerprintEnrollNavigationViewModelFactory(
private val backgroundDispatcher: CoroutineDispatcher, private val backgroundDispatcher: CoroutineDispatcher,
private val fingerprintManagerInteractor: FingerprintManagerInteractor, private val fingerprintManagerInteractor: FingerprintManagerInteractor,
private val fingerprintGatekeeperViewModel: FingerprintGatekeeperViewModel, private val fingerprintGatekeeperViewModel: FingerprintGatekeeperViewModel,
@@ -143,7 +143,7 @@ class FingerprintEnrollmentNavigationViewModel(
modelClass: Class<T>, modelClass: Class<T>,
): T { ): T {
return FingerprintEnrollmentNavigationViewModel( return FingerprintEnrollNavigationViewModel(
backgroundDispatcher, backgroundDispatcher,
object : Validator { object : Validator {
override fun validateGateKeeper(challenge: Long?): Boolean { override fun validateGateKeeper(challenge: Long?): Boolean {

View File

@@ -29,6 +29,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private const val TAG = "FingerprintGatekeeperViewModel"
sealed interface GatekeeperInfo { sealed interface GatekeeperInfo {
object Invalid : GatekeeperInfo object Invalid : GatekeeperInfo
object Timeout : GatekeeperInfo object Timeout : GatekeeperInfo

View File

@@ -22,6 +22,8 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.EnrollReason
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerEnrollStateViewModel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@@ -32,6 +34,7 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
var enrolledFingerprintsInternal: MutableList<FingerprintViewModel> = mutableListOf() var enrolledFingerprintsInternal: MutableList<FingerprintViewModel> = mutableListOf()
var challengeToGenerate: Pair<Long, ByteArray> = Pair(-1L, byteArrayOf()) var challengeToGenerate: Pair<Long, ByteArray> = Pair(-1L, byteArrayOf())
var authenticateAttempt = FingerprintAuthAttemptViewModel.Success(1) var authenticateAttempt = FingerprintAuthAttemptViewModel.Success(1)
val enrollStateViewModel = FingerEnrollStateViewModel.EnrollProgress(1)
var pressToAuthEnabled = true var pressToAuthEnabled = true
var sensorProps = var sensorProps =
@@ -68,6 +71,11 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
override val maxEnrollableFingerprints: Flow<Int> = flow { emit(enrollableFingerprints) } override val maxEnrollableFingerprints: Flow<Int> = flow { emit(enrollableFingerprints) }
override suspend fun enroll(
hardwareAuthToken: ByteArray?,
enrollReason: EnrollReason
): Flow<FingerEnrollStateViewModel> = flow { emit(enrollStateViewModel) }
override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean { override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean {
return enrolledFingerprintsInternal.remove(fp) return enrolledFingerprintsInternal.remove(fp)
} }

View File

@@ -30,6 +30,8 @@ import com.android.settings.biometrics.fingerprint2.domain.interactor.Fingerprin
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
import com.android.settings.biometrics.fingerprint2.shared.model.FingerprintAuthAttemptViewModel 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.FingerprintViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.EnrollReason.FindSensor
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerEnrollStateViewModel
import com.android.settings.password.ChooseLockSettingsHelper import com.android.settings.password.ChooseLockSettingsHelper
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
@@ -143,7 +145,7 @@ class FingerprintManagerInteractorTest {
.thenReturn(byteArray) .thenReturn(byteArray)
val generateChallengeCallback: ArgumentCaptor<FingerprintManager.GenerateChallengeCallback> = val generateChallengeCallback: ArgumentCaptor<FingerprintManager.GenerateChallengeCallback> =
ArgumentCaptor.forClass(FingerprintManager.GenerateChallengeCallback::class.java) argumentCaptor()
var result: Pair<Long, ByteArray?>? = null var result: Pair<Long, ByteArray?>? = null
val job = testScope.launch { result = underTest.generateChallenge(1L) } val job = testScope.launch { result = underTest.generateChallenge(1L) }
@@ -165,8 +167,7 @@ class FingerprintManagerInteractorTest {
val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L) val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L)
val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L) val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L)
val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> = val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> = argumentCaptor()
ArgumentCaptor.forClass(FingerprintManager.RemovalCallback::class.java)
var result: Boolean? = null var result: Boolean? = null
val job = val job =
@@ -189,8 +190,7 @@ class FingerprintManagerInteractorTest {
val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L) val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L)
val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L) val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L)
val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> = val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> = argumentCaptor()
ArgumentCaptor.forClass(FingerprintManager.RemovalCallback::class.java)
var result: Boolean? = null var result: Boolean? = null
val job = val job =
@@ -229,8 +229,7 @@ class FingerprintManagerInteractorTest {
var result: FingerprintAuthAttemptViewModel? = null var result: FingerprintAuthAttemptViewModel? = null
val job = launch { result = underTest.authenticate() } val job = launch { result = underTest.authenticate() }
val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> = val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> = argumentCaptor()
ArgumentCaptor.forClass(FingerprintManager.AuthenticationCallback::class.java)
runCurrent() runCurrent()
@@ -257,8 +256,7 @@ class FingerprintManagerInteractorTest {
var result: FingerprintAuthAttemptViewModel? = null var result: FingerprintAuthAttemptViewModel? = null
val job = launch { result = underTest.authenticate() } val job = launch { result = underTest.authenticate() }
val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> = val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> = argumentCaptor()
ArgumentCaptor.forClass(FingerprintManager.AuthenticationCallback::class.java)
runCurrent() runCurrent()
@@ -280,8 +278,82 @@ class FingerprintManagerInteractorTest {
) )
} }
@Test
fun testEnroll_progress() =
testScope.runTest {
val token = byteArrayOf(5, 3, 2)
var result: FingerEnrollStateViewModel? = null
val job = launch { underTest.enroll(token, FindSensor).collect { result = it } }
val enrollCallback: ArgumentCaptor<FingerprintManager.EnrollmentCallback> = argumentCaptor()
runCurrent()
verify(fingerprintManager)
.enroll(
eq(token),
any(CancellationSignal::class.java),
anyInt(),
capture(enrollCallback),
eq(FingerprintManager.ENROLL_FIND_SENSOR)
)
enrollCallback.value.onEnrollmentProgress(1)
runCurrent()
job.cancelAndJoin()
assertThat(result).isEqualTo(FingerEnrollStateViewModel.EnrollProgress(1))
}
@Test
fun testEnroll_help() =
testScope.runTest {
val token = byteArrayOf(5, 3, 2)
var result: FingerEnrollStateViewModel? = null
val job = launch { underTest.enroll(token, FindSensor).collect { result = it } }
val enrollCallback: ArgumentCaptor<FingerprintManager.EnrollmentCallback> = argumentCaptor()
runCurrent()
verify(fingerprintManager)
.enroll(
eq(token),
any(CancellationSignal::class.java),
anyInt(),
capture(enrollCallback),
eq(FingerprintManager.ENROLL_FIND_SENSOR)
)
enrollCallback.value.onEnrollmentHelp(-1, "help")
runCurrent()
job.cancelAndJoin()
assertThat(result).isEqualTo(FingerEnrollStateViewModel.EnrollHelp(-1, "help"))
}
@Test
fun testEnroll_error() =
testScope.runTest {
val token = byteArrayOf(5, 3, 2)
var result: FingerEnrollStateViewModel? = null
val job = launch { underTest.enroll(token, FindSensor).collect { result = it } }
val enrollCallback: ArgumentCaptor<FingerprintManager.EnrollmentCallback> = argumentCaptor()
runCurrent()
verify(fingerprintManager)
.enroll(
eq(token),
any(CancellationSignal::class.java),
anyInt(),
capture(enrollCallback),
eq(FingerprintManager.ENROLL_FIND_SENSOR)
)
enrollCallback.value.onEnrollmentError(-2, "error")
runCurrent()
job.cancelAndJoin()
assertThat(result).isEqualTo(FingerEnrollStateViewModel.EnrollError(-2, "error"))
}
private fun <T : Any> safeEq(value: T): T = eq(value) ?: value private fun <T : Any> safeEq(value: T): T = eq(value) ?: value
private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() 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> any(type: Class<T>): T = Mockito.any<T>(type)
private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall) private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
ArgumentCaptor.forClass(T::class.java)
} }