• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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