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.settings.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.lib.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.last 30 import kotlinx.coroutines.flow.update 31 import kotlinx.coroutines.launch 32 33 /** A Viewmodel that represents the navigation of the FingerprintSettings activity. */ 34 class FingerprintSettingsNavigationViewModel( 35 private val userId: Int, 36 private val fingerprintManagerInteractor: FingerprintManagerInteractor, 37 private val backgroundDispatcher: CoroutineDispatcher, 38 tokenInit: ByteArray?, 39 challengeInit: Long?, 40 ) : ViewModel() { 41 42 private var token = tokenInit 43 private var challenge = challengeInit 44 45 private val _nextStep: MutableStateFlow<NextStepViewModel?> = MutableStateFlow(null) 46 47 /** This flow represents the high level state for the FingerprintSettingsV2Fragment. */ 48 val nextStep: StateFlow<NextStepViewModel?> = _nextStep.asStateFlow() 49 50 init { 51 if (challengeInit == null || tokenInit == null) { <lambda>null52 _nextStep.update { LaunchConfirmDeviceCredential(userId) } 53 } else { <lambda>null54 viewModelScope.launch { 55 if (fingerprintManagerInteractor.enrolledFingerprints.last()?.isEmpty() == true) { 56 _nextStep.update { EnrollFirstFingerprint(userId, null, challenge, token) } 57 } else { 58 showSettingsHelper() 59 } 60 } 61 } 62 } 63 64 /** Used to indicate that FingerprintSettings is complete. */ finishnull65 fun finish() { 66 _nextStep.update { null } 67 } 68 69 /** Used to finish settings in certain cases. */ maybeFinishActivitynull70 fun maybeFinishActivity(changingConfig: Boolean) { 71 val isConfirmingOrEnrolling = 72 _nextStep.value is LaunchConfirmDeviceCredential || 73 _nextStep.value is EnrollAdditionalFingerprint || 74 _nextStep.value is EnrollFirstFingerprint || 75 _nextStep.value is LaunchedActivity 76 if (!isConfirmingOrEnrolling && !changingConfig) 77 _nextStep.update { 78 FinishSettingsWithResult(BiometricEnrollBase.RESULT_TIMEOUT, "onStop finishing settings") 79 } 80 } 81 82 /** Used to indicate that we have launched another activity and we should await its result. */ setStepToLaunchednull83 fun setStepToLaunched() { 84 _nextStep.update { LaunchedActivity } 85 } 86 87 /** Indicates a successful enroll has occurred */ onEnrollSuccessnull88 fun onEnrollSuccess() { 89 showSettingsHelper() 90 } 91 92 /** Add fingerprint clicked */ onAddFingerprintClickednull93 fun onAddFingerprintClicked() { 94 _nextStep.update { EnrollAdditionalFingerprint(userId, token) } 95 } 96 97 /** Enrolling of an additional fingerprint failed */ onEnrollAdditionalFailurenull98 fun onEnrollAdditionalFailure() { 99 launchFinishSettings("Failed to enroll additional fingerprint") 100 } 101 102 /** The first fingerprint enrollment failed */ onEnrollFirstFailurenull103 fun onEnrollFirstFailure(reason: String) { 104 launchFinishSettings(reason) 105 } 106 107 /** The first fingerprint enrollment failed with a result code */ onEnrollFirstFailurenull108 fun onEnrollFirstFailure(reason: String, resultCode: Int) { 109 launchFinishSettings(reason, resultCode) 110 } 111 112 /** Notifies that a users first enrollment succeeded. */ onEnrollFirstnull113 fun onEnrollFirst(theToken: ByteArray?, theChallenge: Long?) { 114 if (theToken == null) { 115 launchFinishSettings("Error, empty token") 116 return 117 } 118 if (theChallenge == null) { 119 launchFinishSettings("Error, empty keyChallenge") 120 return 121 } 122 token = theToken 123 challenge = theChallenge 124 125 showSettingsHelper() 126 } 127 128 /** 129 * Indicates to the view model that a confirm device credential action has been completed with a 130 * [theGateKeeperPasswordHandle] which will be used for [FingerprintManager] operations such as 131 * [FingerprintManager.enroll]. 132 */ onConfirmDevicenull133 suspend fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?) { 134 if (!wasSuccessful) { 135 launchFinishSettings("ConfirmDeviceCredential was unsuccessful") 136 return 137 } 138 if (theGateKeeperPasswordHandle == null) { 139 launchFinishSettings("ConfirmDeviceCredential gatekeeper password was null") 140 return 141 } 142 143 launchEnrollNextStep(theGateKeeperPasswordHandle) 144 } 145 showSettingsHelpernull146 private fun showSettingsHelper() { 147 _nextStep.update { ShowSettings } 148 } 149 launchEnrollNextStepnull150 private suspend fun launchEnrollNextStep(gateKeeperPasswordHandle: Long?) { 151 fingerprintManagerInteractor.enrolledFingerprints.collect { 152 if (it?.isEmpty() == true) { 153 _nextStep.update { EnrollFirstFingerprint(userId, gateKeeperPasswordHandle, null, null) } 154 } else { 155 viewModelScope.launch(backgroundDispatcher) { 156 val challengePair = 157 fingerprintManagerInteractor.generateChallenge(gateKeeperPasswordHandle!!) 158 challenge = challengePair.first 159 token = challengePair.second 160 161 showSettingsHelper() 162 } 163 } 164 } 165 } 166 launchFinishSettingsnull167 private fun launchFinishSettings(reason: String) { 168 _nextStep.update { FinishSettings(reason) } 169 } 170 launchFinishSettingsnull171 private fun launchFinishSettings(reason: String, errorCode: Int) { 172 _nextStep.update { FinishSettingsWithResult(errorCode, reason) } 173 } 174 175 class FingerprintSettingsNavigationModelFactory( 176 private val userId: Int, 177 private val interactor: FingerprintManagerInteractor, 178 private val backgroundDispatcher: CoroutineDispatcher, 179 private val token: ByteArray?, 180 private val challenge: Long?, 181 ) : ViewModelProvider.Factory { 182 183 @Suppress("UNCHECKED_CAST") createnull184 override fun <T : ViewModel> create(modelClass: Class<T>): T { 185 186 return FingerprintSettingsNavigationViewModel( 187 userId, 188 interactor, 189 backgroundDispatcher, 190 token, 191 challenge, 192 ) 193 as T 194 } 195 } 196 } 197