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