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.FloatEvaluator 20 import android.animation.IntEvaluator 21 import com.android.keyguard.KeyguardViewController 22 import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor 23 import com.android.systemui.biometrics.shared.model.SensorLocation 24 import com.android.systemui.dagger.SysUISingleton 25 import com.android.systemui.dagger.qualifiers.Application 26 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor 27 import com.android.systemui.deviceentry.domain.interactor.DeviceEntrySourceInteractor 28 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor 29 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor 30 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 31 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 32 import com.android.systemui.keyguard.shared.model.KeyguardState 33 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition 34 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView 35 import com.android.systemui.scene.shared.flag.SceneContainerFlag 36 import com.android.systemui.shade.domain.interactor.ShadeInteractor 37 import com.android.systemui.util.kotlin.sample 38 import dagger.Lazy 39 import javax.inject.Inject 40 import kotlinx.coroutines.CoroutineScope 41 import kotlinx.coroutines.ExperimentalCoroutinesApi 42 import kotlinx.coroutines.delay 43 import kotlinx.coroutines.flow.Flow 44 import kotlinx.coroutines.flow.SharingStarted 45 import kotlinx.coroutines.flow.StateFlow 46 import kotlinx.coroutines.flow.combine 47 import kotlinx.coroutines.flow.distinctUntilChanged 48 import kotlinx.coroutines.flow.flatMapLatest 49 import kotlinx.coroutines.flow.flow 50 import kotlinx.coroutines.flow.flowOf 51 import kotlinx.coroutines.flow.map 52 import kotlinx.coroutines.flow.merge 53 import kotlinx.coroutines.flow.onStart 54 import kotlinx.coroutines.flow.shareIn 55 import kotlinx.coroutines.flow.stateIn 56 57 /** Models the UI state for the containing device entry icon & long-press handling view. */ 58 @ExperimentalCoroutinesApi 59 @SysUISingleton 60 class DeviceEntryIconViewModel 61 @Inject 62 constructor( 63 transitions: Set<@JvmSuppressWildcards DeviceEntryIconTransition>, 64 burnInInteractor: BurnInInteractor, 65 shadeInteractor: ShadeInteractor, 66 deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor, 67 transitionInteractor: KeyguardTransitionInteractor, 68 val keyguardInteractor: KeyguardInteractor, 69 val viewModel: AodToLockscreenTransitionViewModel, 70 private val keyguardViewController: Lazy<KeyguardViewController>, 71 private val deviceEntryInteractor: DeviceEntryInteractor, 72 private val deviceEntrySourceInteractor: DeviceEntrySourceInteractor, 73 private val accessibilityInteractor: AccessibilityInteractor, 74 @Application private val scope: CoroutineScope, 75 ) { 76 val isUdfpsSupported: StateFlow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported 77 val udfpsLocation: StateFlow<SensorLocation?> = 78 deviceEntryUdfpsInteractor.udfpsLocation.stateIn( 79 scope = scope, 80 started = SharingStarted.Eagerly, 81 initialValue = null, 82 ) 83 private val intEvaluator = IntEvaluator() 84 private val floatEvaluator = FloatEvaluator() 85 private val showingAlternateBouncer: Flow<Boolean> = 86 transitionInteractor.startedKeyguardState.map { keyguardState -> 87 keyguardState == KeyguardState.ALTERNATE_BOUNCER 88 } 89 private val qsProgress: Flow<Float> = shadeInteractor.qsExpansion.onStart { emit(0f) } 90 private val shadeExpansion: Flow<Float> = shadeInteractor.shadeExpansion.onStart { emit(0f) } 91 private val transitionAlpha: Flow<Float> = 92 transitions 93 .map { it.deviceEntryParentViewAlpha } 94 .merge() 95 .shareIn(scope, SharingStarted.WhileSubscribed()) 96 .onStart { emit(initialAlphaFromKeyguardState(transitionInteractor.getCurrentState())) } 97 private val alphaMultiplierFromShadeExpansion: Flow<Float> = 98 combine( 99 showingAlternateBouncer, 100 shadeExpansion, 101 qsProgress, 102 ) { showingAltBouncer, shadeExpansion, qsProgress -> 103 val interpolatedQsProgress = (qsProgress * 2).coerceIn(0f, 1f) 104 if (showingAltBouncer) { 105 1f 106 } else { 107 (1f - shadeExpansion) * (1f - interpolatedQsProgress) 108 } 109 } 110 .onStart { emit(1f) } 111 // Burn-in offsets in AOD 112 private val nonAnimatedBurnInOffsets: Flow<BurnInOffsets> = 113 combine( 114 burnInInteractor.deviceEntryIconXOffset, 115 burnInInteractor.deviceEntryIconYOffset, 116 burnInInteractor.udfpsProgress 117 ) { fullyDozingBurnInX, fullyDozingBurnInY, fullyDozingBurnInProgress -> 118 BurnInOffsets( 119 fullyDozingBurnInX, 120 fullyDozingBurnInY, 121 fullyDozingBurnInProgress, 122 ) 123 } 124 125 private val dozeAmount: Flow<Float> = transitionInteractor.transitionValue(KeyguardState.AOD) 126 // Burn-in offsets that animate based on the transition amount to AOD 127 private val animatedBurnInOffsets: Flow<BurnInOffsets> = 128 combine(nonAnimatedBurnInOffsets, dozeAmount) { burnInOffsets, dozeAmount -> 129 BurnInOffsets( 130 intEvaluator.evaluate(dozeAmount, 0, burnInOffsets.x), 131 intEvaluator.evaluate(dozeAmount, 0, burnInOffsets.y), 132 floatEvaluator.evaluate(dozeAmount, 0, burnInOffsets.progress) 133 ) 134 } 135 136 val deviceEntryViewAlpha: Flow<Float> = 137 combine( 138 transitionAlpha, 139 alphaMultiplierFromShadeExpansion, 140 ) { alpha, alphaMultiplier -> 141 alpha * alphaMultiplier 142 } 143 .stateIn( 144 scope = scope, 145 started = SharingStarted.WhileSubscribed(), 146 initialValue = 0f, 147 ) 148 149 private fun initialAlphaFromKeyguardState(keyguardState: KeyguardState): Float { 150 return when (keyguardState) { 151 KeyguardState.OFF, 152 KeyguardState.PRIMARY_BOUNCER, 153 KeyguardState.DOZING, 154 KeyguardState.DREAMING, 155 KeyguardState.GLANCEABLE_HUB, 156 KeyguardState.GONE, 157 KeyguardState.OCCLUDED, 158 KeyguardState.DREAMING_LOCKSCREEN_HOSTED, 159 KeyguardState.UNDEFINED, -> 0f 160 KeyguardState.AOD, 161 KeyguardState.ALTERNATE_BOUNCER, 162 KeyguardState.LOCKSCREEN, -> 1f 163 } 164 } 165 166 val useBackgroundProtection: StateFlow<Boolean> = isUdfpsSupported 167 val burnInOffsets: Flow<BurnInOffsets> = 168 deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled 169 .flatMapLatest { udfpsEnrolled -> 170 if (udfpsEnrolled) { 171 combine( 172 transitionInteractor.startedKeyguardTransitionStep.sample( 173 shadeInteractor.isAnyFullyExpanded, 174 ::Pair 175 ), 176 animatedBurnInOffsets, 177 nonAnimatedBurnInOffsets, 178 ) { 179 (startedTransitionStep, shadeExpanded), 180 animatedBurnInOffsets, 181 nonAnimatedBurnInOffsets -> 182 if (startedTransitionStep.to == KeyguardState.AOD) { 183 when (startedTransitionStep.from) { 184 KeyguardState.ALTERNATE_BOUNCER -> animatedBurnInOffsets 185 KeyguardState.LOCKSCREEN -> 186 if (shadeExpanded) { 187 nonAnimatedBurnInOffsets 188 } else { 189 animatedBurnInOffsets 190 } 191 else -> nonAnimatedBurnInOffsets 192 } 193 } else if (startedTransitionStep.from == KeyguardState.AOD) { 194 when (startedTransitionStep.to) { 195 KeyguardState.LOCKSCREEN -> animatedBurnInOffsets 196 else -> BurnInOffsets(x = 0, y = 0, progress = 0f) 197 } 198 } else { 199 BurnInOffsets(x = 0, y = 0, progress = 0f) 200 } 201 } 202 } else { 203 // If UDFPS isn't enrolled, we don't show any UI on AOD so there's no need 204 // to use burn in offsets at all 205 flowOf(BurnInOffsets(x = 0, y = 0, progress = 0f)) 206 } 207 } 208 .distinctUntilChanged() 209 210 private val isUnlocked: Flow<Boolean> = 211 if (SceneContainerFlag.isEnabled) { 212 deviceEntryInteractor.isUnlocked 213 } else { 214 keyguardInteractor.isKeyguardDismissible 215 } 216 .flatMapLatest { isUnlocked -> 217 if (!isUnlocked) { 218 flowOf(false) 219 } else { 220 flow { 221 // delay in case device ends up transitioning away from the lock screen; 222 // we don't want to animate to the unlocked icon and just let the 223 // icon fade with the transition to GONE 224 delay(UNLOCKED_DELAY_MS) 225 emit(true) 226 } 227 } 228 } 229 230 val iconType: Flow<DeviceEntryIconView.IconType> = 231 combine( 232 deviceEntryUdfpsInteractor.isListeningForUdfps, 233 isUnlocked, 234 ) { isListeningForUdfps, isUnlocked -> 235 if (isListeningForUdfps) { 236 if (isUnlocked) { 237 // Don't show any UI until isUnlocked=false. This covers the case 238 // when the "Power button instantly locks > 0s" or the device doesn't lock 239 // immediately after a screen time. 240 DeviceEntryIconView.IconType.NONE 241 } else { 242 DeviceEntryIconView.IconType.FINGERPRINT 243 } 244 } else if (isUnlocked) { 245 DeviceEntryIconView.IconType.UNLOCK 246 } else { 247 DeviceEntryIconView.IconType.LOCK 248 } 249 } 250 val isVisible: Flow<Boolean> = deviceEntryViewAlpha.map { it > 0f }.distinctUntilChanged() 251 252 private val isInteractive: Flow<Boolean> = 253 combine( 254 iconType, 255 isUdfpsSupported, 256 ) { deviceEntryStatus, isUdfps -> 257 when (deviceEntryStatus) { 258 DeviceEntryIconView.IconType.LOCK -> isUdfps 259 DeviceEntryIconView.IconType.UNLOCK -> true 260 DeviceEntryIconView.IconType.FINGERPRINT, 261 DeviceEntryIconView.IconType.NONE -> false 262 } 263 } 264 val accessibilityDelegateHint: Flow<DeviceEntryIconView.AccessibilityHintType> = 265 accessibilityInteractor.isEnabled.flatMapLatest { touchExplorationEnabled -> 266 if (touchExplorationEnabled) { 267 iconType.map { it.toAccessibilityHintType() } 268 } else { 269 flowOf(DeviceEntryIconView.AccessibilityHintType.NONE) 270 } 271 } 272 273 val isLongPressEnabled: Flow<Boolean> = isInteractive 274 275 suspend fun onUserInteraction() { 276 if (SceneContainerFlag.isEnabled) { 277 deviceEntryInteractor.attemptDeviceEntry() 278 } else { 279 keyguardViewController.get().showPrimaryBouncer(/* scrim */ true) 280 } 281 deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon() 282 } 283 284 private fun DeviceEntryIconView.IconType.toAccessibilityHintType(): 285 DeviceEntryIconView.AccessibilityHintType { 286 return when (this) { 287 DeviceEntryIconView.IconType.FINGERPRINT, 288 DeviceEntryIconView.IconType.LOCK -> DeviceEntryIconView.AccessibilityHintType.BOUNCER 289 DeviceEntryIconView.IconType.UNLOCK -> DeviceEntryIconView.AccessibilityHintType.ENTER 290 DeviceEntryIconView.IconType.NONE -> DeviceEntryIconView.AccessibilityHintType.NONE 291 } 292 } 293 294 companion object { 295 const val UNLOCKED_DELAY_MS = 50L 296 } 297 } 298 299 data class BurnInOffsets( 300 val x: Int, // current x burn in offset based on the aodTransitionAmount 301 val y: Int, // current y burn in offset based on the aodTransitionAmount 302 val progress: Float, // current progress based on the aodTransitionAmount 303 ) 304