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