• 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.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