1 /*
<lambda>null2 * 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 android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
21 import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC
22 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
23 import android.util.Log
24 import androidx.lifecycle.ViewModel
25 import androidx.lifecycle.ViewModelProvider
26 import androidx.lifecycle.viewModelScope
27 import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
28 import kotlinx.coroutines.CoroutineDispatcher
29 import kotlinx.coroutines.flow.Flow
30 import kotlinx.coroutines.flow.MutableSharedFlow
31 import kotlinx.coroutines.flow.MutableStateFlow
32 import kotlinx.coroutines.flow.combine
33 import kotlinx.coroutines.flow.combineTransform
34 import kotlinx.coroutines.flow.distinctUntilChanged
35 import kotlinx.coroutines.flow.filterNotNull
36 import kotlinx.coroutines.flow.flowOn
37 import kotlinx.coroutines.flow.last
38 import kotlinx.coroutines.flow.sample
39 import kotlinx.coroutines.flow.transformLatest
40 import kotlinx.coroutines.flow.update
41 import kotlinx.coroutines.launch
42
43 private const val TAG = "FingerprintSettingsViewModel"
44 private const val DEBUG = false
45
46 /** Models the UI state for fingerprint settings. */
47 class FingerprintSettingsViewModel(
48 private val userId: Int,
49 private val fingerprintManagerInteractor: FingerprintManagerInteractor,
50 private val backgroundDispatcher: CoroutineDispatcher,
51 private val navigationViewModel: FingerprintSettingsNavigationViewModel,
52 ) : ViewModel() {
53
54 private val _consumerShouldAuthenticate: MutableStateFlow<Boolean> = MutableStateFlow(false)
55
56 private val fingerprintSensorPropertiesInternal:
57 MutableStateFlow<List<FingerprintSensorPropertiesInternal>?> =
58 MutableStateFlow(null)
59
60 private val _isShowingDialog: MutableStateFlow<PreferenceViewModel?> = MutableStateFlow(null)
61 val isShowingDialog =
62 _isShowingDialog.combine(navigationViewModel.nextStep) { dialogFlow, nextStep ->
63 if (nextStep is ShowSettings) {
64 return@combine dialogFlow
65 } else {
66 return@combine null
67 }
68 }
69
70 init {
71 viewModelScope.launch {
72 fingerprintSensorPropertiesInternal.update {
73 fingerprintManagerInteractor.sensorPropertiesInternal()
74 }
75 }
76
77 viewModelScope.launch {
78 navigationViewModel.nextStep.filterNotNull().collect {
79 _isShowingDialog.update { null }
80 if (it is ShowSettings) {
81 // reset state
82 updateSettingsData()
83 }
84 }
85 }
86 }
87
88 private val _fingerprintStateViewModel: MutableStateFlow<FingerprintStateViewModel?> =
89 MutableStateFlow(null)
90 val fingerprintState: Flow<FingerprintStateViewModel?> =
91 _fingerprintStateViewModel.combineTransform(navigationViewModel.nextStep) {
92 settingsShowingViewModel,
93 currStep ->
94 if (currStep != null && currStep is ShowSettings) {
95 emit(settingsShowingViewModel)
96 }
97 }
98
99 private val _isLockedOut: MutableStateFlow<FingerprintAuthAttemptViewModel.Error?> =
100 MutableStateFlow(null)
101
102 private val _authSucceeded: MutableSharedFlow<FingerprintAuthAttemptViewModel.Success?> =
103 MutableSharedFlow()
104
105 private val attemptsSoFar: MutableStateFlow<Int> = MutableStateFlow(0)
106
107 /**
108 * This is a very tricky flow. The current fingerprint manager APIs are not robust, and a proper
109 * implementation would take quite a lot of code to implement, it might be easier to rewrite
110 * FingerprintManager.
111 *
112 * The hack to note is the sample(400), if we call authentications in too close of proximity
113 * without waiting for a response, the fingerprint manager will send us the results of the
114 * previous attempt.
115 */
116 private val canAuthenticate: Flow<Boolean> =
117 combine(
118 _isShowingDialog,
119 navigationViewModel.nextStep,
120 _consumerShouldAuthenticate,
121 _fingerprintStateViewModel,
122 _isLockedOut,
123 attemptsSoFar,
124 fingerprintSensorPropertiesInternal
125 ) { dialogShowing, step, resume, fingerprints, isLockedOut, attempts, sensorProps ->
126 if (DEBUG) {
127 Log.d(
128 TAG,
129 "canAuthenticate(isShowingDialog=${dialogShowing != null}," +
130 "nextStep=${step}," +
131 "resumed=${resume}," +
132 "fingerprints=${fingerprints}," +
133 "lockedOut=${isLockedOut}," +
134 "attempts=${attempts}," +
135 "sensorProps=${sensorProps}"
136 )
137 }
138 if (sensorProps.isNullOrEmpty()) {
139 return@combine false
140 }
141 val sensorType = sensorProps[0].sensorType
142 if (listOf(TYPE_UDFPS_OPTICAL, TYPE_UDFPS_ULTRASONIC).contains(sensorType)) {
143 return@combine false
144 }
145
146 if (step != null && step is ShowSettings) {
147 if (fingerprints?.fingerprintViewModels?.isNotEmpty() == true) {
148 return@combine dialogShowing == null && isLockedOut == null && resume && attempts < 15
149 }
150 }
151 false
152 }
153 .sample(400)
154 .distinctUntilChanged()
155
156 /** Represents a consistent stream of authentication attempts. */
157 val authFlow: Flow<FingerprintAuthAttemptViewModel> =
158 canAuthenticate
159 .transformLatest {
160 try {
161 Log.d(TAG, "canAuthenticate $it")
162 while (it && navigationViewModel.nextStep.value is ShowSettings) {
163 Log.d(TAG, "canAuthenticate authing")
164 attemptingAuth()
165 when (val authAttempt = fingerprintManagerInteractor.authenticate()) {
166 is FingerprintAuthAttemptViewModel.Success -> {
167 onAuthSuccess(authAttempt)
168 emit(authAttempt)
169 }
170 is FingerprintAuthAttemptViewModel.Error -> {
171 if (authAttempt.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
172 lockout(authAttempt)
173 emit(authAttempt)
174 return@transformLatest
175 }
176 }
177 }
178 }
179 } catch (exception: Exception) {
180 Log.d(TAG, "shouldAuthenticate exception $exception")
181 }
182 }
183 .flowOn(backgroundDispatcher)
184
185 /** The rename dialog has finished */
186 fun onRenameDialogFinished() {
187 _isShowingDialog.update { null }
188 }
189
190 /** The delete dialog has finished */
191 fun onDeleteDialogFinished() {
192 _isShowingDialog.update { null }
193 }
194
195 override fun toString(): String {
196 return "userId: $userId\n" + "fingerprintState: ${_fingerprintStateViewModel.value}\n"
197 }
198
199 /** The fingerprint delete button has been clicked. */
200 fun onDeleteClicked(fingerprintViewModel: FingerprintViewModel) {
201 viewModelScope.launch {
202 if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) {
203 _isShowingDialog.tryEmit(PreferenceViewModel.DeleteDialog(fingerprintViewModel))
204 } else {
205 Log.d(TAG, "Ignoring onDeleteClicked due to dialog showing ${_isShowingDialog.value}")
206 }
207 }
208 }
209
210 /** The rename fingerprint dialog has been clicked. */
211 fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) {
212 viewModelScope.launch {
213 if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) {
214 _isShowingDialog.tryEmit(PreferenceViewModel.RenameDialog(fingerprintViewModel))
215 } else {
216 Log.d(TAG, "Ignoring onPrefClicked due to dialog showing ${_isShowingDialog.value}")
217 }
218 }
219 }
220
221 /** A request to delete a fingerprint */
222 fun deleteFingerprint(fp: FingerprintViewModel) {
223 viewModelScope.launch(backgroundDispatcher) {
224 if (fingerprintManagerInteractor.removeFingerprint(fp)) {
225 updateSettingsData()
226 }
227 }
228 }
229
230 /** A request to rename a fingerprint */
231 fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
232 viewModelScope.launch {
233 fingerprintManagerInteractor.renameFingerprint(fp, newName)
234 updateSettingsData()
235 }
236 }
237
238 private fun attemptingAuth() {
239 attemptsSoFar.update { it + 1 }
240 }
241
242 private suspend fun onAuthSuccess(success: FingerprintAuthAttemptViewModel.Success) {
243 _authSucceeded.emit(success)
244 attemptsSoFar.update { 0 }
245 }
246
247 private fun lockout(attemptViewModel: FingerprintAuthAttemptViewModel.Error) {
248 _isLockedOut.update { attemptViewModel }
249 }
250
251 /**
252 * This function is sort of a hack, it's used whenever we want to check for fingerprint state
253 * updates.
254 */
255 private suspend fun updateSettingsData() {
256 Log.d(TAG, "update settings data called")
257 val fingerprints = fingerprintManagerInteractor.enrolledFingerprints.last()
258 val canEnrollFingerprint =
259 fingerprintManagerInteractor.canEnrollFingerprints(fingerprints.size).last()
260 val maxFingerprints = fingerprintManagerInteractor.maxEnrollableFingerprints.last()
261 val hasSideFps = fingerprintManagerInteractor.hasSideFps()
262 val pressToAuthEnabled = fingerprintManagerInteractor.pressToAuthEnabled()
263 _fingerprintStateViewModel.update {
264 FingerprintStateViewModel(
265 fingerprints,
266 canEnrollFingerprint,
267 maxFingerprints,
268 hasSideFps,
269 pressToAuthEnabled
270 )
271 }
272 }
273
274 /** Used to indicate whether the consumer of the view model is ready for authentication. */
275 fun shouldAuthenticate(authenticate: Boolean) {
276 _consumerShouldAuthenticate.update { authenticate }
277 }
278
279 class FingerprintSettingsViewModelFactory(
280 private val userId: Int,
281 private val interactor: FingerprintManagerInteractor,
282 private val backgroundDispatcher: CoroutineDispatcher,
283 private val navigationViewModel: FingerprintSettingsNavigationViewModel,
284 ) : ViewModelProvider.Factory {
285
286 @Suppress("UNCHECKED_CAST")
287 override fun <T : ViewModel> create(
288 modelClass: Class<T>,
289 ): T {
290
291 return FingerprintSettingsViewModel(
292 userId,
293 interactor,
294 backgroundDispatcher,
295 navigationViewModel,
296 )
297 as T
298 }
299 }
300 }
301
combinenull302 private inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
303 flow: Flow<T1>,
304 flow2: Flow<T2>,
305 flow3: Flow<T3>,
306 flow4: Flow<T4>,
307 flow5: Flow<T5>,
308 flow6: Flow<T6>,
309 flow7: Flow<T7>,
310 crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
311 ): Flow<R> {
312 return combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> ->
313 @Suppress("UNCHECKED_CAST")
314 transform(
315 args[0] as T1,
316 args[1] as T2,
317 args[2] as T3,
318 args[3] as T4,
319 args[4] as T5,
320 args[5] as T6,
321 args[6] as T7,
322 )
323 }
324 }
325