• 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 
18 @file:OptIn(ExperimentalCoroutinesApi::class)
19 
20 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
21 
22 import com.android.compose.animation.scene.ContentKey
23 import com.android.compose.animation.scene.ObservableTransitionState
24 import com.android.compose.animation.scene.ObservableTransitionState.Idle
25 import com.android.compose.animation.scene.ObservableTransitionState.Transition
26 import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeScene
27 import com.android.compose.animation.scene.OverlayKey
28 import com.android.compose.animation.scene.SceneKey
29 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
30 import com.android.systemui.dump.DumpManager
31 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
32 import com.android.systemui.lifecycle.ExclusiveActivatable
33 import com.android.systemui.scene.domain.interactor.SceneInteractor
34 import com.android.systemui.scene.shared.flag.SceneContainerFlag
35 import com.android.systemui.scene.shared.model.Overlays
36 import com.android.systemui.scene.shared.model.Scenes
37 import com.android.systemui.shade.domain.interactor.ShadeInteractor
38 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
39 import com.android.systemui.shade.shared.model.ShadeMode
40 import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
41 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
42 import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent
43 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimClipping
44 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
45 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrollState
46 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_DELAYED_STACK_FADE_IN
47 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
48 import com.android.systemui.util.kotlin.ActivatableFlowDumper
49 import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl
50 import dagger.Lazy
51 import dagger.assisted.AssistedFactory
52 import dagger.assisted.AssistedInject
53 import kotlinx.coroutines.ExperimentalCoroutinesApi
54 import kotlinx.coroutines.flow.Flow
55 import kotlinx.coroutines.flow.MutableStateFlow
56 import kotlinx.coroutines.flow.StateFlow
57 import kotlinx.coroutines.flow.combine
58 import kotlinx.coroutines.flow.combineTransform
59 import kotlinx.coroutines.flow.distinctUntilChanged
60 import kotlinx.coroutines.flow.flatMapLatest
61 import kotlinx.coroutines.flow.flowOf
62 import kotlinx.coroutines.flow.map
63 import kotlinx.coroutines.flow.mapNotNull
64 
65 private typealias ShadeScrimShapeConsumer = (ShadeScrimShape?) -> Unit
66 
67 /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
68 class NotificationScrollViewModel
69 @AssistedInject
70 constructor(
71     dumpManager: DumpManager,
72     private val stackAppearanceInteractor: NotificationStackAppearanceInteractor,
73     shadeInteractor: ShadeInteractor,
74     shadeModeInteractor: ShadeModeInteractor,
75     bouncerInteractor: BouncerInteractor,
76     private val remoteInputInteractor: RemoteInputInteractor,
77     private val sceneInteractor: SceneInteractor,
78     // TODO(b/336364825) Remove Lazy when SceneContainerFlag is released -
79     // while the flag is off, creating this object too early results in a crash
80     keyguardInteractor: Lazy<KeyguardInteractor>,
81 ) :
82     ActivatableFlowDumper by ActivatableFlowDumperImpl(dumpManager, "NotificationScrollViewModel"),
83     ExclusiveActivatable() {
84 
85     override suspend fun onActivated(): Nothing {
86         activateFlowDumper()
87     }
88 
89     private fun expandedInScene(scene: SceneKey): Boolean {
90         return when (scene) {
91             Scenes.Lockscreen,
92             Scenes.Shade,
93             Scenes.QuickSettings -> true
94             else -> false
95         }
96     }
97 
98     private fun fullyExpandedDuringSceneChange(change: ChangeScene): Boolean {
99         // The lockscreen stack is visible during all transitions away from the lockscreen, so keep
100         // the stack expanded until those transitions finish.
101         return if (change.isTransitioning(from = Scenes.Lockscreen)) {
102             true
103         } else if (change.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen)) {
104             false
105         } else {
106             (expandedInScene(change.fromScene) && expandedInScene(change.toScene))
107         }
108     }
109 
110     private fun expandFractionDuringSceneChange(
111         change: ChangeScene,
112         shadeExpansion: Float,
113         qsExpansion: Float,
114     ): Float {
115         return if (fullyExpandedDuringSceneChange(change)) {
116             1f
117         } else if (
118             change.isTransitioningBetween(Scenes.Gone, Scenes.Shade) ||
119                 change.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen)
120         ) {
121             shadeExpansion
122         } else if (change.isTransitioningBetween(Scenes.Gone, Scenes.QuickSettings)) {
123             // during QS expansion, increase fraction at same rate as scrim alpha,
124             // but start when scrim alpha is at EXPANSION_FOR_DELAYED_STACK_FADE_IN.
125             (qsExpansion / EXPANSION_FOR_MAX_SCRIM_ALPHA - EXPANSION_FOR_DELAYED_STACK_FADE_IN)
126                 .coerceIn(0f, 1f)
127         } else {
128             // TODO(b/356596436): If notification shade overlay is open, we'll reach this point and
129             //  the expansion fraction in that case should be `shadeExpansion`.
130             0f
131         }
132     }
133 
134     private fun expandFractionDuringOverlayTransition(
135         transition: Transition,
136         currentScene: SceneKey,
137         currentOverlays: Set<OverlayKey>,
138         shadeExpansion: Float,
139     ): Float {
140         return if (currentScene == Scenes.Lockscreen) {
141             1f
142         } else if (transition.isTransitioningFromOrTo(Overlays.NotificationsShade)) {
143             shadeExpansion
144         } else if (Overlays.NotificationsShade in currentOverlays) {
145             1f
146         } else {
147             0f
148         }
149     }
150 
151     /** Are notification stack height updates suppressed? */
152     val suppressHeightUpdates: Flow<Boolean> =
153         sceneInteractor.transitionState.map { transition: ObservableTransitionState ->
154             transition is Transition &&
155                 transition.fromContent == Scenes.Lockscreen &&
156                 (transition.toContent == Overlays.Bouncer || transition.toContent == Scenes.Gone)
157         }
158 
159     /**
160      * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning
161      * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while
162      * transitioning from Shade to QuickSettings scenes.
163      */
164     val expandFraction: Flow<Float> =
165         combine(
166                 shadeInteractor.shadeExpansion,
167                 shadeInteractor.qsExpansion,
168                 shadeModeInteractor.shadeMode,
169                 sceneInteractor.transitionState,
170                 sceneInteractor.currentOverlays,
171             ) { shadeExpansion, qsExpansion, _, transitionState, currentOverlays ->
172                 when (transitionState) {
173                     is Idle ->
174                         if (
175                             expandedInScene(transitionState.currentScene) ||
176                                 Overlays.NotificationsShade in currentOverlays
177                         ) {
178                             1f
179                         } else {
180                             0f
181                         }
182                     is ChangeScene ->
183                         expandFractionDuringSceneChange(
184                             change = transitionState,
185                             shadeExpansion = shadeExpansion,
186                             qsExpansion = qsExpansion,
187                         )
188                     is Transition.ShowOrHideOverlay ->
189                         expandFractionDuringOverlayTransition(
190                             transition = transitionState,
191                             currentScene = transitionState.currentScene,
192                             currentOverlays = currentOverlays,
193                             shadeExpansion = shadeExpansion,
194                         )
195                     is Transition.ReplaceOverlay ->
196                         expandFractionDuringOverlayTransition(
197                             transition = transitionState,
198                             currentScene = transitionState.currentScene,
199                             currentOverlays = currentOverlays,
200                             shadeExpansion = shadeExpansion,
201                         )
202                 }
203             }
204             .distinctUntilChanged()
205             .dumpWhileCollecting("expandFraction")
206 
207     val qsExpandFraction: Flow<Float> =
208         shadeInteractor.qsExpansion.dumpWhileCollecting("qsExpandFraction")
209 
210     val isOccluded: Flow<Boolean> =
211         bouncerInteractor.bouncerExpansion
212             .map { it == 1f }
213             .distinctUntilChanged()
214             .dumpWhileCollecting("isOccluded")
215 
216     /** Blur radius to be applied to Notifications. */
217     fun blurRadius(maxBlurRadius: Flow<Int>) =
218         combine(blurFraction, maxBlurRadius) { fraction, maxRadius -> fraction * maxRadius }
219 
220     /**
221      * Scale of the blur effect that should be applied to Notifications.
222      *
223      * 0 -> don't blur (default, removes all blur render effects) 1 -> do the full blur (apply a
224      * render effect with the max blur radius)
225      */
226     private val blurFraction: Flow<Float> =
227         if (SceneContainerFlag.isEnabled) {
228             shadeModeInteractor.shadeMode.flatMapLatest { shadeMode ->
229                 when (shadeMode) {
230                     ShadeMode.Dual ->
231                         combineTransform(
232                             shadeInteractor.shadeExpansion,
233                             shadeInteractor.qsExpansion,
234                         ) { notificationShadeExpansion, qsExpansion ->
235                             if (notificationShadeExpansion == 0f) {
236                                 // Blur out notifications as the QS overlay panel expands
237                                 emit(qsExpansion)
238                             }
239                         }
240                     else -> flowOf(0f)
241                 }
242             }
243         } else {
244             flowOf(0f)
245         }
246 
247     /** Whether we should close any open notification guts. */
248     val shouldCloseGuts: Flow<Boolean> = stackAppearanceInteractor.shouldCloseGuts
249 
250     /** Whether the Notification Stack is visibly on the lockscreen scene. */
251     val isShowingStackOnLockscreen: Flow<Boolean> =
252         sceneInteractor.transitionState
253             .mapNotNull { state ->
254                 state.isIdle(Scenes.Lockscreen) ||
255                     state.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade)
256             }
257             .distinctUntilChanged()
258 
259     /** The alpha of the Notification Stack for lockscreen fade-in */
260     val alphaForLockscreenFadeIn = stackAppearanceInteractor.alphaForLockscreenFadeIn
261 
262     private operator fun SceneKey.contains(scene: SceneKey) =
263         sceneInteractor.isSceneInFamily(scene, this)
264 
265     private val qsAllowsClipping: Flow<Boolean> =
266         combine(shadeModeInteractor.shadeMode, shadeInteractor.qsExpansion) { shadeMode, qsExpansion
267                 ->
268                 when (shadeMode) {
269                     is ShadeMode.Dual,
270                     is ShadeMode.Split -> true
271                     is ShadeMode.Single -> qsExpansion < 0.5f
272                 }
273             }
274             .distinctUntilChanged()
275 
276     /** The bounds of the notification stack in the current scene. */
277     private val shadeScrimClipping: Flow<ShadeScrimClipping?> =
278         combine(
279                 qsAllowsClipping,
280                 stackAppearanceInteractor.notificationShadeScrimBounds,
281                 stackAppearanceInteractor.shadeScrimRounding,
282             ) { qsAllowsClipping, bounds, rounding ->
283                 bounds?.takeIf { qsAllowsClipping }?.let { ShadeScrimClipping(it, rounding) }
284             }
285             .distinctUntilChanged()
286             .dumpWhileCollecting("stackClipping")
287 
288     fun notificationScrimShape(
289         cornerRadius: Flow<Int>,
290         viewLeftOffset: Flow<Int>,
291     ): Flow<ShadeScrimShape?> =
292         combine(shadeScrimClipping, cornerRadius, viewLeftOffset) { clipping, radius, leftOffset ->
293                 if (clipping == null) return@combine null
294                 ShadeScrimShape(
295                     bounds = clipping.bounds.minus(leftOffset = leftOffset),
296                     topRadius = radius.takeIf { clipping.rounding.isTopRounded } ?: 0,
297                     bottomRadius = radius.takeIf { clipping.rounding.isBottomRounded } ?: 0,
298                 )
299             }
300             .dumpWhileCollecting("shadeScrimShape")
301 
302     /**
303      * Sets a consumer to be notified when the QuickSettings Overlay panel changes size or position.
304      */
305     fun setQsScrimShapeConsumer(consumer: ShadeScrimShapeConsumer?) {
306         stackAppearanceInteractor.setQsPanelShapeConsumer(consumer)
307     }
308 
309     /**
310      * Max alpha to apply directly to the view based on the compose placeholder.
311      *
312      * TODO(b/338590620): Migrate alphas from [SharedNotificationContainerViewModel] into this flow
313      */
314     val maxAlpha: Flow<Float> =
315         stackAppearanceInteractor.alphaForBrightnessMirror.dumpValue("maxAlpha")
316 
317     /** Scroll state of the notification shade. */
318     val shadeScrollState: Flow<ShadeScrollState> = stackAppearanceInteractor.shadeScrollState
319 
320     /** Receives the amount (px) that the stack should scroll due to internal expansion. */
321     val syntheticScrollConsumer: (Float) -> Unit = stackAppearanceInteractor::setSyntheticScroll
322 
323     /** Receives an event to scroll the stack up or down. */
324     val accessibilityScrollEventConsumer: (AccessibilityScrollEvent) -> Unit =
325         stackAppearanceInteractor::sendAccessibilityScrollEvent
326 
327     /**
328      * Receives whether the current touch gesture is overscroll as it has already been consumed by
329      * the stack.
330      */
331     val currentGestureOverscrollConsumer: (Boolean) -> Unit =
332         stackAppearanceInteractor::setCurrentGestureOverscroll
333 
334     /** Receives whether the current touch gesture is inside any open guts. */
335     val currentGestureInGutsConsumer: (Boolean) -> Unit =
336         stackAppearanceInteractor::setCurrentGestureInGuts
337 
338     /** Receives the bottom bound of the currently focused remote input notification row. */
339     val remoteInputRowBottomBoundConsumer: (Float?) -> Unit =
340         remoteInputInteractor::setRemoteInputRowBottomBound
341 
342     /** Whether the notification stack is scrollable or not. */
343     val isScrollable: Flow<Boolean> =
344         combine(sceneInteractor.currentScene, sceneInteractor.currentOverlays) {
345                 currentScene,
346                 currentOverlays ->
347                 currentScene.showsNotifications() || currentOverlays.any { it.showsNotifications() }
348             }
349             .dumpWhileCollecting("isScrollable")
350 
351     /** Whether the notification stack is displayed in doze mode. */
352     val isDozing: Flow<Boolean> by lazy {
353         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
354             flowOf(false)
355         } else {
356             keyguardInteractor.get().isDozing.dumpWhileCollecting("isDozing")
357         }
358     }
359 
360     /** Whether the notification stack is displayed in pulsing mode. */
361     val isPulsing: Flow<Boolean> by lazy {
362         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
363             flowOf(false)
364         } else {
365             keyguardInteractor.get().isPulsing.dumpWhileCollecting("isPulsing")
366         }
367     }
368 
369     val shouldAnimatePulse: StateFlow<Boolean> by lazy {
370         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
371             MutableStateFlow(false)
372         } else {
373             keyguardInteractor.get().isAodAvailable
374         }
375     }
376 
377     private fun ContentKey.showsNotifications(): Boolean {
378         return when (this) {
379             Overlays.NotificationsShade,
380             Scenes.Lockscreen,
381             Scenes.Shade -> true
382             else -> false
383         }
384     }
385 
386     @AssistedFactory
387     interface Factory {
388         fun create(): NotificationScrollViewModel
389     }
390 }
391