• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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