1 /*
<lambda>null2  *  Copyright (C) 2022 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 
18 @file:OptIn(ExperimentalCoroutinesApi::class)
19 
20 package com.android.systemui.keyguard.domain.interactor
21 
22 import android.app.StatusBarManager
23 import android.graphics.Point
24 import android.util.MathUtils
25 import com.android.app.animation.Interpolators
26 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
27 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
28 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
29 import com.android.systemui.common.shared.model.NotificationContainerBounds
30 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
31 import com.android.systemui.dagger.SysUISingleton
32 import com.android.systemui.dagger.qualifiers.Application
33 import com.android.systemui.keyguard.MigrateClocksToBlueprint
34 import com.android.systemui.keyguard.data.repository.KeyguardRepository
35 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
36 import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel
37 import com.android.systemui.keyguard.shared.model.DozeStateModel
38 import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
39 import com.android.systemui.keyguard.shared.model.DozeTransitionModel
40 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
41 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
42 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
43 import com.android.systemui.keyguard.shared.model.StatusBarState
44 import com.android.systemui.power.domain.interactor.PowerInteractor
45 import com.android.systemui.res.R
46 import com.android.systemui.scene.domain.interactor.SceneInteractor
47 import com.android.systemui.scene.shared.flag.SceneContainerFlag
48 import com.android.systemui.scene.shared.model.Scenes
49 import com.android.systemui.shade.data.repository.ShadeRepository
50 import com.android.systemui.statusbar.CommandQueue
51 import com.android.systemui.statusbar.notification.NotificationUtils.interpolate
52 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
53 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
54 import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter
55 import com.android.systemui.util.kotlin.pairwise
56 import com.android.systemui.util.kotlin.sample
57 import javax.inject.Inject
58 import javax.inject.Provider
59 import kotlinx.coroutines.CoroutineScope
60 import kotlinx.coroutines.ExperimentalCoroutinesApi
61 import kotlinx.coroutines.channels.awaitClose
62 import kotlinx.coroutines.delay
63 import kotlinx.coroutines.flow.Flow
64 import kotlinx.coroutines.flow.MutableStateFlow
65 import kotlinx.coroutines.flow.SharingStarted
66 import kotlinx.coroutines.flow.StateFlow
67 import kotlinx.coroutines.flow.asStateFlow
68 import kotlinx.coroutines.flow.combine
69 import kotlinx.coroutines.flow.combineTransform
70 import kotlinx.coroutines.flow.debounce
71 import kotlinx.coroutines.flow.distinctUntilChanged
72 import kotlinx.coroutines.flow.filter
73 import kotlinx.coroutines.flow.first
74 import kotlinx.coroutines.flow.flatMapLatest
75 import kotlinx.coroutines.flow.flow
76 import kotlinx.coroutines.flow.flowOf
77 import kotlinx.coroutines.flow.map
78 import kotlinx.coroutines.flow.merge
79 import kotlinx.coroutines.flow.onStart
80 import kotlinx.coroutines.flow.stateIn
81 import kotlinx.coroutines.flow.transform
82 
83 /**
84  * Encapsulates business-logic related to the keyguard but not to a more specific part within it.
85  */
86 @SysUISingleton
87 class KeyguardInteractor
88 @Inject
89 constructor(
90     private val repository: KeyguardRepository,
91     private val commandQueue: CommandQueue,
92     powerInteractor: PowerInteractor,
93     bouncerRepository: KeyguardBouncerRepository,
94     configurationInteractor: ConfigurationInteractor,
95     shadeRepository: ShadeRepository,
96     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
97     sceneInteractorProvider: Provider<SceneInteractor>,
98     private val fromGoneTransitionInteractor: Provider<FromGoneTransitionInteractor>,
99     private val fromLockscreenTransitionInteractor: Provider<FromLockscreenTransitionInteractor>,
100     sharedNotificationContainerInteractor: Provider<SharedNotificationContainerInteractor>,
101     @Application applicationScope: CoroutineScope,
102 ) {
103     // TODO(b/296118689): move to a repository
104     private val _notificationPlaceholderBounds = MutableStateFlow(NotificationContainerBounds())
105 
106     // When going to AOD, we interpolate bounds when receiving the new bounds
107     // When going back to LS, we'll apply new bounds directly
108     private val _nonSplitShadeNotifciationPlaceholderBounds =
109         _notificationPlaceholderBounds.pairwise().flatMapLatest { (oldBounds, newBounds) ->
110             val lastChangeStep = keyguardTransitionInteractor.transitionState.first()
111             if (lastChangeStep.to == AOD) {
112                 keyguardTransitionInteractor.transitionState.map { step ->
113                     val startingProgress = lastChangeStep.value
114                     val progress = step.value
115                     if (step.to == AOD && progress >= startingProgress && startingProgress < 1f) {
116                         val adjustedProgress =
117                             ((progress - startingProgress) / (1F - startingProgress)).coerceIn(
118                                 0F,
119                                 1F
120                             )
121                         val top = interpolate(oldBounds.top, newBounds.top, adjustedProgress)
122                         val bottom =
123                             interpolate(
124                                 oldBounds.bottom,
125                                 newBounds.bottom,
126                                 adjustedProgress.coerceIn(0F, 1F)
127                             )
128                         NotificationContainerBounds(top = top, bottom = bottom)
129                     } else {
130                         newBounds
131                     }
132                 }
133             } else {
134                 flow { emit(newBounds) }
135             }
136         }
137 
138     /** Bounds of the notification container. */
139     val notificationContainerBounds: StateFlow<NotificationContainerBounds> by lazy {
140         SceneContainerFlag.assertInLegacyMode()
141         combine(
142                 _notificationPlaceholderBounds,
143                 _nonSplitShadeNotifciationPlaceholderBounds,
144                 sharedNotificationContainerInteractor.get().configurationBasedDimensions,
145             ) { bounds, nonSplitShadeBounds: NotificationContainerBounds, cfg ->
146                 // We offset the placeholder bounds by the configured top margin to account for
147                 // legacy placement behavior within notifications for splitshade.
148                 if (MigrateClocksToBlueprint.isEnabled) {
149                     if (cfg.useSplitShade) {
150                         bounds.copy(bottom = bounds.bottom - cfg.keyguardSplitShadeTopMargin)
151                     } else {
152                         nonSplitShadeBounds
153                     }
154                 } else bounds
155             }
156             .stateIn(
157                 scope = applicationScope,
158                 started = SharingStarted.WhileSubscribed(),
159                 initialValue = NotificationContainerBounds(),
160             )
161     }
162 
163     fun setNotificationContainerBounds(position: NotificationContainerBounds) {
164         SceneContainerFlag.assertInLegacyMode()
165         _notificationPlaceholderBounds.value = position
166     }
167 
168     /**
169      * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at
170      * all.
171      */
172     val dozeAmount: Flow<Float> = repository.linearDozeAmount
173 
174     /** Whether the system is in doze mode. */
175     val isDozing: StateFlow<Boolean> = repository.isDozing
176 
177     /** Receive an event for doze time tick */
178     val dozeTimeTick: Flow<Long> = repository.dozeTimeTick
179 
180     /** Whether Always-on Display mode is available. */
181     val isAodAvailable: StateFlow<Boolean> = repository.isAodAvailable
182 
183     /** Doze transition information. */
184     val dozeTransitionModel: Flow<DozeTransitionModel> = repository.dozeTransitionModel
185 
186     val isPulsing: Flow<Boolean> = dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING }
187 
188     /**
189      * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true,
190      * but not vice-versa.
191      */
192     val isDreaming: Flow<Boolean> = repository.isDreaming
193 
194     /** Whether the system is dreaming with an overlay active */
195     val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay
196 
197     /** Whether the system is dreaming and the active dream is hosted in lockscreen */
198     val isActiveDreamLockscreenHosted: StateFlow<Boolean> = repository.isActiveDreamLockscreenHosted
199 
200     /** Event for when the camera gesture is detected */
201     val onCameraLaunchDetected: Flow<CameraLaunchSourceModel> = conflatedCallbackFlow {
202         val callback =
203             object : CommandQueue.Callbacks {
204                 override fun onCameraLaunchGestureDetected(source: Int) {
205                     trySendWithFailureLogging(
206                         cameraLaunchSourceIntToModel(source),
207                         TAG,
208                         "updated onCameraLaunchGestureDetected"
209                     )
210                 }
211             }
212 
213         commandQueue.addCallback(callback)
214 
215         awaitClose { commandQueue.removeCallback(callback) }
216     }
217 
218     /**
219      * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means
220      * that doze mode is not running and DREAMING is ok to commence.
221      *
222      * Allow a brief moment to prevent rapidly oscillating between true/false signals.
223      */
224     val isAbleToDream: Flow<Boolean> =
225         merge(isDreaming, isDreamingWithOverlay)
226             .combine(dozeTransitionModel) { isDreaming, dozeTransitionModel ->
227                 isDreaming && isDozeOff(dozeTransitionModel.to)
228             }
229             .sample(powerInteractor.isAwake) { isAbleToDream, isAwake -> isAbleToDream && isAwake }
230             .debounce(50L)
231             .stateIn(
232                 scope = applicationScope,
233                 started = SharingStarted.WhileSubscribed(),
234                 initialValue = false,
235             )
236 
237     /** Whether the keyguard is showing or not. */
238     @Deprecated("Use KeyguardTransitionInteractor + KeyguardState")
239     val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
240 
241     /** Whether the keyguard is dismissible or not. */
242     val isKeyguardDismissible: StateFlow<Boolean> = repository.isKeyguardDismissible
243 
244     /** Whether the keyguard is occluded (covered by an activity). */
245     @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.OCCLUDED")
246     val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded
247 
248     /** Whether the keyguard is going away. */
249     @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.GONE")
250     val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway
251 
252     /** Keyguard can be clipped at the top as the shade is dragged */
253     val topClippingBounds: Flow<Int?> by lazy {
254         repository.topClippingBounds
255             .sampleFilter(
256                 keyguardTransitionInteractor
257                     .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
258                     .onStart { emit(0f) }
259             ) { goneValue ->
260                 goneValue != 1f
261             }
262             .distinctUntilChanged()
263     }
264 
265     /** Last point that [KeyguardRootView] view was tapped */
266     val lastRootViewTapPosition: Flow<Point?> = repository.lastRootViewTapPosition.asStateFlow()
267 
268     /** Is the ambient indication area visible? */
269     val ambientIndicationVisible: Flow<Boolean> = repository.ambientIndicationVisible.asStateFlow()
270 
271     /** Whether the primary bouncer is showing or not. */
272     @JvmField val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerShow
273 
274     /** Whether the alternate bouncer is showing or not. */
275     val alternateBouncerShowing: Flow<Boolean> =
276         bouncerRepository.alternateBouncerVisible.sample(isAbleToDream) {
277             alternateBouncerVisible,
278             isAbleToDream ->
279             if (isAbleToDream) {
280                 // If the alternate bouncer will show over a dream, it is likely that the dream has
281                 // requested a dismissal, which will stop the dream. By delaying this slightly, the
282                 // DREAMING->LOCKSCREEN transition will now happen first, followed by
283                 // LOCKSCREEN->ALTERNATE_BOUNCER.
284                 delay(600L)
285             }
286             alternateBouncerVisible
287         }
288 
289     /** Observable for the [StatusBarState] */
290     val statusBarState: Flow<StatusBarState> = repository.statusBarState
291 
292     /** Observable for [BiometricUnlockModel] when biometrics are used to unlock the device. */
293     val biometricUnlockState: Flow<BiometricUnlockModel> = repository.biometricUnlockState
294 
295     /** Keyguard is present and is not occluded. */
296     val isKeyguardVisible: Flow<Boolean> =
297         combine(isKeyguardShowing, isKeyguardOccluded) { showing, occluded -> showing && !occluded }
298 
299     /** Whether camera is launched over keyguard. */
300     val isSecureCameraActive: Flow<Boolean> by lazy {
301         combine(
302                 isKeyguardVisible,
303                 primaryBouncerShowing,
304                 onCameraLaunchDetected,
305             ) { isKeyguardVisible, isPrimaryBouncerShowing, cameraLaunchEvent ->
306                 when {
307                     isKeyguardVisible -> false
308                     isPrimaryBouncerShowing -> false
309                     else -> cameraLaunchEvent == CameraLaunchSourceModel.POWER_DOUBLE_TAP
310                 }
311             }
312             .onStart { emit(false) }
313     }
314 
315     /** The approximate location on the screen of the fingerprint sensor, if one is available. */
316     val fingerprintSensorLocation: Flow<Point?> = repository.fingerprintSensorLocation
317 
318     /** The approximate location on the screen of the face unlock sensor, if one is available. */
319     val faceSensorLocation: Flow<Point?> = repository.faceSensorLocation
320 
321     @Deprecated("Use the relevant TransitionViewModel")
322     val keyguardAlpha: Flow<Float> = repository.keyguardAlpha
323 
324     /**
325      * When the lockscreen can be dismissed, emit an alpha value as the user swipes up. This is
326      * useful just before the code commits to moving to GONE.
327      *
328      * This uses legacyShadeExpansion to process swipe up events. In the future, the touch input
329      * signal should be sent directly to transitions.
330      */
331     val dismissAlpha: Flow<Float> =
332         shadeRepository.legacyShadeExpansion
333             .sampleCombine(
334                 statusBarState,
335                 keyguardTransitionInteractor.currentKeyguardState,
336                 keyguardTransitionInteractor.transitionState,
337                 isKeyguardDismissible,
338             )
339             .filter { (_, _, _, step, _) -> !step.transitionState.isTransitioning() }
340             .transform {
341                 (
342                     legacyShadeExpansion,
343                     statusBarState,
344                     currentKeyguardState,
345                     step,
346                     isKeyguardDismissible) ->
347                 if (
348                     statusBarState == StatusBarState.KEYGUARD &&
349                         isKeyguardDismissible &&
350                         currentKeyguardState == LOCKSCREEN &&
351                         legacyShadeExpansion != 1f
352                 ) {
353                     emit(MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, legacyShadeExpansion))
354                 } else if (legacyShadeExpansion == 0f || legacyShadeExpansion == 1f) {
355                     // Resets alpha state
356                     emit(1f)
357                 }
358             }
359             .onStart { emit(1f) }
360             .distinctUntilChanged()
361 
362     val keyguardTranslationY: Flow<Float> =
363         configurationInteractor
364             .dimensionPixelSize(R.dimen.keyguard_translate_distance_on_swipe_up)
365             .flatMapLatest { translationDistance ->
366                 combineTransform(
367                     shadeRepository.legacyShadeExpansion.onStart { emit(0f) },
368                     keyguardTransitionInteractor.transitionValue(GONE).onStart { emit(0f) },
369                 ) { legacyShadeExpansion, goneValue ->
370                     val isLegacyShadeInResetPosition =
371                         legacyShadeExpansion == 0f || legacyShadeExpansion == 1f
372                     if (goneValue == 1f || (goneValue == 0f && isLegacyShadeInResetPosition)) {
373                         // Reset the translation value
374                         emit(0f)
375                     } else if (!isLegacyShadeInResetPosition) {
376                         // On swipe up, translate the keyguard to reveal the bouncer, OR a GONE
377                         // transition is running, which means this is a swipe to dismiss. Values of
378                         // 0f and 1f need to be ignored in the legacy shade expansion. These can
379                         // flip arbitrarily as the legacy shade is reset, and would cause the
380                         // translation value to jump around unexpectedly.
381                         emit(
382                             MathUtils.lerp(
383                                 translationDistance,
384                                 0,
385                                 Interpolators.FAST_OUT_LINEAR_IN.getInterpolation(
386                                     legacyShadeExpansion
387                                 ),
388                             )
389                         )
390                     }
391                 }
392             }
393             .stateIn(
394                 scope = applicationScope,
395                 started = SharingStarted.WhileSubscribed(),
396                 initialValue = 0f,
397             )
398 
399     val clockShouldBeCentered: Flow<Boolean> = repository.clockShouldBeCentered
400 
401     /** Whether to animate the next doze mode transition. */
402     val animateDozingTransitions: Flow<Boolean> by lazy {
403         if (SceneContainerFlag.isEnabled) {
404             sceneInteractorProvider
405                 .get()
406                 .transitioningTo
407                 .map { it == Scenes.Lockscreen }
408                 .distinctUntilChanged()
409                 .flatMapLatest { isTransitioningToLockscreenScene ->
410                     if (isTransitioningToLockscreenScene) {
411                         flowOf(false)
412                     } else {
413                         repository.animateBottomAreaDozingTransitions
414                     }
415                 }
416         } else {
417             repository.animateBottomAreaDozingTransitions
418         }
419     }
420 
421     /**
422      * Whether the primary authentication is required for the given user due to lockdown or
423      * encryption after reboot.
424      */
425     val isEncryptedOrLockdown: Flow<Boolean> = repository.isEncryptedOrLockdown
426 
427     fun dozeTransitionTo(vararg states: DozeStateModel): Flow<DozeTransitionModel> {
428         return dozeTransitionModel.filter { states.contains(it.to) }
429     }
430 
431     fun isKeyguardShowing(): Boolean {
432         return repository.isKeyguardShowing()
433     }
434 
435     private fun cameraLaunchSourceIntToModel(value: Int): CameraLaunchSourceModel {
436         return when (value) {
437             StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE -> CameraLaunchSourceModel.WIGGLE
438             StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP ->
439                 CameraLaunchSourceModel.POWER_DOUBLE_TAP
440             StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER ->
441                 CameraLaunchSourceModel.LIFT_TRIGGER
442             StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE ->
443                 CameraLaunchSourceModel.QUICK_AFFORDANCE
444             else -> throw IllegalArgumentException("Invalid CameraLaunchSourceModel value: $value")
445         }
446     }
447 
448     fun setIsActiveDreamLockscreenHosted(isLockscreenHosted: Boolean) {
449         repository.setIsActiveDreamLockscreenHosted(isLockscreenHosted)
450     }
451 
452     /** Sets whether quick settings or quick-quick settings is visible. */
453     fun setQuickSettingsVisible(isVisible: Boolean) {
454         repository.setQuickSettingsVisible(isVisible)
455     }
456 
457     fun setAlpha(alpha: Float) {
458         repository.setKeyguardAlpha(alpha)
459     }
460 
461     fun setAnimateDozingTransitions(animate: Boolean) {
462         repository.setAnimateDozingTransitions(animate)
463     }
464 
465     fun setClockShouldBeCentered(shouldBeCentered: Boolean) {
466         repository.setClockShouldBeCentered(shouldBeCentered)
467     }
468 
469     fun setLastRootViewTapPosition(point: Point?) {
470         repository.lastRootViewTapPosition.value = point
471     }
472 
473     fun setAmbientIndicationVisible(isVisible: Boolean) {
474         repository.ambientIndicationVisible.value = isVisible
475     }
476 
477     fun keyguardDoneAnimationsFinished() {
478         repository.keyguardDoneAnimationsFinished()
479     }
480 
481     fun setTopClippingBounds(top: Int?) {
482         repository.topClippingBounds.value = top
483     }
484 
485     fun setDreaming(isDreaming: Boolean) {
486         repository.setDreaming(isDreaming)
487     }
488 
489     /** Temporary shim, until [KeyguardWmStateRefactor] is enabled */
490     fun showKeyguard() {
491         fromGoneTransitionInteractor.get().showKeyguard()
492     }
493 
494     /** Temporary shim, until [KeyguardWmStateRefactor] is enabled */
495     fun dismissKeyguard() {
496         fromLockscreenTransitionInteractor.get().dismissKeyguard()
497     }
498 
499     companion object {
500         private const val TAG = "KeyguardInteractor"
501     }
502 }
503