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