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