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