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.systemui.keyguard.ui.viewmodel 18 19 import android.animation.ValueAnimator 20 import android.content.Context 21 import android.graphics.Point 22 import androidx.annotation.VisibleForTesting 23 import androidx.core.animation.addListener 24 import com.android.systemui.Flags 25 import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor 26 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor 27 import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor 28 import com.android.systemui.biometrics.shared.model.AuthenticationReason 29 import com.android.systemui.biometrics.shared.model.DisplayRotation 30 import com.android.systemui.biometrics.shared.model.isDefaultOrientation 31 import com.android.systemui.dagger.SysUISingleton 32 import com.android.systemui.dagger.qualifiers.Application 33 import com.android.systemui.dagger.qualifiers.Main 34 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor 35 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 36 import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus 37 import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus 38 import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus 39 import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus 40 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus 41 import com.android.systemui.power.domain.interactor.PowerInteractor 42 import com.android.systemui.res.R 43 import com.android.systemui.statusbar.phone.DozeServiceHost 44 import javax.inject.Inject 45 import kotlinx.coroutines.CoroutineDispatcher 46 import kotlinx.coroutines.CoroutineScope 47 import kotlinx.coroutines.ExperimentalCoroutinesApi 48 import kotlinx.coroutines.Job 49 import kotlinx.coroutines.flow.Flow 50 import kotlinx.coroutines.flow.MutableStateFlow 51 import kotlinx.coroutines.flow.asStateFlow 52 import kotlinx.coroutines.flow.collectLatest 53 import kotlinx.coroutines.flow.combine 54 import kotlinx.coroutines.flow.distinctUntilChanged 55 import kotlinx.coroutines.flow.filter 56 import kotlinx.coroutines.flow.flatMapLatest 57 import kotlinx.coroutines.flow.flowOn 58 import kotlinx.coroutines.flow.launchIn 59 import kotlinx.coroutines.flow.map 60 import kotlinx.coroutines.flow.merge 61 import kotlinx.coroutines.flow.onCompletion 62 import kotlinx.coroutines.launch 63 64 @ExperimentalCoroutinesApi 65 @SysUISingleton 66 class SideFpsProgressBarViewModel 67 @Inject 68 constructor( 69 private val context: Context, 70 biometricStatusInteractor: BiometricStatusInteractor, 71 deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, 72 private val sfpsSensorInteractor: SideFpsSensorInteractor, 73 // todo (b/317432075) Injecting DozeServiceHost directly instead of using it through 74 // DozeInteractor as DozeServiceHost already depends on DozeInteractor. 75 private val dozeServiceHost: DozeServiceHost, 76 private val keyguardInteractor: KeyguardInteractor, 77 displayStateInteractor: DisplayStateInteractor, 78 @Main private val mainDispatcher: CoroutineDispatcher, 79 @Application private val applicationScope: CoroutineScope, 80 private val powerInteractor: PowerInteractor, 81 ) { 82 private val _progress = MutableStateFlow(0.0f) 83 private val _visible = MutableStateFlow(false) 84 private var _animator: ValueAnimator? = null 85 private var animatorJob: Job? = null 86 87 private fun onFingerprintCaptureCompleted() { 88 _visible.value = false 89 _progress.value = 0.0f 90 } 91 92 // Merged [FingerprintAuthenticationStatus] from BiometricPrompt acquired messages and 93 // device entry authentication messages 94 private val mergedFingerprintAuthenticationStatus = 95 merge( 96 biometricStatusInteractor.fingerprintAcquiredStatus, 97 deviceEntryFingerprintAuthInteractor.authenticationStatus 98 ) 99 .distinctUntilChanged() 100 .filter { 101 if (it is AcquiredFingerprintAuthenticationStatus) { 102 it.authenticationReason == AuthenticationReason.DeviceEntryAuthentication || 103 it.authenticationReason == 104 AuthenticationReason.BiometricPromptAuthentication 105 } else { 106 true 107 } 108 } 109 110 val isVisible: Flow<Boolean> = _visible.asStateFlow() 111 112 val progress: Flow<Float> = _progress.asStateFlow() 113 114 val progressBarLength: Flow<Int> = 115 sfpsSensorInteractor.sensorLocation.map { it.length }.distinctUntilChanged() 116 117 val progressBarThickness = 118 context.resources.getDimension(R.dimen.sfps_progress_bar_thickness).toInt() 119 120 val progressBarLocation = 121 combine(displayStateInteractor.currentRotation, sfpsSensorInteractor.sensorLocation, ::Pair) 122 .map { (rotation, sensorLocation) -> 123 val paddingFromEdge = 124 context.resources 125 .getDimension(R.dimen.sfps_progress_bar_padding_from_edge) 126 .toInt() 127 val viewLeftTop = Point(sensorLocation.left, sensorLocation.top) 128 val totalDistanceFromTheEdge = paddingFromEdge + progressBarThickness 129 130 val isSensorVerticalNow = 131 sensorLocation.isSensorVerticalInDefaultOrientation == 132 rotation.isDefaultOrientation() 133 if (isSensorVerticalNow) { 134 // Sensor is vertical to the current orientation, we rotate it 270 deg 135 // around the (left,top) point as the pivot. We need to push it down the 136 // length of the progress bar so that it is still aligned to the sensor 137 viewLeftTop.y += sensorLocation.length 138 val isSensorOnTheNearEdge = 139 rotation == DisplayRotation.ROTATION_180 || 140 rotation == DisplayRotation.ROTATION_90 141 if (isSensorOnTheNearEdge) { 142 // Add just the padding from the edge to push the progress bar right 143 viewLeftTop.x += paddingFromEdge 144 } else { 145 // View left top is pushed left from the edge by the progress bar thickness 146 // and the padding. 147 viewLeftTop.x -= totalDistanceFromTheEdge 148 } 149 } else { 150 // Sensor is horizontal to the current orientation. 151 val isSensorOnTheNearEdge = 152 rotation == DisplayRotation.ROTATION_0 || 153 rotation == DisplayRotation.ROTATION_90 154 if (isSensorOnTheNearEdge) { 155 // Add just the padding from the edge to push the progress bar down 156 viewLeftTop.y += paddingFromEdge 157 } else { 158 // Sensor is now at the bottom edge of the device in the current rotation. 159 // We want to push it up from the bottom edge by the padding and 160 // the thickness of the progressbar. 161 viewLeftTop.y -= totalDistanceFromTheEdge 162 } 163 } 164 viewLeftTop 165 } 166 167 val isFingerprintAuthRunning: Flow<Boolean> = 168 combine( 169 deviceEntryFingerprintAuthInteractor.isRunning, 170 biometricStatusInteractor.sfpsAuthenticationReason 171 ) { deviceEntryAuthIsRunning, sfpsAuthReason -> 172 deviceEntryAuthIsRunning || 173 sfpsAuthReason == AuthenticationReason.BiometricPromptAuthentication 174 } 175 176 val rotation: Flow<Float> = 177 combine(displayStateInteractor.currentRotation, sfpsSensorInteractor.sensorLocation, ::Pair) 178 .map { (rotation, sensorLocation) -> 179 if ( 180 rotation.isDefaultOrientation() == 181 sensorLocation.isSensorVerticalInDefaultOrientation 182 ) { 183 // We should rotate the progress bar 270 degrees in the clockwise direction with 184 // the left top point as the pivot so that it fills up from bottom to top 185 270.0f 186 } else { 187 0.0f 188 } 189 } 190 191 val isProlongedTouchRequiredForAuthentication: Flow<Boolean> = 192 sfpsSensorInteractor.isProlongedTouchRequiredForAuthentication 193 194 init { 195 if (Flags.restToUnlock()) { 196 launchAnimator() 197 } 198 } 199 200 private fun launchAnimator() { 201 applicationScope.launch { 202 sfpsSensorInteractor.isProlongedTouchRequiredForAuthentication.collectLatest { enabled 203 -> 204 if (!enabled) { 205 animatorJob?.cancel() 206 return@collectLatest 207 } 208 animatorJob = 209 sfpsSensorInteractor.authenticationDuration 210 .flatMapLatest { authDuration -> 211 _animator?.cancel() 212 mergedFingerprintAuthenticationStatus.map { 213 authStatus: FingerprintAuthenticationStatus -> 214 when (authStatus) { 215 is AcquiredFingerprintAuthenticationStatus -> { 216 if (authStatus.fingerprintCaptureStarted) { 217 if (keyguardInteractor.isDozing.value) { 218 dozeServiceHost.fireSideFpsAcquisitionStarted() 219 } else { 220 powerInteractor 221 .wakeUpForSideFingerprintAcquisition() 222 } 223 _animator?.cancel() 224 _animator = 225 ValueAnimator.ofFloat(0.0f, 1.0f) 226 .setDuration(authDuration) 227 .apply { 228 addUpdateListener { 229 _progress.value = 230 it.animatedValue as Float 231 } 232 addListener( 233 onEnd = { 234 if (_progress.value == 0.0f) { 235 _visible.value = false 236 } 237 }, 238 onStart = { _visible.value = true }, 239 onCancel = { _visible.value = false } 240 ) 241 } 242 _animator?.start() 243 } else if (authStatus.fingerprintCaptureCompleted) { 244 onFingerprintCaptureCompleted() 245 } else { 246 // Abandoned FP Auth attempt 247 _animator?.reverse() 248 } 249 } 250 is ErrorFingerprintAuthenticationStatus -> 251 onFingerprintCaptureCompleted() 252 is FailFingerprintAuthenticationStatus -> 253 onFingerprintCaptureCompleted() 254 is SuccessFingerprintAuthenticationStatus -> 255 onFingerprintCaptureCompleted() 256 else -> Unit 257 } 258 } 259 } 260 .flowOn(mainDispatcher) 261 .onCompletion { _animator?.cancel() } 262 .launchIn(applicationScope) 263 } 264 } 265 } 266 267 @VisibleForTesting 268 fun setVisible(isVisible: Boolean) { 269 _visible.value = isVisible 270 } 271 } 272