1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.biometrics.fingerprint2.ui.viewmodel 18 19 import android.hardware.fingerprint.FingerprintManager 20 import androidx.lifecycle.ViewModel 21 import androidx.lifecycle.ViewModelProvider 22 import androidx.lifecycle.viewModelScope 23 import com.android.settings.biometrics.BiometricEnrollBase 24 import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor 25 import kotlinx.coroutines.CoroutineDispatcher 26 import kotlinx.coroutines.flow.MutableStateFlow 27 import kotlinx.coroutines.flow.StateFlow 28 import kotlinx.coroutines.flow.asStateFlow 29 import kotlinx.coroutines.flow.update 30 import kotlinx.coroutines.launch 31 32 /** A Viewmodel that represents the navigation of the FingerprintSettings activity. */ 33 class FingerprintSettingsNavigationViewModel( 34 private val userId: Int, 35 private val fingerprintManagerInteractor: FingerprintManagerInteractor, 36 private val backgroundDispatcher: CoroutineDispatcher, 37 tokenInit: ByteArray?, 38 challengeInit: Long?, 39 ) : ViewModel() { 40 41 private var token = tokenInit 42 private var challenge = challengeInit 43 44 private val _nextStep: MutableStateFlow<NextStepViewModel?> = MutableStateFlow(null) 45 /** This flow represents the high level state for the FingerprintSettingsV2Fragment. */ 46 val nextStep: StateFlow<NextStepViewModel?> = _nextStep.asStateFlow() 47 48 init { 49 if (challengeInit == null || tokenInit == null) { <lambda>null50 _nextStep.update { LaunchConfirmDeviceCredential(userId) } 51 } else { <lambda>null52 viewModelScope.launch { showSettingsHelper() } 53 } 54 } 55 56 /** Used to indicate that FingerprintSettings is complete. */ finishnull57 fun finish() { 58 _nextStep.update { null } 59 } 60 61 /** Used to finish settings in certain cases. */ maybeFinishActivitynull62 fun maybeFinishActivity(changingConfig: Boolean) { 63 val isConfirmingOrEnrolling = 64 _nextStep.value is LaunchConfirmDeviceCredential || 65 _nextStep.value is EnrollAdditionalFingerprint || 66 _nextStep.value is EnrollFirstFingerprint || 67 _nextStep.value is LaunchedActivity 68 if (!isConfirmingOrEnrolling && !changingConfig) 69 _nextStep.update { 70 FinishSettingsWithResult(BiometricEnrollBase.RESULT_TIMEOUT, "onStop finishing settings") 71 } 72 } 73 74 /** Used to indicate that we have launched another activity and we should await its result. */ setStepToLaunchednull75 fun setStepToLaunched() { 76 _nextStep.update { LaunchedActivity } 77 } 78 79 /** Indicates a successful enroll has occurred */ onEnrollSuccessnull80 fun onEnrollSuccess() { 81 showSettingsHelper() 82 } 83 84 /** Add fingerprint clicked */ onAddFingerprintClickednull85 fun onAddFingerprintClicked() { 86 _nextStep.update { EnrollAdditionalFingerprint(userId, token) } 87 } 88 89 /** Enrolling of an additional fingerprint failed */ onEnrollAdditionalFailurenull90 fun onEnrollAdditionalFailure() { 91 launchFinishSettings("Failed to enroll additional fingerprint") 92 } 93 94 /** The first fingerprint enrollment failed */ onEnrollFirstFailurenull95 fun onEnrollFirstFailure(reason: String) { 96 launchFinishSettings(reason) 97 } 98 99 /** The first fingerprint enrollment failed with a result code */ onEnrollFirstFailurenull100 fun onEnrollFirstFailure(reason: String, resultCode: Int) { 101 launchFinishSettings(reason, resultCode) 102 } 103 104 /** Notifies that a users first enrollment succeeded. */ onEnrollFirstnull105 fun onEnrollFirst(theToken: ByteArray?, theChallenge: Long?) { 106 if (theToken == null) { 107 launchFinishSettings("Error, empty token") 108 return 109 } 110 if (theChallenge == null) { 111 launchFinishSettings("Error, empty keyChallenge") 112 return 113 } 114 token = theToken!! 115 challenge = theChallenge!! 116 117 showSettingsHelper() 118 } 119 120 /** 121 * Indicates to the view model that a confirm device credential action has been completed with a 122 * [theGateKeeperPasswordHandle] which will be used for [FingerprintManager] operations such as 123 * [FingerprintManager.enroll]. 124 */ onConfirmDevicenull125 suspend fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?) { 126 if (!wasSuccessful) { 127 launchFinishSettings("ConfirmDeviceCredential was unsuccessful") 128 return 129 } 130 if (theGateKeeperPasswordHandle == null) { 131 launchFinishSettings("ConfirmDeviceCredential gatekeeper password was null") 132 return 133 } 134 135 launchEnrollNextStep(theGateKeeperPasswordHandle) 136 } 137 showSettingsHelpernull138 private fun showSettingsHelper() { 139 _nextStep.update { ShowSettings } 140 } 141 launchEnrollNextStepnull142 private suspend fun launchEnrollNextStep(gateKeeperPasswordHandle: Long?) { 143 fingerprintManagerInteractor.enrolledFingerprints.collect { 144 if (it.isEmpty()) { 145 _nextStep.update { EnrollFirstFingerprint(userId, gateKeeperPasswordHandle, null, null) } 146 } else { 147 viewModelScope.launch(backgroundDispatcher) { 148 val challengePair = 149 fingerprintManagerInteractor.generateChallenge(gateKeeperPasswordHandle!!) 150 challenge = challengePair.first 151 token = challengePair.second 152 153 showSettingsHelper() 154 } 155 } 156 } 157 } 158 launchFinishSettingsnull159 private fun launchFinishSettings(reason: String) { 160 _nextStep.update { FinishSettings(reason) } 161 } 162 launchFinishSettingsnull163 private fun launchFinishSettings(reason: String, errorCode: Int) { 164 _nextStep.update { FinishSettingsWithResult(errorCode, reason) } 165 } 166 class FingerprintSettingsNavigationModelFactory( 167 private val userId: Int, 168 private val interactor: FingerprintManagerInteractor, 169 private val backgroundDispatcher: CoroutineDispatcher, 170 private val token: ByteArray?, 171 private val challenge: Long?, 172 ) : ViewModelProvider.Factory { 173 174 @Suppress("UNCHECKED_CAST") createnull175 override fun <T : ViewModel> create( 176 modelClass: Class<T>, 177 ): T { 178 179 return FingerprintSettingsNavigationViewModel( 180 userId, 181 interactor, 182 backgroundDispatcher, 183 token, 184 challenge, 185 ) 186 as T 187 } 188 } 189 } 190