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