Implement basic Fingerprint functionality.
Test: Verified enroll/deletion/renaming/authentication flows. Test: atest FingerprintSettingsViewModelTest Test: atest FingerprintManagerInteractorTest Bug: 280862076 Change-Id: Ic34fd89f01f24468d0f769ef0492e742d9330112
This commit is contained in:
@@ -15,4 +15,37 @@
|
|||||||
~ limitations under the License.
|
~ limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"/>
|
<PreferenceScreen
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:settings="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:title="@string/security_settings_fingerprint_preference_title">
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
android:key="security_settings_fingerprints_enrolled"
|
||||||
|
settings:controller="com.android.settings.biometrics.fingerprint.FingerprintsEnrolledCategoryPreferenceController">
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<androidx.preference.Preference
|
||||||
|
android:icon="@drawable/ic_add_24dp"
|
||||||
|
android:key="key_fingerprint_add"
|
||||||
|
android:title="@string/fingerprint_add_title" />
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
android:key="security_settings_fingerprint_unlock_category"
|
||||||
|
android:title="@string/security_settings_fingerprint_settings_preferences_category"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<com.android.settingslib.RestrictedSwitchPreference
|
||||||
|
android:key="security_settings_require_screen_on_to_auth"
|
||||||
|
android:title="@string/security_settings_require_screen_on_to_auth_title"
|
||||||
|
android:summary="@string/security_settings_require_screen_on_to_auth_description"
|
||||||
|
settings:keywords="@string/security_settings_require_screen_on_to_auth_keywords"
|
||||||
|
settings:controller="com.android.settings.biometrics.fingerprint.FingerprintSettingsRequireScreenOnToAuthPreferenceController" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
android:key="security_settings_fingerprint_footer">
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
|
|
||||||
|
@@ -0,0 +1,207 @@
|
|||||||
|
/*
|
||||||
|
* 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.domain.interactor
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.hardware.fingerprint.FingerprintManager
|
||||||
|
import android.hardware.fingerprint.FingerprintManager.GenerateChallengeCallback
|
||||||
|
import android.hardware.fingerprint.FingerprintManager.RemovalCallback
|
||||||
|
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import android.util.Log
|
||||||
|
import com.android.settings.biometrics.GatekeeperPasswordProvider
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
|
||||||
|
import com.android.settings.password.ChooseLockSettingsHelper
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
private const val TAG = "FingerprintManagerInteractor"
|
||||||
|
|
||||||
|
/** Encapsulates business logic related to managing fingerprints. */
|
||||||
|
interface FingerprintManagerInteractor {
|
||||||
|
/** Returns the list of current fingerprints. */
|
||||||
|
val enrolledFingerprints: Flow<List<FingerprintViewModel>>
|
||||||
|
|
||||||
|
/** Returns the max enrollable fingerprints, note during SUW this might be 1 */
|
||||||
|
val maxEnrollableFingerprints: Flow<Int>
|
||||||
|
|
||||||
|
/** Runs [FingerprintManager.authenticate] */
|
||||||
|
suspend fun authenticate(): FingerprintAuthAttemptViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a challenge with the provided [gateKeeperPasswordHandle] and on success returns a
|
||||||
|
* challenge and challenge token. This info can be used for secure operations such as
|
||||||
|
* [FingerprintManager.enroll]
|
||||||
|
*
|
||||||
|
* @param gateKeeperPasswordHandle GateKeeper password handle generated by a Confirm
|
||||||
|
* @return A [Pair] of the challenge and challenge token
|
||||||
|
*/
|
||||||
|
suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray>
|
||||||
|
|
||||||
|
/** Returns true if a user can enroll a fingerprint false otherwise. */
|
||||||
|
fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given fingerprint, returning true if it was successfully removed and false
|
||||||
|
* otherwise
|
||||||
|
*/
|
||||||
|
suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean
|
||||||
|
|
||||||
|
/** Renames the given fingerprint if one exists */
|
||||||
|
suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String)
|
||||||
|
|
||||||
|
/** Indicates if the device has side fingerprint */
|
||||||
|
suspend fun hasSideFps(): Boolean
|
||||||
|
|
||||||
|
/** Indicates if the press to auth feature has been enabled */
|
||||||
|
suspend fun pressToAuthEnabled(): Boolean
|
||||||
|
|
||||||
|
/** Retrieves the sensor properties of a device */
|
||||||
|
suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal>
|
||||||
|
}
|
||||||
|
|
||||||
|
class FingerprintManagerInteractorImpl(
|
||||||
|
applicationContext: Context,
|
||||||
|
private val backgroundDispatcher: CoroutineDispatcher,
|
||||||
|
private val fingerprintManager: FingerprintManager,
|
||||||
|
private val gatekeeperPasswordProvider: GatekeeperPasswordProvider,
|
||||||
|
private val pressToAuthProvider: () -> Boolean,
|
||||||
|
) : FingerprintManagerInteractor {
|
||||||
|
|
||||||
|
private val maxFingerprints =
|
||||||
|
applicationContext.resources.getInteger(
|
||||||
|
com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser
|
||||||
|
)
|
||||||
|
private val applicationContext = applicationContext.applicationContext
|
||||||
|
|
||||||
|
override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> =
|
||||||
|
suspendCoroutine {
|
||||||
|
val callback = GenerateChallengeCallback { _, userId, challenge ->
|
||||||
|
val intent = Intent()
|
||||||
|
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle)
|
||||||
|
val challengeToken =
|
||||||
|
gatekeeperPasswordProvider.requestGatekeeperHat(intent, challenge, userId)
|
||||||
|
|
||||||
|
gatekeeperPasswordProvider.removeGatekeeperPasswordHandle(intent, false)
|
||||||
|
val p = Pair(challenge, challengeToken)
|
||||||
|
it.resume(p)
|
||||||
|
}
|
||||||
|
fingerprintManager.generateChallenge(applicationContext.userId, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val enrolledFingerprints: Flow<List<FingerprintViewModel>> = flow {
|
||||||
|
emit(
|
||||||
|
fingerprintManager
|
||||||
|
.getEnrolledFingerprints(applicationContext.userId)
|
||||||
|
.map { (FingerprintViewModel(it.name.toString(), it.biometricId, it.deviceId)) }
|
||||||
|
.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean> = flow {
|
||||||
|
emit(numFingerprints < maxFingerprints)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val maxEnrollableFingerprints = flow { emit(maxFingerprints) }
|
||||||
|
|
||||||
|
override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean = suspendCoroutine {
|
||||||
|
val callback =
|
||||||
|
object : RemovalCallback() {
|
||||||
|
override fun onRemovalError(
|
||||||
|
fp: android.hardware.fingerprint.Fingerprint,
|
||||||
|
errMsgId: Int,
|
||||||
|
errString: CharSequence
|
||||||
|
) {
|
||||||
|
it.resume(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemovalSucceeded(
|
||||||
|
fp: android.hardware.fingerprint.Fingerprint?,
|
||||||
|
remaining: Int
|
||||||
|
) {
|
||||||
|
it.resume(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fingerprintManager.remove(
|
||||||
|
android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId),
|
||||||
|
applicationContext.userId,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
|
||||||
|
withContext(backgroundDispatcher) {
|
||||||
|
fingerprintManager.rename(fp.fingerId, applicationContext.userId, newName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun hasSideFps(): Boolean = suspendCancellableCoroutine {
|
||||||
|
it.resume(fingerprintManager.isPowerbuttonFps)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun pressToAuthEnabled(): Boolean = suspendCancellableCoroutine {
|
||||||
|
it.resume(pressToAuthProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal> =
|
||||||
|
suspendCancellableCoroutine {
|
||||||
|
it.resume(fingerprintManager.sensorPropertiesInternal)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun authenticate(): FingerprintAuthAttemptViewModel =
|
||||||
|
suspendCancellableCoroutine { c: CancellableContinuation<FingerprintAuthAttemptViewModel> ->
|
||||||
|
val authenticationCallback =
|
||||||
|
object : FingerprintManager.AuthenticationCallback() {
|
||||||
|
|
||||||
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
|
super.onAuthenticationError(errorCode, errString)
|
||||||
|
if (c.isCompleted) {
|
||||||
|
Log.d(TAG, "framework sent down onAuthError after finish")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.resume(FingerprintAuthAttemptViewModel.Error(errorCode, errString.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
|
||||||
|
super.onAuthenticationSucceeded(result)
|
||||||
|
if (c.isCompleted) {
|
||||||
|
Log.d(TAG, "framework sent down onAuthError after finish")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.resume(FingerprintAuthAttemptViewModel.Success(result.fingerprint?.biometricId ?: -1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val cancellationSignal = CancellationSignal()
|
||||||
|
c.invokeOnCancellation { cancellationSignal.cancel() }
|
||||||
|
fingerprintManager.authenticate(
|
||||||
|
null,
|
||||||
|
cancellationSignal,
|
||||||
|
authenticationCallback,
|
||||||
|
null,
|
||||||
|
applicationContext.userId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* 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.binder
|
||||||
|
|
||||||
|
import android.hardware.fingerprint.FingerprintManager
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintSettingsViewBinder.FingerprintView
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollAdditionalFingerprint
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintStateViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchedActivity
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.PreferenceViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private const val TAG = "FingerprintSettingsViewBinder"
|
||||||
|
|
||||||
|
/** Binds a [FingerprintSettingsViewModel] to a [FingerprintView] */
|
||||||
|
object FingerprintSettingsViewBinder {
|
||||||
|
|
||||||
|
interface FingerprintView {
|
||||||
|
/**
|
||||||
|
* Helper function to launch fingerprint enrollment(This should be the default behavior when a
|
||||||
|
* user enters their PIN/PATTERN/PASS and no fingerprints are enrolled).
|
||||||
|
*/
|
||||||
|
fun launchFullFingerprintEnrollment(
|
||||||
|
userId: Int,
|
||||||
|
gateKeeperPasswordHandle: Long?,
|
||||||
|
challenge: Long?,
|
||||||
|
challengeToken: ByteArray?
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Helper to launch an add fingerprint request */
|
||||||
|
fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?)
|
||||||
|
/**
|
||||||
|
* Helper function that will try and launch confirm lock, if that fails we will prompt user to
|
||||||
|
* choose a PIN/PATTERN/PASS.
|
||||||
|
*/
|
||||||
|
fun launchConfirmOrChooseLock(userId: Int)
|
||||||
|
|
||||||
|
/** Used to indicate that FingerprintSettings is finished. */
|
||||||
|
fun finish()
|
||||||
|
|
||||||
|
/** Indicates what result should be set for the returning callee */
|
||||||
|
fun setResultExternal(resultCode: Int)
|
||||||
|
/** Indicates the settings UI should be shown */
|
||||||
|
fun showSettings(state: FingerprintStateViewModel)
|
||||||
|
/** Indicates that a user has been locked out */
|
||||||
|
fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.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
|
||||||
|
/** Indicates a user should be asked to renae ma dialog */
|
||||||
|
suspend fun askUserToRenameDialog(
|
||||||
|
fingerprintViewModel: FingerprintViewModel
|
||||||
|
): Pair<FingerprintViewModel, String>?
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(
|
||||||
|
view: FingerprintView,
|
||||||
|
viewModel: FingerprintSettingsViewModel,
|
||||||
|
navigationViewModel: FingerprintSettingsNavigationViewModel,
|
||||||
|
lifecycleScope: LifecycleCoroutineScope,
|
||||||
|
) {
|
||||||
|
|
||||||
|
/** Result listener for launching enrollments **after** a user has reached the settings page. */
|
||||||
|
|
||||||
|
// Settings display flow
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.fingerprintState.filterNotNull().collect { view.showSettings(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialog flow
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.isShowingDialog.collectLatest {
|
||||||
|
if (it == null) {
|
||||||
|
return@collectLatest
|
||||||
|
}
|
||||||
|
when (it) {
|
||||||
|
is PreferenceViewModel.RenameDialog -> {
|
||||||
|
val willRename = view.askUserToRenameDialog(it.fingerprintViewModel)
|
||||||
|
if (willRename != null) {
|
||||||
|
Log.d(TAG, "renaming fingerprint $it")
|
||||||
|
viewModel.renameFingerprint(willRename.first, willRename.second)
|
||||||
|
}
|
||||||
|
viewModel.onRenameDialogFinished()
|
||||||
|
}
|
||||||
|
is PreferenceViewModel.DeleteDialog -> {
|
||||||
|
if (view.askUserToDeleteDialog(it.fingerprintViewModel)) {
|
||||||
|
Log.d(TAG, "deleting fingerprint $it")
|
||||||
|
viewModel.deleteFingerprint(it.fingerprintViewModel)
|
||||||
|
}
|
||||||
|
viewModel.onDeleteDialogFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth flow
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.authFlow.filterNotNull().collect {
|
||||||
|
when (it) {
|
||||||
|
is FingerprintAuthAttemptViewModel.Success -> {
|
||||||
|
view.highlightPref(it.fingerId)
|
||||||
|
}
|
||||||
|
is FingerprintAuthAttemptViewModel.Error -> {
|
||||||
|
if (it.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
|
||||||
|
view.userLockout(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch this on Dispatchers.Default and not main.
|
||||||
|
// Otherwise it takes too long for state transitions such as PIN/PATTERN/PASS
|
||||||
|
// to enrollment, which makes gives the user a janky experience.
|
||||||
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
|
var settingsShowingJob: Job? = null
|
||||||
|
navigationViewModel.nextStep.filterNotNull().collect { nextStep ->
|
||||||
|
settingsShowingJob?.cancel()
|
||||||
|
settingsShowingJob = null
|
||||||
|
Log.d(TAG, "next step = $nextStep")
|
||||||
|
when (nextStep) {
|
||||||
|
is EnrollFirstFingerprint ->
|
||||||
|
view.launchFullFingerprintEnrollment(
|
||||||
|
nextStep.userId,
|
||||||
|
nextStep.gateKeeperPasswordHandle,
|
||||||
|
nextStep.challenge,
|
||||||
|
nextStep.challengeToken
|
||||||
|
)
|
||||||
|
is EnrollAdditionalFingerprint ->
|
||||||
|
view.launchAddFingerprint(nextStep.userId, nextStep.challengeToken)
|
||||||
|
is LaunchConfirmDeviceCredential -> view.launchConfirmOrChooseLock(nextStep.userId)
|
||||||
|
is FinishSettings -> {
|
||||||
|
Log.d(TAG, "Finishing due to ${nextStep.reason}")
|
||||||
|
view.finish()
|
||||||
|
}
|
||||||
|
is FinishSettingsWithResult -> {
|
||||||
|
Log.d(TAG, "Finishing with result ${nextStep.result} due to ${nextStep.reason}")
|
||||||
|
view.setResultExternal(nextStep.result)
|
||||||
|
view.finish()
|
||||||
|
}
|
||||||
|
is ShowSettings -> Log.d(TAG, "Showing settings")
|
||||||
|
is LaunchedActivity -> Log.d(TAG, "Launched activity, awaiting result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,128 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2023 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.android.settings.biometrics.fingerprint2.ui.binder
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.fragment.FingerprintSettingsV2Fragment
|
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollAdditionalFingerprint
|
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
|
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
|
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
|
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
|
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
|
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a [FingerprintSettingsViewModel] to a [FingerprintSettingsV2Fragment]
|
|
||||||
*/
|
|
||||||
object FingerprintViewBinder {
|
|
||||||
|
|
||||||
interface Binding {
|
|
||||||
fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?)
|
|
||||||
fun onEnrollSuccess()
|
|
||||||
fun onEnrollAdditionalFailure()
|
|
||||||
fun onEnrollFirstFailure(reason: String)
|
|
||||||
fun onEnrollFirstFailure(reason: String, resultCode: Int)
|
|
||||||
fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Initial listener for the first enrollment request */
|
|
||||||
fun bind(
|
|
||||||
viewModel: FingerprintSettingsViewModel,
|
|
||||||
lifecycleScope: LifecycleCoroutineScope,
|
|
||||||
token: ByteArray?,
|
|
||||||
challenge: Long?,
|
|
||||||
launchFullFingerprintEnrollment: (
|
|
||||||
userId: Int,
|
|
||||||
gateKeeperPasswordHandle: Long?,
|
|
||||||
challenge: Long?,
|
|
||||||
challengeToken: ByteArray?
|
|
||||||
) -> Unit,
|
|
||||||
launchAddFingerprint: (userId: Int, challengeToken: ByteArray?) -> Unit,
|
|
||||||
launchConfirmOrChooseLock: (userId: Int) -> Unit,
|
|
||||||
finish: () -> Unit,
|
|
||||||
setResultExternal: (resultCode: Int) -> Unit,
|
|
||||||
): Binding {
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
viewModel.nextStep.filterNotNull().collect { nextStep ->
|
|
||||||
when (nextStep) {
|
|
||||||
is EnrollFirstFingerprint -> launchFullFingerprintEnrollment(
|
|
||||||
nextStep.userId,
|
|
||||||
nextStep.gateKeeperPasswordHandle,
|
|
||||||
nextStep.challenge,
|
|
||||||
nextStep.challengeToken
|
|
||||||
)
|
|
||||||
|
|
||||||
is EnrollAdditionalFingerprint -> launchAddFingerprint(
|
|
||||||
nextStep.userId, nextStep.challengeToken
|
|
||||||
)
|
|
||||||
|
|
||||||
is LaunchConfirmDeviceCredential -> launchConfirmOrChooseLock(nextStep.userId)
|
|
||||||
|
|
||||||
is FinishSettings -> {
|
|
||||||
println("Finishing due to ${nextStep.reason}")
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
is FinishSettingsWithResult -> {
|
|
||||||
println("Finishing with result ${nextStep.result} due to ${nextStep.reason}")
|
|
||||||
setResultExternal(nextStep.result)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
is ShowSettings -> println("show settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.onUiCommandExecuted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.updateTokenAndChallenge(token, if (challenge == -1L) null else challenge)
|
|
||||||
|
|
||||||
return object : Binding {
|
|
||||||
override fun onConfirmDevice(
|
|
||||||
wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?
|
|
||||||
) {
|
|
||||||
viewModel.onConfirmDevice(wasSuccessful, theGateKeeperPasswordHandle)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEnrollSuccess() {
|
|
||||||
viewModel.onEnrollSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEnrollAdditionalFailure() {
|
|
||||||
viewModel.onEnrollAdditionalFailure()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEnrollFirstFailure(reason: String) {
|
|
||||||
viewModel.onEnrollFirstFailure(reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEnrollFirstFailure(reason: String, resultCode: Int) {
|
|
||||||
viewModel.onEnrollFirstFailure(reason, resultCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?) {
|
|
||||||
viewModel.onEnrollFirst(token, keyChallenge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* 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.fragment
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.app.admin.DevicePolicyManager
|
||||||
|
import android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_FINGERPRINT_LAST_DELETE_MESSAGE
|
||||||
|
import android.app.admin.DevicePolicyResources.UNDEFINED
|
||||||
|
import android.app.settings.SettingsEnums
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.UserManager
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.android.settings.R
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
|
||||||
|
import com.android.settings.core.instrumentation.InstrumentedDialogFragment
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
|
private const val KEY_IS_LAST_FINGERPRINT = "IS_LAST_FINGERPRINT"
|
||||||
|
|
||||||
|
class FingerprintDeletionDialog : InstrumentedDialogFragment() {
|
||||||
|
private lateinit var fingerprintViewModel: FingerprintViewModel
|
||||||
|
private var isLastFingerprint: Boolean = false
|
||||||
|
private lateinit var alertDialog: AlertDialog
|
||||||
|
lateinit var onClickListener: DialogInterface.OnClickListener
|
||||||
|
lateinit var onNegativeClickListener: DialogInterface.OnClickListener
|
||||||
|
lateinit var onCancelListener: DialogInterface.OnCancelListener
|
||||||
|
|
||||||
|
override fun getMetricsCategory(): Int {
|
||||||
|
return SettingsEnums.DIALOG_FINGERPINT_EDIT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel(dialog: DialogInterface) {
|
||||||
|
onCancelListener.onCancel(dialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
val context = requireContext()
|
||||||
|
|
||||||
|
if (isLastFingerprint) {
|
||||||
|
val isProfileChallengeUser = UserManager.get(context).isManagedProfile(context.userId)
|
||||||
|
val messageId =
|
||||||
|
if (isProfileChallengeUser) {
|
||||||
|
WORK_PROFILE_FINGERPRINT_LAST_DELETE_MESSAGE
|
||||||
|
} else {
|
||||||
|
UNDEFINED
|
||||||
|
}
|
||||||
|
val defaultMessageId =
|
||||||
|
if (isProfileChallengeUser) {
|
||||||
|
R.string.fingerprint_last_delete_message_profile_challenge
|
||||||
|
} else {
|
||||||
|
R.string.fingerprint_last_delete_message
|
||||||
|
}
|
||||||
|
val devicePolicyManager = requireContext().getSystemService(DevicePolicyManager::class.java)
|
||||||
|
message =
|
||||||
|
devicePolicyManager?.resources?.getString(messageId) {
|
||||||
|
message + "\n\n" + context.getString(defaultMessageId)
|
||||||
|
}
|
||||||
|
?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
alertDialog =
|
||||||
|
AlertDialog.Builder(requireActivity())
|
||||||
|
.setTitle(title)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(
|
||||||
|
R.string.security_settings_fingerprint_enroll_dialog_delete,
|
||||||
|
onClickListener
|
||||||
|
)
|
||||||
|
.setNegativeButton(R.string.cancel, onNegativeClickListener)
|
||||||
|
.create()
|
||||||
|
return alertDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_FINGERPRINT = "fingerprint"
|
||||||
|
suspend fun showInstance(
|
||||||
|
fp: FingerprintViewModel,
|
||||||
|
lastFingerprint: Boolean,
|
||||||
|
target: FingerprintSettingsV2Fragment,
|
||||||
|
) = suspendCancellableCoroutine { continuation ->
|
||||||
|
val dialog = FingerprintDeletionDialog()
|
||||||
|
dialog.onClickListener = DialogInterface.OnClickListener { _, _ -> continuation.resume(true) }
|
||||||
|
dialog.onNegativeClickListener =
|
||||||
|
DialogInterface.OnClickListener { _, _ -> continuation.resume(false) }
|
||||||
|
dialog.onCancelListener = DialogInterface.OnCancelListener { continuation.resume(false) }
|
||||||
|
|
||||||
|
continuation.invokeOnCancellation { dialog.dismiss() }
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putObject(
|
||||||
|
KEY_FINGERPRINT,
|
||||||
|
android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId)
|
||||||
|
)
|
||||||
|
bundle.putBoolean(KEY_IS_LAST_FINGERPRINT, lastFingerprint)
|
||||||
|
dialog.arguments = bundle
|
||||||
|
dialog.show(target.parentFragmentManager, FingerprintDeletionDialog::class.java.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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.fragment
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.preference.PreferenceViewHolder
|
||||||
|
import com.android.settings.R
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
|
||||||
|
import com.android.settingslib.widget.TwoTargetPreference
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private const val TAG = "FingerprintSettingsPreference"
|
||||||
|
|
||||||
|
class FingerprintSettingsPreference(
|
||||||
|
context: Context,
|
||||||
|
val fingerprintViewModel: FingerprintViewModel,
|
||||||
|
val fragment: FingerprintSettingsV2Fragment,
|
||||||
|
val isLastFingerprint: Boolean
|
||||||
|
) : TwoTargetPreference(context) {
|
||||||
|
private lateinit var myView: View
|
||||||
|
|
||||||
|
init {
|
||||||
|
key = "FINGERPRINT_" + fingerprintViewModel.fingerId
|
||||||
|
Log.d(TAG, "FingerprintPreference $this with frag $fragment $key")
|
||||||
|
title = fingerprintViewModel.name
|
||||||
|
isPersistent = false
|
||||||
|
setIcon(R.drawable.ic_fingerprint_24dp)
|
||||||
|
setOnPreferenceClickListener {
|
||||||
|
fragment.lifecycleScope.launch { fragment.onPrefClicked(fingerprintViewModel) }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(view: PreferenceViewHolder) {
|
||||||
|
super.onBindViewHolder(view)
|
||||||
|
myView = view.itemView
|
||||||
|
view.itemView.findViewById<View>(R.id.delete_button)?.setOnClickListener {
|
||||||
|
fragment.lifecycleScope.launch { fragment.onDeletePrefClicked(fingerprintViewModel) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Highlights this dialog. */
|
||||||
|
suspend fun highlight() {
|
||||||
|
fragment.activity?.getDrawable(R.drawable.preference_highlight)?.let { highlight ->
|
||||||
|
val centerX: Float = myView.width / 2.0f
|
||||||
|
val centerY: Float = myView.height / 2.0f
|
||||||
|
highlight.setHotspot(centerX, centerY)
|
||||||
|
myView.background = highlight
|
||||||
|
myView.isPressed = true
|
||||||
|
myView.isPressed = false
|
||||||
|
delay(300)
|
||||||
|
myView.background = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSecondTargetResId(): Int {
|
||||||
|
return R.layout.preference_widget_delete
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun askUserToDeleteDialog(): Boolean {
|
||||||
|
return FingerprintDeletionDialog.showInstance(fingerprintViewModel, isLastFingerprint, fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun askUserToRenameDialog(): Pair<FingerprintViewModel, String>? {
|
||||||
|
return FingerprintSettingsRenameDialog.showInstance(fingerprintViewModel, fragment)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* 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.fragment
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.app.settings.SettingsEnums
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.InputFilter
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.ImeAwareEditText
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.android.settings.R
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
|
||||||
|
import com.android.settings.core.instrumentation.InstrumentedDialogFragment
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
|
private const val TAG = "FingerprintSettingsRenameDialog"
|
||||||
|
|
||||||
|
class FingerprintSettingsRenameDialog : InstrumentedDialogFragment() {
|
||||||
|
lateinit var onClickListener: 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 {
|
||||||
|
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 context = requireContext()
|
||||||
|
val alertDialog =
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setView(R.layout.fingerprint_rename_dialog)
|
||||||
|
.setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok, onClickListener)
|
||||||
|
.create()
|
||||||
|
alertDialog.setOnShowListener {
|
||||||
|
(dialog?.findViewById(R.id.fingerprint_rename_field) as ImeAwareEditText?)?.apply {
|
||||||
|
val name = fingerprintViewModel.name
|
||||||
|
setText(name)
|
||||||
|
filters = this@FingerprintSettingsRenameDialog.getFilters()
|
||||||
|
selectAll()
|
||||||
|
requestFocus()
|
||||||
|
scheduleShowSoftInput()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return alertDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFilters(): Array<InputFilter> {
|
||||||
|
val filter: InputFilter =
|
||||||
|
object : InputFilter {
|
||||||
|
|
||||||
|
override fun filter(
|
||||||
|
source: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
dest: Spanned?,
|
||||||
|
dstart: Int,
|
||||||
|
dend: Int
|
||||||
|
): CharSequence? {
|
||||||
|
for (index in start until end) {
|
||||||
|
val c = source[index]
|
||||||
|
// KXMLSerializer does not allow these characters,
|
||||||
|
// see KXmlSerializer.java:162.
|
||||||
|
if (c.code < 0x20) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arrayOf(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMetricsCategory(): Int {
|
||||||
|
return SettingsEnums.DIALOG_FINGERPINT_EDIT
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_FINGERPRINT = "fingerprint"
|
||||||
|
|
||||||
|
suspend fun showInstance(fp: FingerprintViewModel, target: FingerprintSettingsV2Fragment) =
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
val dialog = FingerprintSettingsRenameDialog()
|
||||||
|
val onClick =
|
||||||
|
DialogInterface.OnClickListener { _, _ ->
|
||||||
|
val dialogTextField =
|
||||||
|
dialog.requireDialog().findViewById(R.id.fingerprint_rename_field) as ImeAwareEditText
|
||||||
|
val newName = dialogTextField.text.toString()
|
||||||
|
if (!TextUtils.equals(newName, fp.name)) {
|
||||||
|
Log.d(TAG, "rename $fp.name to $newName for $dialog")
|
||||||
|
continuation.resume(Pair(fp, newName))
|
||||||
|
} else {
|
||||||
|
continuation.resume(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.onClickListener = onClick
|
||||||
|
dialog.onCancelListener =
|
||||||
|
DialogInterface.OnCancelListener {
|
||||||
|
Log.d(TAG, "onCancelListener clicked $dialog")
|
||||||
|
continuation.resume(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
Log.d(TAG, "invokeOnCancellation $dialog")
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putObject(
|
||||||
|
KEY_FINGERPRINT,
|
||||||
|
android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId)
|
||||||
|
)
|
||||||
|
dialog.arguments = bundle
|
||||||
|
Log.d(TAG, "showing dialog $dialog")
|
||||||
|
dialog.show(
|
||||||
|
target.parentFragmentManager,
|
||||||
|
FingerprintSettingsRenameDialog::class.java.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -17,30 +17,65 @@
|
|||||||
package com.android.settings.biometrics.fingerprint2.ui.fragment
|
package com.android.settings.biometrics.fingerprint2.ui.fragment
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.admin.DevicePolicyManager
|
||||||
|
import android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRINT_UNLOCK_DISABLED_EXPLANATION
|
||||||
import android.app.settings.SettingsEnums
|
import android.app.settings.SettingsEnums
|
||||||
import android.content.Context.FINGERPRINT_SERVICE
|
import android.content.Context.FINGERPRINT_SERVICE
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.hardware.fingerprint.FingerprintManager
|
import android.hardware.fingerprint.FingerprintManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings.Secure
|
||||||
|
import android.text.TextUtils
|
||||||
import android.util.FeatureFlagUtils
|
import android.util.FeatureFlagUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceCategory
|
||||||
|
import com.android.internal.widget.LockPatternUtils
|
||||||
import com.android.settings.R
|
import com.android.settings.R
|
||||||
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
|
||||||
|
import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST
|
||||||
|
import com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY
|
||||||
|
import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED
|
||||||
|
import com.android.settings.biometrics.GatekeeperPasswordProvider
|
||||||
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling
|
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling
|
||||||
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal
|
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintViewBinder
|
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintSettingsViewBinder
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintStateViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
|
||||||
import com.android.settings.core.SettingsBaseActivity
|
import com.android.settings.core.SettingsBaseActivity
|
||||||
|
import com.android.settings.core.instrumentation.InstrumentedDialogFragment
|
||||||
import com.android.settings.dashboard.DashboardFragment
|
import com.android.settings.dashboard.DashboardFragment
|
||||||
import com.android.settings.password.ChooseLockGeneric
|
import com.android.settings.password.ChooseLockGeneric
|
||||||
import com.android.settings.password.ChooseLockSettingsHelper
|
import com.android.settings.password.ChooseLockSettingsHelper
|
||||||
|
import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE
|
||||||
|
import com.android.settingslib.HelpUtils
|
||||||
|
import com.android.settingslib.RestrictedLockUtils
|
||||||
|
import com.android.settingslib.RestrictedLockUtilsInternal
|
||||||
import com.android.settingslib.transition.SettingsTransitionHelper
|
import com.android.settingslib.transition.SettingsTransitionHelper
|
||||||
|
import com.android.settingslib.widget.FooterPreference
|
||||||
|
import com.google.android.setupdesign.util.DeviceHelper
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
const val TAG = "FingerprintSettingsV2Fragment"
|
private const val TAG = "FingerprintSettingsV2Fragment"
|
||||||
|
private const val KEY_FINGERPRINTS_ENROLLED_CATEGORY = "security_settings_fingerprints_enrolled"
|
||||||
|
private const val KEY_FINGERPRINT_SIDE_FPS_CATEGORY =
|
||||||
|
"security_settings_fingerprint_unlock_category"
|
||||||
|
private const val KEY_FINGERPRINT_ADD = "key_fingerprint_add"
|
||||||
|
private const val KEY_FINGERPRINT_SIDE_FPS_SCREEN_ON_TO_AUTH =
|
||||||
|
"security_settings_require_screen_on_to_auth"
|
||||||
|
private const val KEY_FINGERPRINT_FOOTER = "security_settings_fingerprint_footer"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class responsible for showing FingerprintSettings. Typical activity Flows are
|
* A class responsible for showing FingerprintSettings. Typical activity Flows are
|
||||||
@@ -53,200 +88,494 @@ const val TAG = "FingerprintSettingsV2Fragment"
|
|||||||
* 3. Renaming a fingerprint
|
* 3. Renaming a fingerprint
|
||||||
* 4. Enabling/Disabling a feature
|
* 4. Enabling/Disabling a feature
|
||||||
*/
|
*/
|
||||||
class FingerprintSettingsV2Fragment : DashboardFragment() {
|
class FingerprintSettingsV2Fragment :
|
||||||
private lateinit var binding: FingerprintViewBinder.Binding
|
DashboardFragment(), FingerprintSettingsViewBinder.FingerprintView {
|
||||||
|
private lateinit var settingsViewModel: FingerprintSettingsViewModel
|
||||||
|
private lateinit var navigationViewModel: FingerprintSettingsNavigationViewModel
|
||||||
|
|
||||||
private val launchFirstEnrollmentListener =
|
/** Result listener for ChooseLock activity flow. */
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val confirmDeviceResultListener =
|
||||||
|
registerForActivityResult(StartActivityForResult()) { result ->
|
||||||
val resultCode = result.resultCode
|
val resultCode = result.resultCode
|
||||||
val data = result.data
|
val data = result.data
|
||||||
|
onConfirmDevice(resultCode, data)
|
||||||
Log.d(
|
|
||||||
TAG, "onEnrollFirstFingerprint($resultCode, $data)"
|
|
||||||
)
|
|
||||||
if (resultCode != BiometricEnrollBase.RESULT_FINISHED || data == null) {
|
|
||||||
if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
|
|
||||||
binding.onEnrollFirstFailure(
|
|
||||||
"Received RESULT_TIMEOUT when enrolling", resultCode
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
binding.onEnrollFirstFailure("Incorrect resultCode or data was null")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val token =
|
|
||||||
data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
|
|
||||||
val keyChallenge = data.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
|
|
||||||
binding.onEnrollFirst(token, keyChallenge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Result listener for launching enrollments **after** a user has reached the settings page. */
|
|
||||||
private val launchAdditionalFingerprintListener =
|
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
|
||||||
val resultCode = result.resultCode
|
|
||||||
Log.d(
|
|
||||||
TAG, "onEnrollAdditionalFingerprint($resultCode)"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
|
|
||||||
binding.onEnrollAdditionalFailure()
|
|
||||||
} else {
|
|
||||||
binding.onEnrollSuccess()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Result listener for ChooseLock activity flow. */
|
|
||||||
private val confirmDeviceResultListener =
|
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
|
||||||
val resultCode = result.resultCode
|
|
||||||
val data = result.data
|
|
||||||
onConfirmDevice(resultCode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
// This is needed to support ChooseLockSettingBuilder...show(). All other activity
|
|
||||||
// calls should use the registerForActivity method call.
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
val wasSuccessful =
|
|
||||||
resultCode == BiometricEnrollBase.RESULT_FINISHED || resultCode == Activity.RESULT_OK
|
|
||||||
val gateKeeperPasswordHandle =
|
|
||||||
data?.getExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE) as Long?
|
|
||||||
binding.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Result listener for launching enrollments **after** a user has reached the settings page. */
|
||||||
|
private val launchAdditionalFingerprintListener: ActivityResultLauncher<Intent> =
|
||||||
|
registerForActivityResult(StartActivityForResult()) { result ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val resultCode = result.resultCode
|
||||||
|
Log.d(TAG, "onEnrollAdditionalFingerprint($resultCode)")
|
||||||
|
|
||||||
override fun onCreate(icicle: Bundle?) {
|
if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
|
||||||
super.onCreate(icicle)
|
navigationViewModel.onEnrollAdditionalFailure()
|
||||||
if (!FeatureFlagUtils.isEnabled(
|
|
||||||
context, FeatureFlagUtils.SETTINGS_BIOMETRICS2_FINGERPRINT_SETTINGS
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Log.d(
|
|
||||||
TAG, "Finishing due to feature not being enabled"
|
|
||||||
)
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val viewModel = ViewModelProvider(
|
|
||||||
this, FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
|
|
||||||
requireContext().applicationContext.userId, requireContext().getSystemService(
|
|
||||||
FINGERPRINT_SERVICE
|
|
||||||
) as FingerprintManager
|
|
||||||
)
|
|
||||||
)[FingerprintSettingsViewModel::class.java]
|
|
||||||
|
|
||||||
val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
|
|
||||||
val challenge = intent.getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L)
|
|
||||||
|
|
||||||
binding = FingerprintViewBinder.bind(
|
|
||||||
viewModel,
|
|
||||||
lifecycleScope,
|
|
||||||
token,
|
|
||||||
challenge,
|
|
||||||
::launchFullFingerprintEnrollment,
|
|
||||||
::launchAddFingerprint,
|
|
||||||
::launchConfirmOrChooseLock,
|
|
||||||
::finish,
|
|
||||||
::setResultExternal,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMetricsCategory(): Int {
|
|
||||||
return SettingsEnums.FINGERPRINT
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPreferenceScreenResId(): Int {
|
|
||||||
return R.xml.security_settings_fingerprint_limbo
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLogTag(): String {
|
|
||||||
return TAG
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function that will try and launch confirm lock, if that fails we will prompt user
|
|
||||||
* to choose a PIN/PATTERN/PASS.
|
|
||||||
*/
|
|
||||||
private fun launchConfirmOrChooseLock(userId: Int) {
|
|
||||||
val intent = Intent()
|
|
||||||
val builder = ChooseLockSettingsHelper.Builder(requireActivity(), this)
|
|
||||||
val launched = builder.setRequestCode(BiometricEnrollBase.CONFIRM_REQUEST)
|
|
||||||
.setTitle(getString(R.string.security_settings_fingerprint_preference_title))
|
|
||||||
.setRequestGatekeeperPasswordHandle(true).setUserId(userId).setForegroundOnly(true)
|
|
||||||
.setReturnCredentials(true).show()
|
|
||||||
if (!launched) {
|
|
||||||
intent.setClassName(
|
|
||||||
Utils.SETTINGS_PACKAGE_NAME, ChooseLockGeneric::class.java.name
|
|
||||||
)
|
|
||||||
intent.putExtra(
|
|
||||||
ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true
|
|
||||||
)
|
|
||||||
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true)
|
|
||||||
intent.putExtra(Intent.EXTRA_USER_ID, userId)
|
|
||||||
confirmDeviceResultListener.launch(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper for confirming a PIN/PATTERN/PASS
|
|
||||||
*/
|
|
||||||
private fun onConfirmDevice(resultCode: Int, data: Intent?) {
|
|
||||||
val wasSuccessful =
|
|
||||||
resultCode == BiometricEnrollBase.RESULT_FINISHED || resultCode == Activity.RESULT_OK
|
|
||||||
val gateKeeperPasswordHandle =
|
|
||||||
data?.getExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE) as Long?
|
|
||||||
binding.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to launch fingerprint enrollment(This should be the default behavior
|
|
||||||
* when a user enters their PIN/PATTERN/PASS and no fingerprints are enrolled.
|
|
||||||
*/
|
|
||||||
private fun launchFullFingerprintEnrollment(
|
|
||||||
userId: Int,
|
|
||||||
gateKeeperPasswordHandle: Long?,
|
|
||||||
challenge: Long?,
|
|
||||||
challengeToken: ByteArray?,
|
|
||||||
) {
|
|
||||||
val intent = Intent()
|
|
||||||
intent.setClassName(
|
|
||||||
Utils.SETTINGS_PACKAGE_NAME, FingerprintEnrollIntroductionInternal::class.java.name
|
|
||||||
)
|
|
||||||
intent.putExtra(BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY, true)
|
|
||||||
intent.putExtra(
|
|
||||||
SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
|
|
||||||
SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE
|
|
||||||
)
|
|
||||||
|
|
||||||
intent.putExtra(Intent.EXTRA_USER_ID, userId)
|
|
||||||
|
|
||||||
if (gateKeeperPasswordHandle != null) {
|
|
||||||
intent.putExtra(
|
|
||||||
ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
intent.putExtra(
|
navigationViewModel.onEnrollSuccess()
|
||||||
ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken
|
|
||||||
)
|
|
||||||
intent.putExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge)
|
|
||||||
}
|
}
|
||||||
launchFirstEnrollmentListener.launch(intent)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setResultExternal(resultCode: Int) {
|
/** Initial listener for the first enrollment request */
|
||||||
setResult(resultCode)
|
private val launchFirstEnrollmentListener: ActivityResultLauncher<Intent> =
|
||||||
|
registerForActivityResult(StartActivityForResult()) { result ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val resultCode = result.resultCode
|
||||||
|
val data = result.data
|
||||||
|
|
||||||
|
Log.d(TAG, "onEnrollFirstFingerprint($resultCode, $data)")
|
||||||
|
if (resultCode != RESULT_FINISHED || data == null) {
|
||||||
|
if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
|
||||||
|
navigationViewModel.onEnrollFirstFailure(
|
||||||
|
"Received RESULT_TIMEOUT when enrolling",
|
||||||
|
resultCode
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
navigationViewModel.onEnrollFirstFailure(
|
||||||
|
"Incorrect resultCode or data was null",
|
||||||
|
resultCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val token = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
|
||||||
|
val challenge = data.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
|
||||||
|
navigationViewModel.onEnrollFirst(token, challenge)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper to launch an add fingerprint request */
|
override fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error) {
|
||||||
private fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?) {
|
Toast.makeText(activity, authAttemptViewModel.message, Toast.LENGTH_SHORT).show()
|
||||||
val intent = Intent()
|
}
|
||||||
intent.setClassName(
|
|
||||||
Utils.SETTINGS_PACKAGE_NAME, FingerprintEnrollEnrolling::class.qualifiedName.toString()
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
// This is needed to support ChooseLockSettingBuilder...show(). All other activity
|
||||||
|
// calls should use the registerForActivity method call.
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
onConfirmDevice(resultCode, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(icicle: Bundle?) {
|
||||||
|
super.onCreate(icicle)
|
||||||
|
|
||||||
|
if (icicle != null) {
|
||||||
|
Log.d(TAG, "onCreateWithSavedState")
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "onCreate()")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!FeatureFlagUtils.isEnabled(
|
||||||
|
context,
|
||||||
|
FeatureFlagUtils.SETTINGS_BIOMETRICS2_FINGERPRINT_SETTINGS
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Finishing due to feature not being enabled")
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = requireContext()
|
||||||
|
val userId = context.userId
|
||||||
|
|
||||||
|
preferenceScreen.isVisible = false
|
||||||
|
|
||||||
|
val fingerprintManager = context.getSystemService(FINGERPRINT_SERVICE) as FingerprintManager
|
||||||
|
|
||||||
|
val backgroundDispatcher = Dispatchers.IO
|
||||||
|
val activity = requireActivity()
|
||||||
|
val userHandle = activity.user.identifier
|
||||||
|
|
||||||
|
val interactor =
|
||||||
|
FingerprintManagerInteractorImpl(
|
||||||
|
context.applicationContext,
|
||||||
|
backgroundDispatcher,
|
||||||
|
fingerprintManager,
|
||||||
|
GatekeeperPasswordProvider(LockPatternUtils(context.applicationContext))
|
||||||
|
) {
|
||||||
|
var toReturn: Int =
|
||||||
|
Secure.getIntForUser(
|
||||||
|
context.contentResolver,
|
||||||
|
Secure.SFPS_PERFORMANT_AUTH_ENABLED,
|
||||||
|
-1,
|
||||||
|
userHandle,
|
||||||
|
)
|
||||||
|
if (toReturn == -1) {
|
||||||
|
toReturn =
|
||||||
|
if (
|
||||||
|
context.resources.getBoolean(com.android.internal.R.bool.config_performantAuthDefault)
|
||||||
|
) {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Secure.putIntForUser(
|
||||||
|
context.contentResolver,
|
||||||
|
Secure.SFPS_PERFORMANT_AUTH_ENABLED,
|
||||||
|
toReturn,
|
||||||
|
userHandle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
toReturn == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
|
||||||
|
val challenge = intent.getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L)
|
||||||
|
|
||||||
|
navigationViewModel =
|
||||||
|
ViewModelProvider(
|
||||||
|
this,
|
||||||
|
FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
|
||||||
|
userId,
|
||||||
|
interactor,
|
||||||
|
backgroundDispatcher,
|
||||||
|
token,
|
||||||
|
challenge
|
||||||
)
|
)
|
||||||
intent.putExtra(Intent.EXTRA_USER_ID, userId)
|
)[FingerprintSettingsNavigationViewModel::class.java]
|
||||||
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
|
|
||||||
launchAdditionalFingerprintListener.launch(intent)
|
settingsViewModel =
|
||||||
|
ViewModelProvider(
|
||||||
|
this,
|
||||||
|
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
|
||||||
|
userId,
|
||||||
|
interactor,
|
||||||
|
backgroundDispatcher,
|
||||||
|
navigationViewModel,
|
||||||
|
)
|
||||||
|
)[FingerprintSettingsViewModel::class.java]
|
||||||
|
|
||||||
|
FingerprintSettingsViewBinder.bind(
|
||||||
|
this,
|
||||||
|
settingsViewModel,
|
||||||
|
navigationViewModel,
|
||||||
|
lifecycleScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMetricsCategory(): Int {
|
||||||
|
return SettingsEnums.FINGERPRINT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPreferenceScreenResId(): Int {
|
||||||
|
return R.xml.security_settings_fingerprint_limbo
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogTag(): String {
|
||||||
|
return TAG
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
navigationViewModel.maybeFinishActivity(requireActivity().isChangingConfigurations)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
settingsViewModel.shouldAuthenticate(false)
|
||||||
|
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.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
settingsViewModel.shouldAuthenticate(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Used to indicate that preference has been clicked */
|
||||||
|
fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) {
|
||||||
|
Log.d(TAG, "onPrefClicked(${fingerprintViewModel})")
|
||||||
|
settingsViewModel.onPrefClicked(fingerprintViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Used to indicate that a delete pref has been clicked */
|
||||||
|
fun onDeletePrefClicked(fingerprintViewModel: FingerprintViewModel) {
|
||||||
|
Log.d(TAG, "onDeletePrefClicked(${fingerprintViewModel})")
|
||||||
|
settingsViewModel.onDeleteClicked(fingerprintViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showSettings(state: FingerprintStateViewModel) {
|
||||||
|
val category =
|
||||||
|
this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)
|
||||||
|
as PreferenceCategory?
|
||||||
|
|
||||||
|
category?.removeAll()
|
||||||
|
|
||||||
|
state.fingerprintViewModels.forEach { fingerprint ->
|
||||||
|
category?.addPreference(
|
||||||
|
FingerprintSettingsPreference(
|
||||||
|
requireContext(),
|
||||||
|
fingerprint,
|
||||||
|
this@FingerprintSettingsV2Fragment,
|
||||||
|
state.fingerprintViewModels.size == 1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
category?.isVisible = true
|
||||||
|
|
||||||
|
createFingerprintsFooterPreference(state.canEnroll, state.maxFingerprints)
|
||||||
|
preferenceScreen.isVisible = true
|
||||||
|
|
||||||
|
val sideFpsPref =
|
||||||
|
this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_SIDE_FPS_CATEGORY)
|
||||||
|
as PreferenceCategory?
|
||||||
|
sideFpsPref?.isVisible = false
|
||||||
|
|
||||||
|
if (state.hasSideFps) {
|
||||||
|
sideFpsPref?.isVisible = state.fingerprintViewModels.isNotEmpty()
|
||||||
|
val otherPref =
|
||||||
|
this@FingerprintSettingsV2Fragment.findPreference(
|
||||||
|
KEY_FINGERPRINT_SIDE_FPS_SCREEN_ON_TO_AUTH
|
||||||
|
) as Preference?
|
||||||
|
otherPref?.isVisible = state.fingerprintViewModels.isNotEmpty()
|
||||||
|
}
|
||||||
|
addFooter(state.hasSideFps)
|
||||||
|
}
|
||||||
|
private fun addFooter(hasSideFps: Boolean) {
|
||||||
|
val footer =
|
||||||
|
this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_FOOTER)
|
||||||
|
as PreferenceCategory?
|
||||||
|
val admin =
|
||||||
|
RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
|
||||||
|
activity,
|
||||||
|
DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT,
|
||||||
|
requireActivity().userId
|
||||||
|
)
|
||||||
|
val activity = requireActivity()
|
||||||
|
val helpIntent =
|
||||||
|
HelpUtils.getHelpIntent(activity, getString(helpResource), activity::class.java.name)
|
||||||
|
val learnMoreClickListener =
|
||||||
|
View.OnClickListener { v: View? -> activity.startActivityForResult(helpIntent, 0) }
|
||||||
|
|
||||||
|
class FooterColumn {
|
||||||
|
var title: CharSequence? = null
|
||||||
|
var learnMoreOverrideText: CharSequence? = null
|
||||||
|
var learnMoreOnClickListener: View.OnClickListener? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
var footerColumns = mutableListOf<FooterColumn>()
|
||||||
|
if (admin != null) {
|
||||||
|
val devicePolicyManager = getSystemService(DevicePolicyManager::class.java)
|
||||||
|
val column1 = FooterColumn()
|
||||||
|
column1.title =
|
||||||
|
devicePolicyManager.resources.getString(FINGERPRINT_UNLOCK_DISABLED_EXPLANATION) {
|
||||||
|
getString(R.string.security_fingerprint_disclaimer_lockscreen_disabled_1)
|
||||||
|
}
|
||||||
|
|
||||||
|
column1.learnMoreOnClickListener =
|
||||||
|
View.OnClickListener { _ ->
|
||||||
|
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(activity, admin)
|
||||||
|
}
|
||||||
|
column1.learnMoreOverrideText = getText(R.string.admin_support_more_info)
|
||||||
|
footerColumns.add(column1)
|
||||||
|
val column2 = FooterColumn()
|
||||||
|
column2.title = getText(R.string.security_fingerprint_disclaimer_lockscreen_disabled_2)
|
||||||
|
if (hasSideFps) {
|
||||||
|
column2.learnMoreOverrideText =
|
||||||
|
getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
|
||||||
|
}
|
||||||
|
column2.learnMoreOnClickListener = learnMoreClickListener
|
||||||
|
footerColumns.add(column2)
|
||||||
|
} else {
|
||||||
|
val column = FooterColumn()
|
||||||
|
column.title =
|
||||||
|
getString(
|
||||||
|
R.string.security_settings_fingerprint_enroll_introduction_v3_message,
|
||||||
|
DeviceHelper.getDeviceName(requireActivity())
|
||||||
|
)
|
||||||
|
column.learnMoreOnClickListener = learnMoreClickListener
|
||||||
|
if (hasSideFps) {
|
||||||
|
column.learnMoreOverrideText =
|
||||||
|
getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
|
||||||
|
}
|
||||||
|
footerColumns.add(column)
|
||||||
|
}
|
||||||
|
|
||||||
|
footer?.removeAll()
|
||||||
|
for (i in 0 until footerColumns.size) {
|
||||||
|
val column = footerColumns[i]
|
||||||
|
val footerPrefToAdd: FooterPreference =
|
||||||
|
FooterPreference.Builder(requireContext()).setTitle(column.title).build()
|
||||||
|
if (i > 0) {
|
||||||
|
footerPrefToAdd.setIconVisibility(View.GONE)
|
||||||
|
}
|
||||||
|
if (column.learnMoreOnClickListener != null) {
|
||||||
|
footerPrefToAdd.setLearnMoreAction(column.learnMoreOnClickListener)
|
||||||
|
if (!TextUtils.isEmpty(column.learnMoreOverrideText)) {
|
||||||
|
footerPrefToAdd.setLearnMoreText(column.learnMoreOverrideText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
footer?.addPreference(footerPrefToAdd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintViewModel): Boolean {
|
||||||
|
Log.d(TAG, "showing delete dialog for (${fingerprintViewModel})")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val willDelete =
|
||||||
|
fingerprintPreferences()
|
||||||
|
.first { it?.fingerprintViewModel == fingerprintViewModel }
|
||||||
|
?.askUserToDeleteDialog()
|
||||||
|
?: false
|
||||||
|
if (willDelete) {
|
||||||
|
mMetricsFeatureProvider.action(
|
||||||
|
context,
|
||||||
|
SettingsEnums.ACTION_FINGERPRINT_DELETE,
|
||||||
|
fingerprintViewModel.fingerId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return willDelete
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.d(TAG, "askUserToDeleteDialog exception $exception")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun askUserToRenameDialog(
|
||||||
|
fingerprintViewModel: FingerprintViewModel
|
||||||
|
): Pair<FingerprintViewModel, String>? {
|
||||||
|
Log.d(TAG, "showing rename dialog for (${fingerprintViewModel})")
|
||||||
|
try {
|
||||||
|
val toReturn =
|
||||||
|
fingerprintPreferences()
|
||||||
|
.first { it?.fingerprintViewModel == fingerprintViewModel }
|
||||||
|
?.askUserToRenameDialog()
|
||||||
|
if (toReturn != null) {
|
||||||
|
mMetricsFeatureProvider.action(
|
||||||
|
context,
|
||||||
|
SettingsEnums.ACTION_FINGERPRINT_RENAME,
|
||||||
|
toReturn.first.fingerId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return toReturn
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.d(TAG, "askUserToRenameDialog exception $exception")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun highlightPref(fingerId: Int) {
|
||||||
|
fingerprintPreferences()
|
||||||
|
.first { pref -> pref?.fingerprintViewModel?.fingerId == fingerId }
|
||||||
|
?.highlight()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun launchConfirmOrChooseLock(userId: Int) {
|
||||||
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
|
navigationViewModel.setStepToLaunched()
|
||||||
|
val intent = Intent()
|
||||||
|
val builder =
|
||||||
|
ChooseLockSettingsHelper.Builder(requireActivity(), this@FingerprintSettingsV2Fragment)
|
||||||
|
val launched =
|
||||||
|
builder
|
||||||
|
.setRequestCode(CONFIRM_REQUEST)
|
||||||
|
.setTitle(getString(R.string.security_settings_fingerprint_preference_title))
|
||||||
|
.setRequestGatekeeperPasswordHandle(true)
|
||||||
|
.setUserId(userId)
|
||||||
|
.setForegroundOnly(true)
|
||||||
|
.setReturnCredentials(true)
|
||||||
|
.show()
|
||||||
|
if (!launched) {
|
||||||
|
intent.setClassName(SETTINGS_PACKAGE_NAME, ChooseLockGeneric::class.java.name)
|
||||||
|
intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true)
|
||||||
|
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true)
|
||||||
|
intent.putExtra(Intent.EXTRA_USER_ID, userId)
|
||||||
|
confirmDeviceResultListener.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun launchFullFingerprintEnrollment(
|
||||||
|
userId: Int,
|
||||||
|
gateKeeperPasswordHandle: Long?,
|
||||||
|
challenge: Long?,
|
||||||
|
challengeToken: ByteArray?,
|
||||||
|
) {
|
||||||
|
navigationViewModel.setStepToLaunched()
|
||||||
|
Log.d(TAG, "launchFullFingerprintEnrollment")
|
||||||
|
val intent = Intent()
|
||||||
|
intent.setClassName(
|
||||||
|
SETTINGS_PACKAGE_NAME,
|
||||||
|
FingerprintEnrollIntroductionInternal::class.java.name
|
||||||
|
)
|
||||||
|
intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true)
|
||||||
|
intent.putExtra(
|
||||||
|
SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
|
||||||
|
SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE
|
||||||
|
)
|
||||||
|
|
||||||
|
intent.putExtra(Intent.EXTRA_USER_ID, userId)
|
||||||
|
|
||||||
|
if (gateKeeperPasswordHandle != null) {
|
||||||
|
intent.putExtra(EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle)
|
||||||
|
} else {
|
||||||
|
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
|
||||||
|
intent.putExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge)
|
||||||
|
}
|
||||||
|
launchFirstEnrollmentListener.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setResultExternal(resultCode: Int) {
|
||||||
|
setResult(resultCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?) {
|
||||||
|
navigationViewModel.setStepToLaunched()
|
||||||
|
val intent = Intent()
|
||||||
|
intent.setClassName(
|
||||||
|
SETTINGS_PACKAGE_NAME,
|
||||||
|
FingerprintEnrollEnrolling::class.qualifiedName.toString()
|
||||||
|
)
|
||||||
|
intent.putExtra(Intent.EXTRA_USER_ID, userId)
|
||||||
|
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
|
||||||
|
launchAdditionalFingerprintListener.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
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?
|
||||||
|
lifecycleScope.launch {
|
||||||
|
navigationViewModel.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFingerprintsFooterPreference(canEnroll: Boolean, maxFingerprints: Int) {
|
||||||
|
val pref = this@FingerprintSettingsV2Fragment.findPreference<Preference>(KEY_FINGERPRINT_ADD)
|
||||||
|
val maxSummary = context?.getString(R.string.fingerprint_add_max, maxFingerprints) ?: ""
|
||||||
|
pref?.summary = maxSummary
|
||||||
|
pref?.isEnabled = canEnroll
|
||||||
|
pref?.setOnPreferenceClickListener {
|
||||||
|
navigationViewModel.onAddFingerprintClicked()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
pref?.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fingerprintPreferences(): List<FingerprintSettingsPreference?> {
|
||||||
|
val category =
|
||||||
|
this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)
|
||||||
|
as PreferenceCategory?
|
||||||
|
|
||||||
|
return category?.let { cat ->
|
||||||
|
cat.childrenToList().map { it as FingerprintSettingsPreference? }
|
||||||
|
}
|
||||||
|
?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PreferenceCategory.childrenToList(): List<Preference> {
|
||||||
|
val mutable: MutableList<Preference> = mutableListOf()
|
||||||
|
for (i in 0 until this.preferenceCount) {
|
||||||
|
mutable.add(this.getPreference(i))
|
||||||
|
}
|
||||||
|
return mutable.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* 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.viewmodel
|
||||||
|
|
||||||
|
import android.hardware.fingerprint.FingerprintManager
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.settings.biometrics.BiometricEnrollBase
|
||||||
|
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/** A Viewmodel that represents the navigation of the FingerprintSettings activity. */
|
||||||
|
class FingerprintSettingsNavigationViewModel(
|
||||||
|
private val userId: Int,
|
||||||
|
private val fingerprintManagerInteractor: FingerprintManagerInteractor,
|
||||||
|
private val backgroundDispatcher: CoroutineDispatcher,
|
||||||
|
tokenInit: ByteArray?,
|
||||||
|
challengeInit: Long?,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private var token = tokenInit
|
||||||
|
private var challenge = challengeInit
|
||||||
|
|
||||||
|
private val _nextStep: MutableStateFlow<NextStepViewModel?> = MutableStateFlow(null)
|
||||||
|
/** This flow represents the high level state for the FingerprintSettingsV2Fragment. */
|
||||||
|
val nextStep: StateFlow<NextStepViewModel?> = _nextStep.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (challengeInit == null || tokenInit == null) {
|
||||||
|
_nextStep.update { LaunchConfirmDeviceCredential(userId) }
|
||||||
|
} else {
|
||||||
|
viewModelScope.launch { showSettingsHelper() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Used to indicate that FingerprintSettings is complete. */
|
||||||
|
fun finish() {
|
||||||
|
_nextStep.update { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Used to finish settings in certain cases. */
|
||||||
|
fun maybeFinishActivity(changingConfig: Boolean) {
|
||||||
|
val isConfirmingOrEnrolling =
|
||||||
|
_nextStep.value is LaunchConfirmDeviceCredential ||
|
||||||
|
_nextStep.value is EnrollAdditionalFingerprint ||
|
||||||
|
_nextStep.value is EnrollFirstFingerprint ||
|
||||||
|
_nextStep.value is LaunchedActivity
|
||||||
|
if (!isConfirmingOrEnrolling && !changingConfig)
|
||||||
|
_nextStep.update {
|
||||||
|
FinishSettingsWithResult(BiometricEnrollBase.RESULT_TIMEOUT, "onStop finishing settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Used to indicate that we have launched another activity and we should await its result. */
|
||||||
|
fun setStepToLaunched() {
|
||||||
|
_nextStep.update { LaunchedActivity }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Indicates a successful enroll has occurred */
|
||||||
|
fun onEnrollSuccess() {
|
||||||
|
showSettingsHelper()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add fingerprint clicked */
|
||||||
|
fun onAddFingerprintClicked() {
|
||||||
|
_nextStep.update { EnrollAdditionalFingerprint(userId, token) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Enrolling of an additional fingerprint failed */
|
||||||
|
fun onEnrollAdditionalFailure() {
|
||||||
|
launchFinishSettings("Failed to enroll additional fingerprint")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The first fingerprint enrollment failed */
|
||||||
|
fun onEnrollFirstFailure(reason: String) {
|
||||||
|
launchFinishSettings(reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The first fingerprint enrollment failed with a result code */
|
||||||
|
fun onEnrollFirstFailure(reason: String, resultCode: Int) {
|
||||||
|
launchFinishSettings(reason, resultCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Notifies that a users first enrollment succeeded. */
|
||||||
|
fun onEnrollFirst(theToken: ByteArray?, theChallenge: Long?) {
|
||||||
|
if (theToken == null) {
|
||||||
|
launchFinishSettings("Error, empty token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (theChallenge == null) {
|
||||||
|
launchFinishSettings("Error, empty keyChallenge")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token = theToken!!
|
||||||
|
challenge = theChallenge!!
|
||||||
|
|
||||||
|
showSettingsHelper()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates to the view model that a confirm device credential action has been completed with a
|
||||||
|
* [theGateKeeperPasswordHandle] which will be used for [FingerprintManager] operations such as
|
||||||
|
* [FingerprintManager.enroll].
|
||||||
|
*/
|
||||||
|
suspend fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?) {
|
||||||
|
if (!wasSuccessful) {
|
||||||
|
launchFinishSettings("ConfirmDeviceCredential was unsuccessful")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (theGateKeeperPasswordHandle == null) {
|
||||||
|
launchFinishSettings("ConfirmDeviceCredential gatekeeper password was null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
launchEnrollNextStep(theGateKeeperPasswordHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSettingsHelper() {
|
||||||
|
_nextStep.update { ShowSettings }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun launchEnrollNextStep(gateKeeperPasswordHandle: Long?) {
|
||||||
|
fingerprintManagerInteractor.enrolledFingerprints.collect {
|
||||||
|
if (it.isEmpty()) {
|
||||||
|
_nextStep.update { EnrollFirstFingerprint(userId, gateKeeperPasswordHandle, null, null) }
|
||||||
|
} else {
|
||||||
|
viewModelScope.launch(backgroundDispatcher) {
|
||||||
|
val challengePair =
|
||||||
|
fingerprintManagerInteractor.generateChallenge(gateKeeperPasswordHandle!!)
|
||||||
|
challenge = challengePair.first
|
||||||
|
token = challengePair.second
|
||||||
|
|
||||||
|
showSettingsHelper()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchFinishSettings(reason: String) {
|
||||||
|
_nextStep.update { FinishSettings(reason) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchFinishSettings(reason: String, errorCode: Int) {
|
||||||
|
_nextStep.update { FinishSettingsWithResult(errorCode, reason) }
|
||||||
|
}
|
||||||
|
class FingerprintSettingsNavigationModelFactory(
|
||||||
|
private val userId: Int,
|
||||||
|
private val interactor: FingerprintManagerInteractor,
|
||||||
|
private val backgroundDispatcher: CoroutineDispatcher,
|
||||||
|
private val token: ByteArray?,
|
||||||
|
private val challenge: Long?,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(
|
||||||
|
modelClass: Class<T>,
|
||||||
|
): T {
|
||||||
|
|
||||||
|
return FingerprintSettingsNavigationViewModel(
|
||||||
|
userId,
|
||||||
|
interactor,
|
||||||
|
backgroundDispatcher,
|
||||||
|
token,
|
||||||
|
challenge,
|
||||||
|
)
|
||||||
|
as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -17,171 +17,308 @@
|
|||||||
package com.android.settings.biometrics.fingerprint2.ui.viewmodel
|
package com.android.settings.biometrics.fingerprint2.ui.viewmodel
|
||||||
|
|
||||||
import android.hardware.fingerprint.FingerprintManager
|
import android.hardware.fingerprint.FingerprintManager
|
||||||
|
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
|
||||||
|
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC
|
||||||
|
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.combineTransform
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.last
|
||||||
|
import kotlinx.coroutines.flow.sample
|
||||||
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
private const val TAG = "FingerprintSettingsViewModel"
|
||||||
* Models the UI state for fingerprint settings.
|
private const val DEBUG = false
|
||||||
*/
|
|
||||||
|
/** Models the UI state for fingerprint settings. */
|
||||||
class FingerprintSettingsViewModel(
|
class FingerprintSettingsViewModel(
|
||||||
private val userId: Int,
|
private val userId: Int,
|
||||||
gateKeeperPassword: Long?,
|
private val fingerprintManagerInteractor: FingerprintManagerInteractor,
|
||||||
theChallenge: Long?,
|
private val backgroundDispatcher: CoroutineDispatcher,
|
||||||
theChallengeToken: ByteArray?,
|
private val navigationViewModel: FingerprintSettingsNavigationViewModel,
|
||||||
private val fingerprintManager: FingerprintManager
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _nextStep: MutableStateFlow<NextStepViewModel?> = MutableStateFlow(null)
|
private val _consumerShouldAuthenticate: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
/**
|
|
||||||
* This flow represents the high level state for the FingerprintSettingsV2Fragment. The
|
|
||||||
* consumer of this flow should call [onUiCommandExecuted] which will set the state to null,
|
|
||||||
* confirming that the UI has consumed the last command and is ready to consume another
|
|
||||||
* command.
|
|
||||||
*/
|
|
||||||
val nextStep = _nextStep.asStateFlow()
|
|
||||||
|
|
||||||
|
private val fingerprintSensorPropertiesInternal:
|
||||||
|
MutableStateFlow<List<FingerprintSensorPropertiesInternal>?> =
|
||||||
|
MutableStateFlow(null)
|
||||||
|
|
||||||
private var gateKeeperPasswordHandle: Long? = gateKeeperPassword
|
private val _isShowingDialog: MutableStateFlow<PreferenceViewModel?> = MutableStateFlow(null)
|
||||||
private var challenge: Long? = theChallenge
|
val isShowingDialog =
|
||||||
private var challengeToken: ByteArray? = theChallengeToken
|
_isShowingDialog.combine(navigationViewModel.nextStep) { dialogFlow, nextStep ->
|
||||||
|
if (nextStep is ShowSettings) {
|
||||||
/**
|
return@combine dialogFlow
|
||||||
* Indicates to the view model that a confirm device credential action has been completed
|
} else {
|
||||||
* with a [theGateKeeperPasswordHandle] which will be used for [FingerprintManager]
|
return@combine null
|
||||||
* operations such as [FingerprintManager.enroll].
|
}
|
||||||
*/
|
|
||||||
fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?) {
|
|
||||||
|
|
||||||
if (!wasSuccessful) {
|
|
||||||
launchFinishSettings("ConfirmDeviceCredential was unsuccessful")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (theGateKeeperPasswordHandle == null) {
|
|
||||||
launchFinishSettings("ConfirmDeviceCredential gatekeeper password was null")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
gateKeeperPasswordHandle = theGateKeeperPasswordHandle
|
|
||||||
launchEnrollNextStep()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
init {
|
||||||
* Notifies that enrollment was successful.
|
viewModelScope.launch {
|
||||||
*/
|
fingerprintSensorPropertiesInternal.update {
|
||||||
fun onEnrollSuccess() {
|
fingerprintManagerInteractor.sensorPropertiesInternal()
|
||||||
_nextStep.update {
|
}
|
||||||
ShowSettings(userId)
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
navigationViewModel.nextStep.filterNotNull().collect {
|
||||||
|
_isShowingDialog.update { null }
|
||||||
|
if (it is ShowSettings) {
|
||||||
|
// reset state
|
||||||
|
updateSettingsData()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _fingerprintStateViewModel: MutableStateFlow<FingerprintStateViewModel?> =
|
||||||
|
MutableStateFlow(null)
|
||||||
|
val fingerprintState: Flow<FingerprintStateViewModel?> =
|
||||||
|
_fingerprintStateViewModel.combineTransform(navigationViewModel.nextStep) {
|
||||||
|
settingsShowingViewModel,
|
||||||
|
currStep ->
|
||||||
|
if (currStep != null && currStep is ShowSettings) {
|
||||||
|
emit(settingsShowingViewModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private val _isLockedOut: MutableStateFlow<FingerprintAuthAttemptViewModel.Error?> =
|
||||||
* Notifies that an additional enrollment failed.
|
MutableStateFlow(null)
|
||||||
*/
|
|
||||||
fun onEnrollAdditionalFailure() {
|
|
||||||
launchFinishSettings("Failed to enroll additional fingerprint")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private val _authSucceeded: MutableSharedFlow<FingerprintAuthAttemptViewModel.Success?> =
|
||||||
* Notifies that the first enrollment failed.
|
MutableSharedFlow()
|
||||||
*/
|
|
||||||
fun onEnrollFirstFailure(reason: String) {
|
|
||||||
launchFinishSettings(reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private val attemptsSoFar: MutableStateFlow<Int> = MutableStateFlow(0)
|
||||||
* Notifies that first enrollment failed (with resultCode)
|
|
||||||
*/
|
|
||||||
fun onEnrollFirstFailure(reason: String, resultCode: Int) {
|
|
||||||
launchFinishSettings(reason, resultCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies that a users first enrollment succeeded.
|
* This is a very tricky flow. The current fingerprint manager APIs are not robust, and a proper
|
||||||
*/
|
* implementation would take quite a lot of code to implement, it might be easier to rewrite
|
||||||
fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?) {
|
* FingerprintManager.
|
||||||
if (token == null) {
|
*
|
||||||
launchFinishSettings("Error, empty token")
|
* The hack to note is the sample(400), if we call authentications in too close of proximity
|
||||||
return
|
* without waiting for a response, the fingerprint manager will send us the results of the
|
||||||
|
* previous attempt.
|
||||||
|
*/
|
||||||
|
private val canAuthenticate: Flow<Boolean> =
|
||||||
|
combine(
|
||||||
|
_isShowingDialog,
|
||||||
|
navigationViewModel.nextStep,
|
||||||
|
_consumerShouldAuthenticate,
|
||||||
|
_fingerprintStateViewModel,
|
||||||
|
_isLockedOut,
|
||||||
|
attemptsSoFar,
|
||||||
|
fingerprintSensorPropertiesInternal
|
||||||
|
) { dialogShowing, step, resume, fingerprints, isLockedOut, attempts, sensorProps ->
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"canAuthenticate(isShowingDialog=${dialogShowing != null}," +
|
||||||
|
"nextStep=${step}," +
|
||||||
|
"resumed=${resume}," +
|
||||||
|
"fingerprints=${fingerprints}," +
|
||||||
|
"lockedOut=${isLockedOut}," +
|
||||||
|
"attempts=${attempts}," +
|
||||||
|
"sensorProps=${sensorProps}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (keyChallenge == null) {
|
if (sensorProps.isNullOrEmpty()) {
|
||||||
launchFinishSettings("Error, empty keyChallenge")
|
return@combine false
|
||||||
return
|
|
||||||
}
|
}
|
||||||
challengeToken = token
|
val sensorType = sensorProps[0].sensorType
|
||||||
challenge = keyChallenge
|
if (listOf(TYPE_UDFPS_OPTICAL, TYPE_UDFPS_ULTRASONIC).contains(sensorType)) {
|
||||||
|
return@combine false
|
||||||
_nextStep.update {
|
|
||||||
ShowSettings(userId)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if (step != null && step is ShowSettings) {
|
||||||
|
if (fingerprints?.fingerprintViewModels?.isNotEmpty() == true) {
|
||||||
|
return@combine dialogShowing == null && isLockedOut == null && resume && attempts < 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
.sample(400)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
/**
|
/** Represents a consistent stream of authentication attempts. */
|
||||||
* Indicates if this settings activity has been called with correct token and challenge
|
val authFlow: Flow<FingerprintAuthAttemptViewModel> =
|
||||||
* and that we do not need to launch confirm device credential.
|
canAuthenticate
|
||||||
*/
|
.transformLatest {
|
||||||
fun updateTokenAndChallenge(token: ByteArray?, theChallenge: Long?) {
|
try {
|
||||||
challengeToken = token
|
Log.d(TAG, "canAuthenticate $it")
|
||||||
challenge = theChallenge
|
while (it && navigationViewModel.nextStep.value is ShowSettings) {
|
||||||
if (challengeToken == null) {
|
Log.d(TAG, "canAuthenticate authing")
|
||||||
_nextStep.update {
|
attemptingAuth()
|
||||||
LaunchConfirmDeviceCredential(userId)
|
when (val authAttempt = fingerprintManagerInteractor.authenticate()) {
|
||||||
|
is FingerprintAuthAttemptViewModel.Success -> {
|
||||||
|
onAuthSuccess(authAttempt)
|
||||||
|
emit(authAttempt)
|
||||||
|
}
|
||||||
|
is FingerprintAuthAttemptViewModel.Error -> {
|
||||||
|
if (authAttempt.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
|
||||||
|
lockout(authAttempt)
|
||||||
|
emit(authAttempt)
|
||||||
|
return@transformLatest
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
launchEnrollNextStep()
|
} catch (exception: Exception) {
|
||||||
|
Log.d(TAG, "shouldAuthenticate exception $exception")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.flowOn(backgroundDispatcher)
|
||||||
|
|
||||||
|
/** The rename dialog has finished */
|
||||||
|
fun onRenameDialogFinished() {
|
||||||
|
_isShowingDialog.update { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The delete dialog has finished */
|
||||||
|
fun onDeleteDialogFinished() {
|
||||||
|
_isShowingDialog.update { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "userId: $userId\n" + "fingerprintState: ${_fingerprintStateViewModel.value}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The fingerprint delete button has been clicked. */
|
||||||
|
fun onDeleteClicked(fingerprintViewModel: FingerprintViewModel) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) {
|
||||||
|
_isShowingDialog.tryEmit(PreferenceViewModel.DeleteDialog(fingerprintViewModel))
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Ignoring onDeleteClicked due to dialog showing ${_isShowingDialog.value}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/** The rename fingerprint dialog has been clicked. */
|
||||||
* Indicates a UI command has been consumed by the UI, and the logic can send another
|
fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) {
|
||||||
* UI command.
|
viewModelScope.launch {
|
||||||
*/
|
if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) {
|
||||||
fun onUiCommandExecuted() {
|
_isShowingDialog.tryEmit(PreferenceViewModel.RenameDialog(fingerprintViewModel))
|
||||||
_nextStep.update {
|
} else {
|
||||||
null
|
Log.d(TAG, "Ignoring onPrefClicked due to dialog showing ${_isShowingDialog.value}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchEnrollNextStep() {
|
/** A request to delete a fingerprint */
|
||||||
if (fingerprintManager.getEnrolledFingerprints(userId).isEmpty()) {
|
fun deleteFingerprint(fp: FingerprintViewModel) {
|
||||||
_nextStep.update {
|
viewModelScope.launch(backgroundDispatcher) {
|
||||||
EnrollFirstFingerprint(userId, gateKeeperPasswordHandle, challenge, challengeToken)
|
if (fingerprintManagerInteractor.removeFingerprint(fp)) {
|
||||||
}
|
updateSettingsData()
|
||||||
} else {
|
}
|
||||||
_nextStep.update {
|
|
||||||
ShowSettings(userId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchFinishSettings(reason: String) {
|
/** A request to rename a fingerprint */
|
||||||
_nextStep.update {
|
fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
|
||||||
FinishSettings(reason)
|
viewModelScope.launch {
|
||||||
}
|
fingerprintManagerInteractor.renameFingerprint(fp, newName)
|
||||||
|
updateSettingsData()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchFinishSettings(reason: String, errorCode: Int) {
|
private fun attemptingAuth() {
|
||||||
_nextStep.update {
|
attemptsSoFar.update { it + 1 }
|
||||||
FinishSettingsWithResult(errorCode, reason)
|
}
|
||||||
}
|
|
||||||
|
private suspend fun onAuthSuccess(success: FingerprintAuthAttemptViewModel.Success) {
|
||||||
|
_authSucceeded.emit(success)
|
||||||
|
attemptsSoFar.update { 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun lockout(attemptViewModel: FingerprintAuthAttemptViewModel.Error) {
|
||||||
|
_isLockedOut.update { attemptViewModel }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is sort of a hack, it's used whenever we want to check for fingerprint state
|
||||||
|
* updates.
|
||||||
|
*/
|
||||||
|
private suspend fun updateSettingsData() {
|
||||||
|
Log.d(TAG, "update settings data called")
|
||||||
|
val fingerprints = fingerprintManagerInteractor.enrolledFingerprints.last()
|
||||||
|
val canEnrollFingerprint =
|
||||||
|
fingerprintManagerInteractor.canEnrollFingerprints(fingerprints.size).last()
|
||||||
|
val maxFingerprints = fingerprintManagerInteractor.maxEnrollableFingerprints.last()
|
||||||
|
val hasSideFps = fingerprintManagerInteractor.hasSideFps()
|
||||||
|
val pressToAuthEnabled = fingerprintManagerInteractor.pressToAuthEnabled()
|
||||||
|
_fingerprintStateViewModel.update {
|
||||||
|
FingerprintStateViewModel(
|
||||||
|
fingerprints,
|
||||||
|
canEnrollFingerprint,
|
||||||
|
maxFingerprints,
|
||||||
|
hasSideFps,
|
||||||
|
pressToAuthEnabled
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class FingerprintSettingsViewModelFactory(
|
/** Used to indicate whether the consumer of the view model is ready for authentication. */
|
||||||
private val userId: Int,
|
fun shouldAuthenticate(authenticate: Boolean) {
|
||||||
private val fingerprintManager: FingerprintManager,
|
_consumerShouldAuthenticate.update { authenticate }
|
||||||
) : ViewModelProvider.Factory {
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
class FingerprintSettingsViewModelFactory(
|
||||||
override fun <T : ViewModel> create(
|
private val userId: Int,
|
||||||
modelClass: Class<T>,
|
private val interactor: FingerprintManagerInteractor,
|
||||||
): T {
|
private val backgroundDispatcher: CoroutineDispatcher,
|
||||||
|
private val navigationViewModel: FingerprintSettingsNavigationViewModel,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
return FingerprintSettingsViewModel(
|
@Suppress("UNCHECKED_CAST")
|
||||||
userId, null, null, null, fingerprintManager
|
override fun <T : ViewModel> create(
|
||||||
) as T
|
modelClass: Class<T>,
|
||||||
}
|
): T {
|
||||||
|
|
||||||
|
return FingerprintSettingsViewModel(
|
||||||
|
userId,
|
||||||
|
interactor,
|
||||||
|
backgroundDispatcher,
|
||||||
|
navigationViewModel,
|
||||||
|
)
|
||||||
|
as T
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
|
||||||
|
flow: Flow<T1>,
|
||||||
|
flow2: Flow<T2>,
|
||||||
|
flow3: Flow<T3>,
|
||||||
|
flow4: Flow<T4>,
|
||||||
|
flow5: Flow<T5>,
|
||||||
|
flow6: Flow<T6>,
|
||||||
|
flow7: Flow<T7>,
|
||||||
|
crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
|
||||||
|
): Flow<R> {
|
||||||
|
return combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
transform(
|
||||||
|
args[0] as T1,
|
||||||
|
args[1] as T2,
|
||||||
|
args[2] as T3,
|
||||||
|
args[3] as T4,
|
||||||
|
args[4] as T5,
|
||||||
|
args[5] as T6,
|
||||||
|
args[6] as T7,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.viewmodel
|
||||||
|
|
||||||
|
/** Represents the fingerprint data nad the relevant state. */
|
||||||
|
data class FingerprintStateViewModel(
|
||||||
|
val fingerprintViewModels: List<FingerprintViewModel>,
|
||||||
|
val canEnroll: Boolean,
|
||||||
|
val maxFingerprints: Int,
|
||||||
|
val hasSideFps: Boolean,
|
||||||
|
val pressToAuth: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FingerprintViewModel(
|
||||||
|
val name: String,
|
||||||
|
val fingerId: Int,
|
||||||
|
val deviceId: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed class FingerprintAuthAttemptViewModel {
|
||||||
|
data class Success(
|
||||||
|
val fingerId: Int,
|
||||||
|
) : FingerprintAuthAttemptViewModel()
|
||||||
|
|
||||||
|
data class Error(
|
||||||
|
val error: Int,
|
||||||
|
val message: String,
|
||||||
|
) : FingerprintAuthAttemptViewModel()
|
||||||
|
}
|
@@ -17,32 +17,29 @@
|
|||||||
package com.android.settings.biometrics.fingerprint2.ui.viewmodel
|
package com.android.settings.biometrics.fingerprint2.ui.viewmodel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to represent a next step for FingerprintSettings. This is typically to perform an action
|
* A class to represent a high level step for FingerprintSettings. This is typically to perform an
|
||||||
* such that launches another activity such as EnrollFirstFingerprint() or
|
* action like launching an activity.
|
||||||
* LaunchConfirmDeviceCredential().
|
|
||||||
*/
|
*/
|
||||||
sealed class NextStepViewModel
|
sealed class NextStepViewModel
|
||||||
|
|
||||||
data class EnrollFirstFingerprint(
|
data class EnrollFirstFingerprint(
|
||||||
val userId: Int, val gateKeeperPasswordHandle: Long?,
|
val userId: Int,
|
||||||
val challenge: Long?,
|
val gateKeeperPasswordHandle: Long?,
|
||||||
val challengeToken: ByteArray?,
|
val challenge: Long?,
|
||||||
|
val challengeToken: ByteArray?,
|
||||||
) : NextStepViewModel()
|
) : NextStepViewModel()
|
||||||
|
|
||||||
data class EnrollAdditionalFingerprint(
|
data class EnrollAdditionalFingerprint(
|
||||||
val userId: Int,
|
val userId: Int,
|
||||||
val challengeToken: ByteArray?,
|
val challengeToken: ByteArray?,
|
||||||
) : NextStepViewModel()
|
) : NextStepViewModel()
|
||||||
|
|
||||||
data class FinishSettings(
|
data class FinishSettings(val reason: String) : NextStepViewModel()
|
||||||
val reason: String
|
|
||||||
) : NextStepViewModel()
|
|
||||||
|
|
||||||
data class FinishSettingsWithResult(
|
data class FinishSettingsWithResult(val result: Int, val reason: String) : NextStepViewModel()
|
||||||
val result: Int, val reason: String
|
|
||||||
) : NextStepViewModel()
|
|
||||||
|
|
||||||
data class ShowSettings(val userId: Int) : NextStepViewModel()
|
object ShowSettings : NextStepViewModel()
|
||||||
|
|
||||||
|
object LaunchedActivity : NextStepViewModel()
|
||||||
|
|
||||||
data class LaunchConfirmDeviceCredential(val userId: Int) : NextStepViewModel()
|
data class LaunchConfirmDeviceCredential(val userId: Int) : NextStepViewModel()
|
||||||
|
|
||||||
|
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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.viewmodel
|
||||||
|
|
||||||
|
/** Classed use to represent a Dialogs state. */
|
||||||
|
sealed class PreferenceViewModel {
|
||||||
|
data class RenameDialog(
|
||||||
|
val fingerprintViewModel: FingerprintViewModel,
|
||||||
|
) : PreferenceViewModel()
|
||||||
|
|
||||||
|
data class DeleteDialog(
|
||||||
|
val fingerprintViewModel: FingerprintViewModel,
|
||||||
|
) : PreferenceViewModel()
|
||||||
|
}
|
@@ -21,6 +21,7 @@ android_test {
|
|||||||
],
|
],
|
||||||
|
|
||||||
static_libs: [
|
static_libs: [
|
||||||
|
"androidx.arch.core_core-testing",
|
||||||
"androidx.test.core",
|
"androidx.test.core",
|
||||||
"androidx.test.rules",
|
"androidx.test.rules",
|
||||||
"androidx.test.espresso.core",
|
"androidx.test.espresso.core",
|
||||||
|
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* 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.domain.interactor
|
||||||
|
|
||||||
|
import android.hardware.biometrics.SensorProperties
|
||||||
|
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON
|
||||||
|
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
|
||||||
|
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
|
/** Fake to be used by other classes to easily fake the FingerprintManager implementation. */
|
||||||
|
class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
|
||||||
|
|
||||||
|
var enrollableFingerprints: Int = 5
|
||||||
|
var enrolledFingerprintsInternal: MutableList<FingerprintViewModel> = mutableListOf()
|
||||||
|
var challengeToGenerate: Pair<Long, ByteArray> = Pair(-1L, byteArrayOf())
|
||||||
|
var authenticateAttempt = FingerprintAuthAttemptViewModel.Success(1)
|
||||||
|
var pressToAuthEnabled = true
|
||||||
|
|
||||||
|
var sensorProps =
|
||||||
|
listOf(
|
||||||
|
FingerprintSensorPropertiesInternal(
|
||||||
|
0 /* sensorId */,
|
||||||
|
SensorProperties.STRENGTH_STRONG,
|
||||||
|
5 /* maxEnrollmentsPerUser */,
|
||||||
|
emptyList() /* ComponentInfoInternal */,
|
||||||
|
TYPE_POWER_BUTTON,
|
||||||
|
true /* resetLockoutRequiresHardwareAuthToken */
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun authenticate(): FingerprintAuthAttemptViewModel {
|
||||||
|
return authenticateAttempt
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> {
|
||||||
|
return challengeToGenerate
|
||||||
|
}
|
||||||
|
override val enrolledFingerprints: Flow<List<FingerprintViewModel>> = flow {
|
||||||
|
emit(enrolledFingerprintsInternal)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean> = flow {
|
||||||
|
emit(numFingerprints < enrollableFingerprints)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val maxEnrollableFingerprints: Flow<Int> = flow { emit(enrollableFingerprints) }
|
||||||
|
|
||||||
|
override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean {
|
||||||
|
return enrolledFingerprintsInternal.remove(fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {}
|
||||||
|
|
||||||
|
override suspend fun hasSideFps(): Boolean {
|
||||||
|
return sensorProps.any { it.isAnySidefpsType }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun pressToAuthEnabled(): Boolean {
|
||||||
|
return pressToAuthEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal> =
|
||||||
|
sensorProps
|
||||||
|
}
|
@@ -0,0 +1,287 @@
|
|||||||
|
/*
|
||||||
|
* 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.domain.interactor
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.hardware.fingerprint.Fingerprint
|
||||||
|
import android.hardware.fingerprint.FingerprintManager
|
||||||
|
import android.hardware.fingerprint.FingerprintManager.CryptoObject
|
||||||
|
import android.hardware.fingerprint.FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT
|
||||||
|
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.domain.interactor.FingerprintManagerInteractor
|
||||||
|
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
|
||||||
|
import com.android.settings.password.ChooseLockSettingsHelper
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.flow.last
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.ArgumentCaptor
|
||||||
|
import org.mockito.ArgumentMatchers.anyInt
|
||||||
|
import org.mockito.ArgumentMatchers.anyLong
|
||||||
|
import org.mockito.ArgumentMatchers.eq
|
||||||
|
import org.mockito.ArgumentMatchers.nullable
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.junit.MockitoJUnit
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner
|
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner::class)
|
||||||
|
class FingerprintManagerInteractorTest {
|
||||||
|
|
||||||
|
@JvmField @Rule var rule = MockitoJUnit.rule()
|
||||||
|
private lateinit var underTest: FingerprintManagerInteractor
|
||||||
|
private var context: Context = ApplicationProvider.getApplicationContext()
|
||||||
|
private var backgroundDispatcher = StandardTestDispatcher()
|
||||||
|
@Mock private lateinit var fingerprintManager: FingerprintManager
|
||||||
|
@Mock private lateinit var gateKeeperPasswordProvider: GatekeeperPasswordProvider
|
||||||
|
|
||||||
|
private var testScope = TestScope(backgroundDispatcher)
|
||||||
|
private var pressToAuthProvider = { true }
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
underTest =
|
||||||
|
FingerprintManagerInteractorImpl(
|
||||||
|
context,
|
||||||
|
backgroundDispatcher,
|
||||||
|
fingerprintManager,
|
||||||
|
gateKeeperPasswordProvider,
|
||||||
|
pressToAuthProvider,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEmptyFingerprints() =
|
||||||
|
testScope.runTest {
|
||||||
|
Mockito.`when`(fingerprintManager.getEnrolledFingerprints(Mockito.anyInt()))
|
||||||
|
.thenReturn(emptyList())
|
||||||
|
|
||||||
|
val emptyFingerprintList: List<Fingerprint> = emptyList()
|
||||||
|
assertThat(underTest.enrolledFingerprints.last()).isEqualTo(emptyFingerprintList)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOneFingerprint() =
|
||||||
|
testScope.runTest {
|
||||||
|
val expected = Fingerprint("Finger 1,", 2, 3L)
|
||||||
|
val fingerprintList: List<Fingerprint> = listOf(expected)
|
||||||
|
Mockito.`when`(fingerprintManager.getEnrolledFingerprints(Mockito.anyInt()))
|
||||||
|
.thenReturn(fingerprintList)
|
||||||
|
|
||||||
|
val list = underTest.enrolledFingerprints.last()
|
||||||
|
assertThat(list.size).isEqualTo(fingerprintList.size)
|
||||||
|
val actual = list[0]
|
||||||
|
assertThat(actual.name).isEqualTo(expected.name)
|
||||||
|
assertThat(actual.fingerId).isEqualTo(expected.biometricId)
|
||||||
|
assertThat(actual.deviceId).isEqualTo(expected.deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCanEnrollFingerprint() =
|
||||||
|
testScope.runTest {
|
||||||
|
val mockContext = Mockito.mock(Context::class.java)
|
||||||
|
val resources = Mockito.mock(Resources::class.java)
|
||||||
|
Mockito.`when`(mockContext.resources).thenReturn(resources)
|
||||||
|
Mockito.`when`(resources.getInteger(anyInt())).thenReturn(3)
|
||||||
|
underTest =
|
||||||
|
FingerprintManagerInteractorImpl(
|
||||||
|
mockContext,
|
||||||
|
backgroundDispatcher,
|
||||||
|
fingerprintManager,
|
||||||
|
gateKeeperPasswordProvider,
|
||||||
|
pressToAuthProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(underTest.canEnrollFingerprints(2).last()).isTrue()
|
||||||
|
assertThat(underTest.canEnrollFingerprints(3).last()).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGenerateChallenge() =
|
||||||
|
testScope.runTest {
|
||||||
|
val byteArray = byteArrayOf(5, 3, 2)
|
||||||
|
val challenge = 100L
|
||||||
|
val intent = Intent()
|
||||||
|
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, challenge)
|
||||||
|
Mockito.`when`(
|
||||||
|
gateKeeperPasswordProvider.requestGatekeeperHat(
|
||||||
|
any(Intent::class.java),
|
||||||
|
anyLong(),
|
||||||
|
anyInt()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.thenReturn(byteArray)
|
||||||
|
|
||||||
|
val generateChallengeCallback: ArgumentCaptor<FingerprintManager.GenerateChallengeCallback> =
|
||||||
|
ArgumentCaptor.forClass(FingerprintManager.GenerateChallengeCallback::class.java)
|
||||||
|
|
||||||
|
var result: Pair<Long, ByteArray?>? = null
|
||||||
|
val job = testScope.launch { result = underTest.generateChallenge(1L) }
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
Mockito.verify(fingerprintManager)
|
||||||
|
.generateChallenge(anyInt(), capture(generateChallengeCallback))
|
||||||
|
generateChallengeCallback.value.onChallengeGenerated(1, 2, challenge)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
job.cancelAndJoin()
|
||||||
|
|
||||||
|
assertThat(result?.first).isEqualTo(challenge)
|
||||||
|
assertThat(result?.second).isEqualTo(byteArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRemoveFingerprint_succeeds() =
|
||||||
|
testScope.runTest {
|
||||||
|
val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L)
|
||||||
|
val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L)
|
||||||
|
|
||||||
|
val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> =
|
||||||
|
ArgumentCaptor.forClass(FingerprintManager.RemovalCallback::class.java)
|
||||||
|
|
||||||
|
var result: Boolean? = null
|
||||||
|
val job =
|
||||||
|
testScope.launch { result = underTest.removeFingerprint(fingerprintViewModelToRemove) }
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
Mockito.verify(fingerprintManager)
|
||||||
|
.remove(any(Fingerprint::class.java), anyInt(), capture(removalCallback))
|
||||||
|
removalCallback.value.onRemovalSucceeded(fingerprintToRemove, 1)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
job.cancelAndJoin()
|
||||||
|
|
||||||
|
assertThat(result).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRemoveFingerprint_fails() =
|
||||||
|
testScope.runTest {
|
||||||
|
val fingerprintViewModelToRemove = FingerprintViewModel("Finger 2", 1, 2L)
|
||||||
|
val fingerprintToRemove = Fingerprint("Finger 2", 1, 2L)
|
||||||
|
|
||||||
|
val removalCallback: ArgumentCaptor<FingerprintManager.RemovalCallback> =
|
||||||
|
ArgumentCaptor.forClass(FingerprintManager.RemovalCallback::class.java)
|
||||||
|
|
||||||
|
var result: Boolean? = null
|
||||||
|
val job =
|
||||||
|
testScope.launch { result = underTest.removeFingerprint(fingerprintViewModelToRemove) }
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
Mockito.verify(fingerprintManager)
|
||||||
|
.remove(any(Fingerprint::class.java), anyInt(), capture(removalCallback))
|
||||||
|
removalCallback.value.onRemovalError(
|
||||||
|
fingerprintToRemove,
|
||||||
|
100,
|
||||||
|
"Oh no, we couldn't find that one"
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
job.cancelAndJoin()
|
||||||
|
|
||||||
|
assertThat(result).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRenameFingerprint_succeeds() =
|
||||||
|
testScope.runTest {
|
||||||
|
val fingerprintToRename = FingerprintViewModel("Finger 2", 1, 2L)
|
||||||
|
|
||||||
|
underTest.renameFingerprint(fingerprintToRename, "Woo")
|
||||||
|
|
||||||
|
Mockito.verify(fingerprintManager)
|
||||||
|
.rename(eq(fingerprintToRename.fingerId), anyInt(), safeEq("Woo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAuth_succeeds() =
|
||||||
|
testScope.runTest {
|
||||||
|
val fingerprint = Fingerprint("Woooo", 100, 101L)
|
||||||
|
|
||||||
|
var result: FingerprintAuthAttemptViewModel? = null
|
||||||
|
val job = launch { result = underTest.authenticate() }
|
||||||
|
|
||||||
|
val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> =
|
||||||
|
ArgumentCaptor.forClass(FingerprintManager.AuthenticationCallback::class.java)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
Mockito.verify(fingerprintManager)
|
||||||
|
.authenticate(
|
||||||
|
nullable(CryptoObject::class.java),
|
||||||
|
any(CancellationSignal::class.java),
|
||||||
|
capture(authCallback),
|
||||||
|
nullable(Handler::class.java),
|
||||||
|
anyInt()
|
||||||
|
)
|
||||||
|
authCallback.value.onAuthenticationSucceeded(
|
||||||
|
FingerprintManager.AuthenticationResult(null, fingerprint, 1, false)
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
job.cancelAndJoin()
|
||||||
|
assertThat(result).isEqualTo(FingerprintAuthAttemptViewModel.Success(fingerprint.biometricId))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAuth_lockout() =
|
||||||
|
testScope.runTest {
|
||||||
|
var result: FingerprintAuthAttemptViewModel? = null
|
||||||
|
val job = launch { result = underTest.authenticate() }
|
||||||
|
|
||||||
|
val authCallback: ArgumentCaptor<FingerprintManager.AuthenticationCallback> =
|
||||||
|
ArgumentCaptor.forClass(FingerprintManager.AuthenticationCallback::class.java)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
Mockito.verify(fingerprintManager)
|
||||||
|
.authenticate(
|
||||||
|
nullable(CryptoObject::class.java),
|
||||||
|
any(CancellationSignal::class.java),
|
||||||
|
capture(authCallback),
|
||||||
|
nullable(Handler::class.java),
|
||||||
|
anyInt()
|
||||||
|
)
|
||||||
|
authCallback.value.onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!")
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
job.cancelAndJoin()
|
||||||
|
assertThat(result)
|
||||||
|
.isEqualTo(
|
||||||
|
FingerprintAuthAttemptViewModel.Error(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "Lockout!!")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
@@ -0,0 +1,275 @@
|
|||||||
|
/*
|
||||||
|
* 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.viewmodel
|
||||||
|
|
||||||
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.NextStepViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
|
||||||
|
import com.android.settings.fingerprint2.domain.interactor.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 FingerprintSettingsNavigationViewModelTest {
|
||||||
|
|
||||||
|
@JvmField @Rule var rule = MockitoJUnit.rule()
|
||||||
|
|
||||||
|
@get:Rule val instantTaskRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
private lateinit var underTest: FingerprintSettingsNavigationViewModel
|
||||||
|
private val defaultUserId = 0
|
||||||
|
private var backgroundDispatcher = StandardTestDispatcher()
|
||||||
|
private var testScope = TestScope(backgroundDispatcher)
|
||||||
|
private lateinit var fakeFingerprintManagerInteractor: FakeFingerprintManagerInteractor
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
fakeFingerprintManagerInteractor = FakeFingerprintManagerInteractor()
|
||||||
|
backgroundDispatcher = StandardTestDispatcher()
|
||||||
|
testScope = TestScope(backgroundDispatcher)
|
||||||
|
Dispatchers.setMain(backgroundDispatcher)
|
||||||
|
|
||||||
|
underTest =
|
||||||
|
FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
|
||||||
|
defaultUserId,
|
||||||
|
fakeFingerprintManagerInteractor,
|
||||||
|
backgroundDispatcher,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
.create(FingerprintSettingsNavigationViewModel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNoGateKeeper_launchesConfirmDeviceCredential() =
|
||||||
|
testScope.runTest {
|
||||||
|
var nextStep: NextStepViewModel? = null
|
||||||
|
val job = launch { underTest.nextStep.collect { nextStep = it } }
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
assertThat(nextStep).isEqualTo(LaunchConfirmDeviceCredential(defaultUserId))
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testConfirmDevice_fails() =
|
||||||
|
testScope.runTest {
|
||||||
|
var nextStep: NextStepViewModel? = null
|
||||||
|
val job = launch { underTest.nextStep.collect { nextStep = it } }
|
||||||
|
|
||||||
|
underTest.onConfirmDevice(false, null)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun confirmDeviceSuccess_noGateKeeper() =
|
||||||
|
testScope.runTest {
|
||||||
|
var nextStep: NextStepViewModel? = null
|
||||||
|
val job = launch { underTest.nextStep.collect { nextStep = it } }
|
||||||
|
|
||||||
|
underTest.onConfirmDevice(true, null)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun confirmDeviceSuccess_launchesEnrollment_ifNoPreviousEnrollments() =
|
||||||
|
testScope.runTest {
|
||||||
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
|
||||||
|
|
||||||
|
var nextStep: NextStepViewModel? = null
|
||||||
|
val job = launch { underTest.nextStep.collect { nextStep = it } }
|
||||||
|
|
||||||
|
underTest.onConfirmDevice(true, 10L)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, 10L, null, null))
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun firstEnrollment_fails() =
|
||||||
|
testScope.runTest {
|
||||||
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
|
||||||
|
|
||||||
|
var nextStep: NextStepViewModel? = null
|
||||||
|
val job = launch { underTest.nextStep.collect { nextStep = it } }
|
||||||
|
|
||||||
|
underTest.onConfirmDevice(true, 10L)
|
||||||
|
underTest.onEnrollFirstFailure("We failed!!")
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun firstEnrollment_failsWithReason() =
|
||||||
|
testScope.runTest {
|
||||||
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
|
||||||
|
|
||||||
|
var nextStep: NextStepViewModel? = null
|
||||||
|
val job = launch { underTest.nextStep.collect { nextStep = it } }
|
||||||
|
|
||||||
|
val failStr = "We failed!!"
|
||||||
|
val failReason = 101
|
||||||
|
|
||||||
|
underTest.onConfirmDevice(true, 10L)
|
||||||
|
underTest.onEnrollFirstFailure(failStr, failReason)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(nextStep).isEqualTo(FinishSettingsWithResult(failReason, failStr))
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun firstEnrollmentSucceeds_noToken() =
|
||||||
|
testScope.runTest {
|
||||||
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
|
||||||
|
|
||||||
|
var nextStep: NextStepViewModel? = null
|
||||||
|
val job = launch { underTest.nextStep.collect { nextStep = it } }
|
||||||
|
|
||||||
|
underTest.onConfirmDevice(true, 10L)
|
||||||
|
underTest.onEnrollFirst(null, null)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(nextStep).isEqualTo(FinishSettings("Error, empty token"))
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun firstEnrollmentSucceeds_noKeyChallenge() =
|
||||||
|
testScope.runTest {
|
||||||
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
|
||||||
|
|
||||||
|
var nextStep: NextStepViewModel? = null
|
||||||
|
val job = launch { underTest.nextStep.collect { nextStep = it } }
|
||||||
|
|
||||||
|
val byteArray = ByteArray(1) { 3 }
|
||||||
|
|
||||||
|
underTest.onConfirmDevice(true, 10L)
|
||||||
|
underTest.onEnrollFirst(byteArray, null)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(nextStep).isEqualTo(FinishSettings("Error, empty keyChallenge"))
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun firstEnrollment_succeeds() =
|
||||||
|
testScope.runTest {
|
||||||
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
|
||||||
|
|
||||||
|
var nextStep: NextStepViewModel? = null
|
||||||
|
val job = testScope.launch { underTest.nextStep.collect { nextStep = it } }
|
||||||
|
|
||||||
|
val byteArray = ByteArray(1) { 3 }
|
||||||
|
val keyChallenge = 89L
|
||||||
|
|
||||||
|
underTest.onConfirmDevice(true, 10L)
|
||||||
|
underTest.onEnrollFirst(byteArray, keyChallenge)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(nextStep).isEqualTo(ShowSettings)
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun enrollAdditionalFingerprints_fails() =
|
||||||
|
testScope.runTest {
|
||||||
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
|
||||||
|
mutableListOf(FingerprintViewModel("a", 1, 3L))
|
||||||
|
fakeFingerprintManagerInteractor.challengeToGenerate = Pair(4L, byteArrayOf(3, 3, 1))
|
||||||
|
|
||||||
|
var nextStep: NextStepViewModel? = null
|
||||||
|
val job = launch { underTest.nextStep.collect { nextStep = it } }
|
||||||
|
|
||||||
|
underTest.onConfirmDevice(true, 10L)
|
||||||
|
runCurrent()
|
||||||
|
underTest.onEnrollAdditionalFailure()
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun enrollAdditional_success() =
|
||||||
|
testScope.runTest {
|
||||||
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
|
||||||
|
mutableListOf(FingerprintViewModel("a", 1, 3L))
|
||||||
|
|
||||||
|
var nextStep: NextStepViewModel? = null
|
||||||
|
val job = launch { underTest.nextStep.collect { nextStep = it } }
|
||||||
|
|
||||||
|
underTest.onConfirmDevice(true, 10L)
|
||||||
|
underTest.onEnrollSuccess()
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(nextStep).isEqualTo(ShowSettings)
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun confirmDeviceCredential_withEnrolledFingerprint_showsSettings() =
|
||||||
|
testScope.runTest {
|
||||||
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
|
||||||
|
mutableListOf(FingerprintViewModel("a", 1, 3L))
|
||||||
|
fakeFingerprintManagerInteractor.challengeToGenerate = Pair(10L, byteArrayOf(1, 2, 3))
|
||||||
|
|
||||||
|
var nextStep: NextStepViewModel? = null
|
||||||
|
val job = launch { underTest.nextStep.collect { nextStep = it } }
|
||||||
|
|
||||||
|
underTest.onConfirmDevice(true, 10L)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(nextStep).isEqualTo(ShowSettings)
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
}
|
@@ -16,317 +16,232 @@
|
|||||||
|
|
||||||
package com.android.settings.fingerprint2.viewmodel
|
package com.android.settings.fingerprint2.viewmodel
|
||||||
|
|
||||||
import android.hardware.fingerprint.Fingerprint
|
import android.hardware.biometrics.SensorProperties
|
||||||
import android.hardware.fingerprint.FingerprintManager
|
import android.hardware.fingerprint.FingerprintSensorProperties
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
|
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.NextStepViewModel
|
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
|
|
||||||
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
|
||||||
|
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.PreferenceViewModel
|
||||||
|
import com.android.settings.fingerprint2.domain.interactor.FakeFingerprintManagerInteractor
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.take
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.Mock
|
|
||||||
import org.mockito.Mockito.anyInt
|
|
||||||
import org.mockito.junit.MockitoJUnit
|
import org.mockito.junit.MockitoJUnit
|
||||||
import org.mockito.junit.MockitoJUnitRunner
|
import org.mockito.junit.MockitoJUnitRunner
|
||||||
import org.mockito.Mockito.`when` as whenever
|
|
||||||
|
|
||||||
@RunWith(MockitoJUnitRunner::class)
|
@RunWith(MockitoJUnitRunner::class)
|
||||||
class FingerprintSettingsViewModelTest {
|
class FingerprintSettingsViewModelTest {
|
||||||
|
|
||||||
@JvmField
|
@JvmField @Rule var rule = MockitoJUnit.rule()
|
||||||
@Rule
|
|
||||||
var rule = MockitoJUnit.rule()
|
|
||||||
|
|
||||||
@Mock
|
@get:Rule val instantTaskRule = InstantTaskExecutorRule()
|
||||||
private lateinit var fingerprintManager: FingerprintManager
|
|
||||||
private lateinit var underTest: FingerprintSettingsViewModel
|
|
||||||
private val defaultUserId = 0
|
|
||||||
|
|
||||||
@Before
|
private lateinit var underTest: FingerprintSettingsViewModel
|
||||||
fun setup() {
|
private lateinit var navigationViewModel: FingerprintSettingsNavigationViewModel
|
||||||
// @formatter:off
|
private val defaultUserId = 0
|
||||||
underTest = FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
|
private var backgroundDispatcher = StandardTestDispatcher()
|
||||||
|
private var testScope = TestScope(backgroundDispatcher)
|
||||||
|
private lateinit var fakeFingerprintManagerInteractor: FakeFingerprintManagerInteractor
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
fakeFingerprintManagerInteractor = FakeFingerprintManagerInteractor()
|
||||||
|
backgroundDispatcher = StandardTestDispatcher()
|
||||||
|
testScope = TestScope(backgroundDispatcher)
|
||||||
|
Dispatchers.setMain(backgroundDispatcher)
|
||||||
|
|
||||||
|
navigationViewModel =
|
||||||
|
FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
|
||||||
|
defaultUserId,
|
||||||
|
fakeFingerprintManagerInteractor,
|
||||||
|
backgroundDispatcher,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
.create(FingerprintSettingsNavigationViewModel::class.java)
|
||||||
|
|
||||||
|
underTest =
|
||||||
|
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
|
||||||
|
defaultUserId,
|
||||||
|
fakeFingerprintManagerInteractor,
|
||||||
|
backgroundDispatcher,
|
||||||
|
navigationViewModel,
|
||||||
|
)
|
||||||
|
.create(FingerprintSettingsViewModel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun authenticate_DoesNotRun_ifOptical() =
|
||||||
|
testScope.runTest {
|
||||||
|
fakeFingerprintManagerInteractor.sensorProps =
|
||||||
|
listOf(
|
||||||
|
FingerprintSensorPropertiesInternal(
|
||||||
|
0 /* sensorId */,
|
||||||
|
SensorProperties.STRENGTH_STRONG,
|
||||||
|
5 /* maxEnrollmentsPerUser */,
|
||||||
|
emptyList() /* ComponentInfoInternal */,
|
||||||
|
FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
|
||||||
|
true /* resetLockoutRequiresHardwareAuthToken */
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
|
||||||
|
mutableListOf(FingerprintViewModel("a", 1, 3L))
|
||||||
|
|
||||||
|
underTest =
|
||||||
|
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
|
||||||
defaultUserId,
|
defaultUserId,
|
||||||
fingerprintManager,
|
fakeFingerprintManagerInteractor,
|
||||||
).create(FingerprintSettingsViewModel::class.java)
|
backgroundDispatcher,
|
||||||
// @formatter:on
|
navigationViewModel,
|
||||||
|
)
|
||||||
|
.create(FingerprintSettingsViewModel::class.java)
|
||||||
|
|
||||||
|
var authAttempt: FingerprintAuthAttemptViewModel? = null
|
||||||
|
val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
|
||||||
|
|
||||||
|
underTest.shouldAuthenticate(true)
|
||||||
|
// Ensure we are showing settings
|
||||||
|
navigationViewModel.onConfirmDevice(true, 10L)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
advanceTimeBy(400)
|
||||||
|
|
||||||
|
assertThat(authAttempt).isNull()
|
||||||
|
job.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testNoGateKeeper_launchesConfirmDeviceCredential() = runTest {
|
fun authenticate_DoesNotRun_ifUltrasonic() =
|
||||||
var nextStep: NextStepViewModel? = null
|
testScope.runTest {
|
||||||
val job = launch {
|
fakeFingerprintManagerInteractor.sensorProps =
|
||||||
underTest.nextStep.collect {
|
listOf(
|
||||||
nextStep = it
|
FingerprintSensorPropertiesInternal(
|
||||||
}
|
0 /* sensorId */,
|
||||||
}
|
SensorProperties.STRENGTH_STRONG,
|
||||||
|
5 /* maxEnrollmentsPerUser */,
|
||||||
underTest.updateTokenAndChallenge(null, null)
|
emptyList() /* ComponentInfoInternal */,
|
||||||
|
FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC,
|
||||||
runCurrent()
|
true /* resetLockoutRequiresHardwareAuthToken */
|
||||||
assertThat(nextStep).isEqualTo(LaunchConfirmDeviceCredential(defaultUserId))
|
)
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testConfirmDevice_fails() = runTest {
|
|
||||||
var nextStep: NextStepViewModel? = null
|
|
||||||
val job = launch {
|
|
||||||
underTest.nextStep.collect {
|
|
||||||
nextStep = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
underTest.updateTokenAndChallenge(null, null)
|
|
||||||
underTest.onConfirmDevice(false, null)
|
|
||||||
|
|
||||||
runCurrent()
|
|
||||||
|
|
||||||
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun confirmDeviceSuccess_noGateKeeper() = runTest {
|
|
||||||
var nextStep: NextStepViewModel? = null
|
|
||||||
val job = launch {
|
|
||||||
underTest.nextStep.collect {
|
|
||||||
nextStep = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
underTest.updateTokenAndChallenge(null, null)
|
|
||||||
underTest.onConfirmDevice(true, null)
|
|
||||||
|
|
||||||
runCurrent()
|
|
||||||
|
|
||||||
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun confirmDeviceSuccess_launchesEnrollment_ifNoPreviousEnrollments() = runTest {
|
|
||||||
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
|
|
||||||
|
|
||||||
var nextStep: NextStepViewModel? = null
|
|
||||||
val job = launch {
|
|
||||||
underTest.nextStep.collect {
|
|
||||||
nextStep = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
underTest.updateTokenAndChallenge(null, null)
|
|
||||||
underTest.onConfirmDevice(true, 10L)
|
|
||||||
|
|
||||||
runCurrent()
|
|
||||||
|
|
||||||
assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, 10L, null, null))
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun firstEnrollment_fails() = runTest {
|
|
||||||
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
|
|
||||||
|
|
||||||
var nextStep: NextStepViewModel? = null
|
|
||||||
val job = launch {
|
|
||||||
underTest.nextStep.collect {
|
|
||||||
nextStep = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
underTest.updateTokenAndChallenge(null, null)
|
|
||||||
underTest.onConfirmDevice(true, 10L)
|
|
||||||
underTest.onEnrollFirstFailure("We failed!!")
|
|
||||||
|
|
||||||
runCurrent()
|
|
||||||
|
|
||||||
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun firstEnrollment_failsWithReason() = runTest {
|
|
||||||
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
|
|
||||||
|
|
||||||
var nextStep: NextStepViewModel? = null
|
|
||||||
val job = launch {
|
|
||||||
underTest.nextStep.collect {
|
|
||||||
nextStep = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val failStr = "We failed!!"
|
|
||||||
val failReason = 101
|
|
||||||
|
|
||||||
underTest.updateTokenAndChallenge(null, null)
|
|
||||||
underTest.onConfirmDevice(true, 10L)
|
|
||||||
underTest.onEnrollFirstFailure(failStr, failReason)
|
|
||||||
|
|
||||||
runCurrent()
|
|
||||||
|
|
||||||
assertThat(nextStep).isEqualTo(FinishSettingsWithResult(failReason, failStr))
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun firstEnrollmentSucceeds_noToken() = runTest {
|
|
||||||
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
|
|
||||||
|
|
||||||
var nextStep: NextStepViewModel? = null
|
|
||||||
val job = launch {
|
|
||||||
underTest.nextStep.collect {
|
|
||||||
nextStep = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
underTest.updateTokenAndChallenge(null, null)
|
|
||||||
underTest.onConfirmDevice(true, 10L)
|
|
||||||
underTest.onEnrollFirst(null, null)
|
|
||||||
|
|
||||||
runCurrent()
|
|
||||||
|
|
||||||
assertThat(nextStep).isEqualTo(FinishSettings("Error, empty token"))
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun firstEnrollmentSucceeds_noKeyChallenge() = runTest {
|
|
||||||
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
|
|
||||||
|
|
||||||
var nextStep: NextStepViewModel? = null
|
|
||||||
val job = launch {
|
|
||||||
underTest.nextStep.collect {
|
|
||||||
nextStep = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val byteArray = ByteArray(1) {
|
|
||||||
3
|
|
||||||
}
|
|
||||||
|
|
||||||
underTest.updateTokenAndChallenge(null, null)
|
|
||||||
underTest.onConfirmDevice(true, 10L)
|
|
||||||
underTest.onEnrollFirst(byteArray, null)
|
|
||||||
|
|
||||||
runCurrent()
|
|
||||||
|
|
||||||
assertThat(nextStep).isEqualTo(FinishSettings("Error, empty keyChallenge"))
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun firstEnrollment_succeeds() = runTest {
|
|
||||||
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList())
|
|
||||||
|
|
||||||
var nextStep: NextStepViewModel? = null
|
|
||||||
val job = launch {
|
|
||||||
underTest.nextStep.collect {
|
|
||||||
nextStep = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val byteArray = ByteArray(1) {
|
|
||||||
3
|
|
||||||
}
|
|
||||||
val keyChallenge = 89L
|
|
||||||
|
|
||||||
underTest.updateTokenAndChallenge(null, null)
|
|
||||||
underTest.onConfirmDevice(true, 10L)
|
|
||||||
underTest.onEnrollFirst(byteArray, keyChallenge)
|
|
||||||
|
|
||||||
runCurrent()
|
|
||||||
|
|
||||||
assertThat(nextStep).isEqualTo(ShowSettings(defaultUserId))
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun confirmDeviceCredential_withEnrolledFingerprint_showsSettings() = runTest {
|
|
||||||
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(
|
|
||||||
listOf(
|
|
||||||
Fingerprint(
|
|
||||||
"a", 1, 2, 3L
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
|
||||||
|
mutableListOf(FingerprintViewModel("a", 1, 3L))
|
||||||
|
|
||||||
var nextStep: NextStepViewModel? = null
|
underTest =
|
||||||
val job = launch {
|
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
|
||||||
underTest.nextStep.collect {
|
defaultUserId,
|
||||||
nextStep = it
|
fakeFingerprintManagerInteractor,
|
||||||
}
|
backgroundDispatcher,
|
||||||
}
|
navigationViewModel,
|
||||||
|
)
|
||||||
|
.create(FingerprintSettingsViewModel::class.java)
|
||||||
|
|
||||||
underTest.updateTokenAndChallenge(null, null)
|
var authAttempt: FingerprintAuthAttemptViewModel? = null
|
||||||
underTest.onConfirmDevice(true, 10L)
|
val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
|
||||||
|
|
||||||
runCurrent()
|
underTest.shouldAuthenticate(true)
|
||||||
|
navigationViewModel.onConfirmDevice(true, 10L)
|
||||||
|
advanceTimeBy(400)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
assertThat(nextStep).isEqualTo(ShowSettings(defaultUserId))
|
assertThat(authAttempt).isNull()
|
||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun enrollAdditionalFingerprints_fails() = runTest {
|
fun authenticate_DoesRun_ifNotUdfps() =
|
||||||
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(
|
testScope.runTest {
|
||||||
listOf(
|
fakeFingerprintManagerInteractor.sensorProps =
|
||||||
Fingerprint(
|
listOf(
|
||||||
"a", 1, 2, 3L
|
FingerprintSensorPropertiesInternal(
|
||||||
)
|
0 /* sensorId */,
|
||||||
)
|
SensorProperties.STRENGTH_STRONG,
|
||||||
|
5 /* maxEnrollmentsPerUser */,
|
||||||
|
emptyList() /* ComponentInfoInternal */,
|
||||||
|
FingerprintSensorProperties.TYPE_POWER_BUTTON,
|
||||||
|
true /* resetLockoutRequiresHardwareAuthToken */
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
|
||||||
|
mutableListOf(FingerprintViewModel("a", 1, 3L))
|
||||||
|
val success = FingerprintAuthAttemptViewModel.Success(1)
|
||||||
|
fakeFingerprintManagerInteractor.authenticateAttempt = success
|
||||||
|
|
||||||
var nextStep: NextStepViewModel? = null
|
underTest =
|
||||||
val job = launch {
|
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
|
||||||
underTest.nextStep.collect {
|
defaultUserId,
|
||||||
nextStep = it
|
fakeFingerprintManagerInteractor,
|
||||||
}
|
backgroundDispatcher,
|
||||||
}
|
navigationViewModel,
|
||||||
|
)
|
||||||
|
.create(FingerprintSettingsViewModel::class.java)
|
||||||
|
|
||||||
underTest.updateTokenAndChallenge(null, null)
|
var authAttempt: FingerprintAuthAttemptViewModel? = null
|
||||||
underTest.onConfirmDevice(true, 10L)
|
|
||||||
underTest.onEnrollAdditionalFailure()
|
|
||||||
|
|
||||||
runCurrent()
|
val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }
|
||||||
|
underTest.shouldAuthenticate(true)
|
||||||
|
navigationViewModel.onConfirmDevice(true, 10L)
|
||||||
|
advanceTimeBy(400)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
assertThat(nextStep).isInstanceOf(FinishSettings::class.java)
|
assertThat(authAttempt).isEqualTo(success)
|
||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun enrollAdditional_success() = runTest {
|
fun deleteDialog_showAndDismiss() = runTest {
|
||||||
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(
|
val fingerprintToDelete = FingerprintViewModel("A", 1, 10L)
|
||||||
listOf(
|
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToDelete)
|
||||||
Fingerprint(
|
|
||||||
"a", 1, 2, 3L
|
underTest =
|
||||||
)
|
FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
|
||||||
)
|
defaultUserId,
|
||||||
|
fakeFingerprintManagerInteractor,
|
||||||
|
backgroundDispatcher,
|
||||||
|
navigationViewModel,
|
||||||
)
|
)
|
||||||
|
.create(FingerprintSettingsViewModel::class.java)
|
||||||
|
|
||||||
var nextStep: NextStepViewModel? = null
|
var dialog: PreferenceViewModel? = null
|
||||||
val job = launch {
|
val dialogJob = launch { underTest.isShowingDialog.collect { dialog = it } }
|
||||||
underTest.nextStep.collect {
|
|
||||||
nextStep = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
underTest.updateTokenAndChallenge(null, null)
|
// Move to the ShowSettings state
|
||||||
underTest.onConfirmDevice(true, 10L)
|
navigationViewModel.onConfirmDevice(true, 10L)
|
||||||
underTest.onEnrollSuccess()
|
runCurrent()
|
||||||
|
underTest.onDeleteClicked(fingerprintToDelete)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
runCurrent()
|
assertThat(dialog is PreferenceViewModel.DeleteDialog)
|
||||||
|
assertThat(dialog).isEqualTo(PreferenceViewModel.DeleteDialog(fingerprintToDelete))
|
||||||
|
|
||||||
assertThat(nextStep).isEqualTo(ShowSettings(defaultUserId))
|
underTest.deleteFingerprint(fingerprintToDelete)
|
||||||
job.cancel()
|
underTest.onDeleteDialogFinished()
|
||||||
}
|
runCurrent()
|
||||||
}
|
|
||||||
|
assertThat(dialog).isNull()
|
||||||
|
|
||||||
|
dialogJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user