1 /*
<lambda>null2  * Copyright (C) 2024 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.enrollment.modules.enrolling.udfps.ui.viewmodel
18 
19 import android.graphics.PointF
20 import android.hardware.fingerprint.FingerprintEnrollOptions
21 import android.view.MotionEvent
22 import android.view.Surface
23 import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY
24 import androidx.lifecycle.ViewModel
25 import androidx.lifecycle.ViewModelProvider
26 import androidx.lifecycle.viewModelScope
27 import androidx.lifecycle.viewmodel.initializer
28 import androidx.lifecycle.viewmodel.viewModelFactory
29 import com.android.settings.SettingsApplication
30 import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
31 import com.android.settings.biometrics.fingerprint2.domain.interactor.AccessibilityInteractor
32 import com.android.settings.biometrics.fingerprint2.domain.interactor.DebuggingInteractor
33 import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractor
34 import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractor
35 import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintSensorInteractor
36 import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintVibrationEffects
37 import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor
38 import com.android.settings.biometrics.fingerprint2.domain.interactor.TouchEventInteractor
39 import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractor
40 import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor
41 import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor
42 import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
43 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.DescriptionText
44 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.HeaderText
45 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel
46 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintAction
47 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel
48 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep
49 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationViewModel
50 import kotlinx.coroutines.flow.Flow
51 import kotlinx.coroutines.flow.MutableStateFlow
52 import kotlinx.coroutines.flow.SharingStarted
53 import kotlinx.coroutines.flow.asStateFlow
54 import kotlinx.coroutines.flow.combine
55 import kotlinx.coroutines.flow.combineTransform
56 import kotlinx.coroutines.flow.distinctUntilChanged
57 import kotlinx.coroutines.flow.filter
58 import kotlinx.coroutines.flow.filterIsInstance
59 import kotlinx.coroutines.flow.filterNotNull
60 import kotlinx.coroutines.flow.flowOf
61 import kotlinx.coroutines.flow.map
62 import kotlinx.coroutines.flow.shareIn
63 import kotlinx.coroutines.flow.transform
64 import kotlinx.coroutines.flow.update
65 import kotlinx.coroutines.launch
66 
67 /** ViewModel used to drive UDFPS Enrollment through [UdfpsEnrollFragment] */
68 class UdfpsViewModel(
69   val navigationViewModel: FingerprintNavigationViewModel,
70   val fingerprintEnrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel,
71   backgroundViewModel: BackgroundViewModel,
72   val udfpsLastStepViewModel: UdfpsLastStepViewModel,
73   val vibrationInteractor: VibrationInteractor,
74   displayDensityInteractor: DisplayDensityInteractor,
75   debuggingInteractor: DebuggingInteractor,
76   enrollStageInteractor: EnrollStageInteractor,
77   orientationInteractor: OrientationInteractor,
78   udfpsEnrollInteractor: UdfpsEnrollInteractor,
79   fingerprintManager: FingerprintManagerInteractor,
80   accessibilityInteractor: AccessibilityInteractor,
81   sensorRepository: FingerprintSensorInteractor,
82   touchEventInteractor: TouchEventInteractor,
83 ) : ViewModel() {
84 
85   private val isSetupWizard = flowOf(false)
86   private var shouldResetErollment = false
87 
88   private var _enrollState: Flow<FingerEnrollState?> =
89     fingerprintManager.sensorPropertiesInternal.filterNotNull().combine(
90       fingerprintEnrollEnrollingViewModel.enrollFlow
91     ) { props, enroll ->
92       if (props.sensorType.isUdfps()) {
93         enroll
94       } else {
95         null
96       }
97     }
98 
99   /** The current state of the enrollment. */
100   var enrollState: Flow<FingerEnrollState> =
101     combine(fingerprintEnrollEnrollingViewModel.enrollFlowShouldBeRunning, _enrollState) {
102         shouldBeRunning,
103         state ->
104         if (shouldBeRunning) {
105           state
106         } else {
107           null
108         }
109       }
110       .filterNotNull()
111 
112   /** Indicates that overlay has been shown */
113   val overlayShown =
114     enrollState
115       .filterIsInstance<FingerEnrollState.OverlayShown>()
116       .shareIn(viewModelScope, SharingStarted.Eagerly, 1)
117 
118   private var _userInteractedWithSensor = MutableStateFlow(false)
119 
120   /**
121    * This indicates whether the user has interacted with the sensor or not. This indicates if we are
122    * in the initial state of the UI.
123    */
124   val userInteractedWithSensor: Flow<Boolean> =
125     enrollState.transform {
126       val interactiveMessage =
127         when (it) {
128           is FingerEnrollState.Acquired,
129           is FingerEnrollState.EnrollError,
130           is FingerEnrollState.EnrollHelp,
131           is FingerEnrollState.EnrollProgress,
132           is FingerEnrollState.PointerDown,
133           is FingerEnrollState.PointerUp -> true
134           else -> false
135         }
136       val hasPreviouslyInteracted = _userInteractedWithSensor.value or interactiveMessage
137       _userInteractedWithSensor.update { hasPreviouslyInteracted }
138       emit(hasPreviouslyInteracted)
139     }
140 
141   /**
142    * Forwards the property sensor information. This is typically used to recreate views that must be
143    * aligned with the sensor.
144    */
145   val sensorLocation = sensorRepository.fingerprintSensor
146 
147   /** Indicates a step of guided enrollment, the ui should animate the icon to the new location. */
148   val guidedEnrollment: Flow<PointF> =
149     udfpsEnrollInteractor.guidedEnrollmentOffset
150       .distinctUntilChanged()
151       .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 0)
152 
153   private var _lastOrientation: Int? = null
154 
155   /** In case of rotations we should ensure the UI does not re-animate the last state. */
156   private val shouldReplayLastEvent =
157     orientationInteractor.rotation.transform {
158       if (_lastOrientation != null && _lastOrientation!! != it) {
159         emit(true)
160       } else {
161         emit(false)
162       }
163       _lastOrientation = it
164     }
165 
166   /**
167    * This is the saved progress, this is for when views are recreated and need saved state for the
168    * first time.
169    */
170   var progressSaved: Flow<FingerEnrollState.EnrollProgress> =
171     enrollState
172       .filterIsInstance<FingerEnrollState.EnrollProgress>()
173       .combineTransform(shouldReplayLastEvent) { enroll, shouldReplay ->
174         if (shouldReplay) {
175           emit(enroll)
176         }
177       }
178       .shareIn(viewModelScope, SharingStarted.Eagerly, 1)
179 
180   /** Indicates if accessibility is enabled */
181   val accessibilityEnabled =
182     accessibilityInteractor.isAccessibilityEnabled.shareIn(
183       this.viewModelScope,
184       SharingStarted.Eagerly,
185       replay = 1,
186     )
187 
188   /** Indicates if we are in reverse landscape orientation. */
189   val isReverseLandscape =
190     orientationInteractor.rotation
191       .transform { emit(it == Surface.ROTATION_270) }
192       .distinctUntilChanged()
193 
194   /** Indicates if we are in the landscape orientation */
195   val isLandscape =
196     orientationInteractor.rotation
197       .transform { emit(it == Surface.ROTATION_90) }
198       .distinctUntilChanged()
199 
200   private val _touchEvent: MutableStateFlow<MotionEvent?> = MutableStateFlow(null)
201   val touchEvent =
202     _touchEvent.asStateFlow().filterNotNull()
203 
204   /** Determines the current [EnrollStageModel] enrollment is in */
205   private val enrollStage: Flow<EnrollStageModel> =
206     combine(enrollStageInteractor.enrollStageThresholds, enrollState) { thresholds, event ->
207         if (event is FingerEnrollState.EnrollProgress) {
208           val progress =
209             (event.totalStepsRequired - event.remainingSteps).toFloat() / event.totalStepsRequired
210           var stageToReturn: EnrollStageModel = EnrollStageModel.Center
211           thresholds.forEach { (threshold, stage) ->
212             if (progress < threshold) {
213               return@forEach
214             }
215             stageToReturn = stage
216           }
217           stageToReturn
218         } else {
219           null
220         }
221       }
222       .filterNotNull()
223       .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
224 
225   /** The saved version of [guidedEnrollment] */
226   val guidedEnrollmentSaved: Flow<PointF> =
227     combineTransform(guidedEnrollment, shouldReplayLastEvent, enrollStage) {
228         point,
229         shouldReplay,
230         stage ->
231         if (shouldReplay && stage is EnrollStageModel.Guided) {
232           emit(point)
233         }
234       }
235       .shareIn(viewModelScope, SharingStarted.Eagerly, 1)
236 
237   init {
238     viewModelScope.launch {
239       enrollState
240         .combine(accessibilityEnabled) { event, isEnabled -> Pair(event, isEnabled) }
241         .collect {
242           if (
243             when (it.first) {
244               is FingerEnrollState.EnrollError -> true
245               is FingerEnrollState.EnrollHelp -> it.second
246               is FingerEnrollState.EnrollProgress -> true
247               else -> false
248             }
249           ) {
250             vibrate(it.first)
251           }
252         }
253     }
254     viewModelScope.launch {
255       enrollStage.collect {
256         udfpsEnrollInteractor.updateGuidedEnrollment(it is EnrollStageModel.Guided)
257       }
258     }
259 
260     viewModelScope.launch {
261       enrollState.filterIsInstance<FingerEnrollState.EnrollProgress>().collect {
262         udfpsEnrollInteractor.onEnrollmentStep(it.remainingSteps, it.totalStepsRequired)
263       }
264     }
265 
266     viewModelScope.launch {
267       backgroundViewModel.background.filter { it }.collect { didGoToBackground() }
268     }
269 
270     viewModelScope.launch {
271       touchEventInteractor.touchEvent.collect {
272         _touchEvent.update { it }
273       }
274     }
275   }
276 
277   /** Indicates if we should show the lottie. */
278   val shouldShowLottie: Flow<Boolean> =
279     combine(
280         displayDensityInteractor.displayDensity,
281         displayDensityInteractor.defaultDisplayDensity,
282         displayDensityInteractor.fontScale,
283       ) { currDisplayDensity, defaultDisplayDensity, fontScale ->
284         if (fontScale > 1.0f) {
285           false
286         } else {
287           defaultDisplayDensity == currDisplayDensity
288         }
289       }
290       .shareIn(viewModelScope, SharingStarted.Eagerly, 1)
291 
292   /** The header text for UDFPS enrollment */
293   val headerText: Flow<HeaderText> =
294     combine(isSetupWizard, accessibilityEnabled, enrollStage) { isSuw, isAccessibility, stage ->
295         return@combine HeaderText(isSuw, isAccessibility, stage)
296       }
297       .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
298 
299   /** Indicates if we should or shold not draw the fingerprint icon */
300   val shouldDrawIcon: Flow<Boolean> =
301     enrollState.transform { state ->
302       when (state) {
303         is FingerEnrollState.EnrollProgress,
304         is FingerEnrollState.EnrollError,
305         is FingerEnrollState.PointerUp -> emit(true)
306         is FingerEnrollState.PointerDown -> emit(false)
307         else -> {}
308       }
309     }
310 
311   private val shouldClearDescriptionText = enrollStage.map { it is EnrollStageModel.Unknown }
312 
313   /** The description text for UDFPS enrollment */
314   val descriptionText: Flow<DescriptionText?> =
315     combine(isSetupWizard, accessibilityEnabled, enrollStage, shouldClearDescriptionText) {
316         isSuw,
317         isAccessibility,
318         stage,
319         shouldClearText ->
320         if (shouldClearText) {
321           return@combine null
322         } else {
323           return@combine DescriptionText(isSuw, isAccessibility, stage)
324         }
325       }
326       .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
327 
328   /** Indicates if the consumer is ready for enrollment */
329   fun readyForEnrollment() {
330     if (shouldResetErollment) {
331       shouldResetErollment = false
332       _enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow
333     }
334     fingerprintEnrollEnrollingViewModel.canEnroll()
335   }
336 
337   /** Indicates if enrollment should stop */
338   fun stopEnrollment() {
339     fingerprintEnrollEnrollingViewModel.stopEnroll()
340   }
341 
342   /** Indicates the negative button has been clicked */
343   fun negativeButtonClicked() {
344     navigationViewModel.update(
345       FingerprintAction.NEGATIVE_BUTTON_PRESSED,
346       navStep,
347       "$TAG#negativeButtonClicked",
348     )
349     doReset()
350   }
351 
352   /** Indicates that an enrollment was completed */
353   fun finishedSuccessfully() {
354     doReset()
355     navigationViewModel.update(FingerprintAction.NEXT, navStep, "${TAG}#progressFinished")
356   }
357 
358   /** Indicates that the application went to the background. */
359   fun didGoToBackground() {
360     navigationViewModel.update(
361       FingerprintAction.DID_GO_TO_BACKGROUND,
362       navStep,
363       "$TAG#didGoToBackground",
364     )
365     stopEnrollment()
366   }
367 
368   private fun doReset() {
369     _enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow
370     _userInteractedWithSensor.update { false }
371   }
372 
373   /** The lottie that should be shown for UDFPS Enrollment */
374   val lottie: Flow<EducationAnimationModel> =
375     combine(isSetupWizard, accessibilityEnabled, enrollStage) { isSuw, isAccessibility, stage ->
376         return@combine EducationAnimationModel(isSuw, isAccessibility, stage)
377       }
378       .distinctUntilChanged()
379       .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
380 
381   /** Indicates we should send a vibration event */
382   private fun vibrate(event: FingerEnrollState) {
383     val vibrationEvent =
384       when (event) {
385         is FingerEnrollState.EnrollError -> FingerprintVibrationEffects.UdfpsError
386         is FingerEnrollState.EnrollHelp -> FingerprintVibrationEffects.UdfpsHelp
387         is FingerEnrollState.EnrollProgress -> FingerprintVibrationEffects.UdfpsSuccess
388         else -> FingerprintVibrationEffects.UdfpsError
389       }
390     vibrationInteractor.vibrate(vibrationEvent, "UdfpsEnrollFragment")
391   }
392 
393   /** Indicates an error sent by the HAL has been acknowledged by the user */
394   fun errorDialogShown(it: FingerEnrollState.EnrollError) {
395     navigationViewModel.update(
396       FingerprintAction.USER_CLICKED_FINISH,
397       navStep,
398       "${TAG}#userClickedStopEnrollingDialog",
399     )
400   }
401 
402   /** Starts enrollment. */
403   fun enroll(enrollOptions: FingerprintEnrollOptions) {
404     fingerprintEnrollEnrollingViewModel.enroll(enrollOptions)
405   }
406 
407   /** Indicates a touch event has occurred. */
408   fun onTouchEvent(event: MotionEvent) {
409     _touchEvent.update { event }
410   }
411 
412   companion object {
413     private val navStep = FingerprintNavigationStep.Enrollment::class
414     private const val TAG = "UDFPSViewModel"
415     val Factory: ViewModelProvider.Factory = viewModelFactory {
416       initializer {
417         val settingsApplication =
418           this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as SettingsApplication
419         val biometricEnvironment = settingsApplication.biometricEnvironment!!
420         val provider = ViewModelProvider(this[VIEW_MODEL_STORE_OWNER_KEY]!!)
421 
422         UdfpsViewModel(
423           provider[FingerprintNavigationViewModel::class],
424           provider[FingerprintEnrollEnrollingViewModel::class],
425           provider[BackgroundViewModel::class],
426           provider[UdfpsLastStepViewModel::class],
427           biometricEnvironment.vibrationInteractor,
428           biometricEnvironment.displayDensityInteractor,
429           biometricEnvironment.debuggingInteractor,
430           biometricEnvironment.enrollStageInteractor,
431           biometricEnvironment.orientationInteractor,
432           biometricEnvironment.udfpsEnrollInteractor,
433           biometricEnvironment.fingerprintManagerInteractor,
434           biometricEnvironment.accessibilityInteractor,
435           biometricEnvironment.sensorInteractor,
436           biometricEnvironment.touchEventInteractor,
437         )
438       }
439     }
440   }
441 }
442