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