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 androidx.annotation.VisibleForTesting
23 import com.android.systemui.common.shared.model.NotificationContainerBounds
24 import com.android.systemui.dagger.SysUISingleton
25 import com.android.systemui.dagger.qualifiers.Application
26 import com.android.systemui.dump.DumpManager
27 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
28 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
29 import com.android.systemui.keyguard.shared.model.Edge
30 import com.android.systemui.keyguard.shared.model.KeyguardState
31 import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
32 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
33 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
34 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
35 import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
36 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
37 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
38 import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
39 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
40 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE
41 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
42 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
43 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel
44 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
45 import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel
46 import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
47 import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel
48 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
49 import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel
50 import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel
51 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
52 import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
53 import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel
54 import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel
55 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel
56 import com.android.systemui.keyguard.ui.viewmodel.GoneToLockscreenTransitionViewModel
57 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel
58 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel
59 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGoneTransitionViewModel
60 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
61 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTransitionViewModel
62 import com.android.systemui.keyguard.ui.viewmodel.OccludedToAodTransitionViewModel
63 import com.android.systemui.keyguard.ui.viewmodel.OccludedToGoneTransitionViewModel
64 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
65 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
66 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel
67 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
68 import com.android.systemui.scene.shared.flag.SceneContainerFlag
69 import com.android.systemui.scene.shared.model.Scenes
70 import com.android.systemui.shade.domain.interactor.ShadeInteractor
71 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
72 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
73 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
74 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
75 import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
76 import com.android.systemui.util.kotlin.FlowDumperImpl
77 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
78 import javax.inject.Inject
79 import kotlinx.coroutines.CoroutineScope
80 import kotlinx.coroutines.ExperimentalCoroutinesApi
81 import kotlinx.coroutines.currentCoroutineContext
82 import kotlinx.coroutines.flow.Flow
83 import kotlinx.coroutines.flow.SharingStarted
84 import kotlinx.coroutines.flow.StateFlow
85 import kotlinx.coroutines.flow.combine
86 import kotlinx.coroutines.flow.combineTransform
87 import kotlinx.coroutines.flow.distinctUntilChanged
88 import kotlinx.coroutines.flow.emptyFlow
89 import kotlinx.coroutines.flow.first
90 import kotlinx.coroutines.flow.flatMapLatest
91 import kotlinx.coroutines.flow.flow
92 import kotlinx.coroutines.flow.map
93 import kotlinx.coroutines.flow.merge
94 import kotlinx.coroutines.flow.onStart
95 import kotlinx.coroutines.flow.stateIn
96 import kotlinx.coroutines.flow.transformWhile
97 import kotlinx.coroutines.isActive
98 
99 /** View-model for the shared notification container, used by both the shade and keyguard spaces */
100 @SysUISingleton
101 class SharedNotificationContainerViewModel
102 @Inject
103 constructor(
104     private val interactor: SharedNotificationContainerInteractor,
105     dumpManager: DumpManager,
106     @Application applicationScope: CoroutineScope,
107     private val keyguardInteractor: KeyguardInteractor,
108     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
109     private val shadeInteractor: ShadeInteractor,
110     private val notificationStackAppearanceInteractor: NotificationStackAppearanceInteractor,
111     private val alternateBouncerToGoneTransitionViewModel:
112         AlternateBouncerToGoneTransitionViewModel,
113     private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel,
114     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
115     private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
116     private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
117     private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
118     private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
119     private val glanceableHubToLockscreenTransitionViewModel:
120         GlanceableHubToLockscreenTransitionViewModel,
121     private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
122     private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel,
123     private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel,
124     private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel,
125     private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
126     private val lockscreenToGlanceableHubTransitionViewModel:
127         LockscreenToGlanceableHubTransitionViewModel,
128     private val lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel,
129     private val lockscreenToPrimaryBouncerTransitionViewModel:
130         LockscreenToPrimaryBouncerTransitionViewModel,
131     private val lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
132     private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
133     private val occludedToGoneTransitionViewModel: OccludedToGoneTransitionViewModel,
134     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
135     private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
136     private val primaryBouncerToLockscreenTransitionViewModel:
137         PrimaryBouncerToLockscreenTransitionViewModel,
138     private val aodBurnInViewModel: AodBurnInViewModel,
139     unfoldTransitionInteractor: UnfoldTransitionInteractor,
140 ) : FlowDumperImpl(dumpManager) {
141     private val statesForConstrainedNotifications: Set<KeyguardState> =
142         setOf(AOD, LOCKSCREEN, DOZING, ALTERNATE_BOUNCER, PRIMARY_BOUNCER)
143     private val statesForHiddenKeyguard: Set<KeyguardState> = setOf(GONE, OCCLUDED)
144 
145     /**
146      * Is either shade/qs expanded? This intentionally does not use the [ShadeInteractor] version,
147      * as the legacy implementation has extra logic that produces incorrect results.
148      */
149     private val isAnyExpanded =
150         combine(
151                 shadeInteractor.shadeExpansion.map { it > 0f },
152                 shadeInteractor.qsExpansion.map { it > 0f },
153             ) { shadeExpansion, qsExpansion ->
154                 shadeExpansion || qsExpansion
155             }
156             .stateIn(
157                 scope = applicationScope,
158                 started = SharingStarted.Eagerly,
159                 initialValue = false,
160             )
161 
162     /**
163      * Shade locked is a legacy concept, but necessary to mimic current functionality. Listen for
164      * both SHADE_LOCKED and shade/qs expansion in order to determine lock state, as one can arrive
165      * before the other.
166      */
167     private val isShadeLocked: Flow<Boolean> =
168         combine(
169                 keyguardInteractor.statusBarState.map { it == SHADE_LOCKED },
170                 isAnyExpanded,
171             ) { isShadeLocked, isAnyExpanded ->
172                 isShadeLocked && isAnyExpanded
173             }
174             .stateIn(
175                 scope = applicationScope,
176                 started = SharingStarted.Eagerly,
177                 initialValue = false,
178             )
179             .dumpWhileCollecting("isShadeLocked")
180 
181     @VisibleForTesting
182     val paddingTopDimen: Flow<Int> =
183         interactor.configurationBasedDimensions
184             .map {
185                 when {
186                     !it.useSplitShade -> 0
187                     it.useLargeScreenHeader -> it.marginTopLargeScreen
188                     else -> it.marginTop
189                 }
190             }
191             .distinctUntilChanged()
192             .dumpWhileCollecting("paddingTopDimen")
193 
194     val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
195         interactor.configurationBasedDimensions
196             .map {
197                 val marginTop =
198                     when {
199                         // y position of the NSSL in the window needs to be 0 under scene container
200                         SceneContainerFlag.isEnabled -> 0
201                         it.useLargeScreenHeader -> it.marginTopLargeScreen
202                         else -> it.marginTop
203                     }
204                 ConfigurationBasedDimensions(
205                     marginStart = if (it.useSplitShade) 0 else it.marginHorizontal,
206                     marginEnd = it.marginHorizontal,
207                     marginBottom = it.marginBottom,
208                     marginTop = marginTop,
209                     useSplitShade = it.useSplitShade,
210                 )
211             }
212             .distinctUntilChanged()
213             .dumpWhileCollecting("configurationBasedDimensions")
214 
215     /** If the user is visually on one of the unoccluded lockscreen states. */
216     val isOnLockscreen: Flow<Boolean> =
217         combine(
218                 keyguardTransitionInteractor.finishedKeyguardState.map {
219                     statesForConstrainedNotifications.contains(it)
220                 },
221                 keyguardTransitionInteractor.transitionValue(LOCKSCREEN).map { it > 0f },
222             ) { constrainedNotificationState, transitioningToOrFromLockscreen ->
223                 constrainedNotificationState || transitioningToOrFromLockscreen
224             }
225             .stateIn(
226                 scope = applicationScope,
227                 started = SharingStarted.Eagerly,
228                 initialValue = false
229             )
230             .dumpValue("isOnLockscreen")
231 
232     /** Are we purely on the keyguard without the shade/qs? */
233     val isOnLockscreenWithoutShade: Flow<Boolean> =
234         combine(
235                 isOnLockscreen,
236                 isAnyExpanded,
237             ) { isKeyguard, isAnyExpanded ->
238                 isKeyguard && !isAnyExpanded
239             }
240             .stateIn(
241                 scope = applicationScope,
242                 started = SharingStarted.Eagerly,
243                 initialValue = false,
244             )
245             .dumpValue("isOnLockscreenWithoutShade")
246 
247     /** If the user is visually on the glanceable hub or transitioning to/from it */
248     private val isOnGlanceableHub: Flow<Boolean> =
249         combine(
250                 keyguardTransitionInteractor.finishedKeyguardState.map { state ->
251                     state == GLANCEABLE_HUB
252                 },
253                 anyOf(
254                     keyguardTransitionInteractor.isInTransition(
255                         edge = Edge.create(to = Scenes.Communal),
256                         edgeWithoutSceneContainer = Edge.create(to = GLANCEABLE_HUB)
257                     ),
258                     keyguardTransitionInteractor.isInTransition(
259                         edge = Edge.create(from = Scenes.Communal),
260                         edgeWithoutSceneContainer = Edge.create(from = GLANCEABLE_HUB)
261                     ),
262                 ),
263             ) { isOnGlanceableHub, transitioningToOrFromHub ->
264                 isOnGlanceableHub || transitioningToOrFromHub
265             }
266             .distinctUntilChanged()
267             .dumpWhileCollecting("isOnGlanceableHub")
268 
269     /** Are we purely on the glanceable hub without the shade/qs? */
270     val isOnGlanceableHubWithoutShade: Flow<Boolean> =
271         combine(
272                 isOnGlanceableHub,
273                 isAnyExpanded,
274             ) { isGlanceableHub, isAnyExpanded ->
275                 isGlanceableHub && !isAnyExpanded
276             }
277             .stateIn(
278                 scope = applicationScope,
279                 started = SharingStarted.Eagerly,
280                 initialValue = false,
281             )
282             .dumpValue("isOnGlanceableHubWithoutShade")
283 
284     /** Are we on the dream without the shade/qs? */
285     private val isDreamingWithoutShade: Flow<Boolean> =
286         combine(
287                 keyguardTransitionInteractor.isFinishedInState(DREAMING),
288                 isAnyExpanded,
289             ) { isDreaming, isAnyExpanded ->
290                 isDreaming && !isAnyExpanded
291             }
292             .stateIn(
293                 scope = applicationScope,
294                 started = SharingStarted.Eagerly,
295                 initialValue = false,
296             )
297             .dumpValue("isDreamingWithoutShade")
298 
299     /**
300      * Fade in if the user swipes the shade back up, not if collapsed by going to AOD. This is
301      * needed due to the lack of a SHADE state with existing keyguard transitions.
302      */
303     private fun awaitCollapse(): Flow<Boolean> {
304         var aodTransitionIsComplete = true
305         return combine(
306                 isOnLockscreenWithoutShade,
307                 keyguardTransitionInteractor.isInTransition(
308                     edge = Edge.create(from = LOCKSCREEN, to = AOD)
309                 ),
310                 ::Pair
311             )
312             .transformWhile { (isOnLockscreenWithoutShade, aodTransitionIsRunning) ->
313                 // Wait until the AOD transition is complete before terminating
314                 if (!aodTransitionIsComplete && !aodTransitionIsRunning) {
315                     aodTransitionIsComplete = true
316                     emit(false) // do not fade in
317                     false
318                 } else if (aodTransitionIsRunning) {
319                     aodTransitionIsComplete = false
320                     true
321                 } else if (isOnLockscreenWithoutShade) {
322                     // Shade is closed, fade in and terminate
323                     emit(true)
324                     false
325                 } else {
326                     true
327                 }
328             }
329     }
330 
331     /** Fade in only for use after the shade collapses */
332     val shadeCollapseFadeIn: Flow<Boolean> =
333         flow {
334                 while (currentCoroutineContext().isActive) {
335                     // Ensure shade is collapsed
336                     isShadeLocked.first { !it }
337                     emit(false)
338                     // Wait for shade to be fully expanded
339                     isShadeLocked.first { it }
340                     // ... and then for it to be collapsed OR a transition to AOD begins.
341                     // If AOD, do not fade in (a fade out occurs instead).
342                     awaitCollapse().collect { doFadeIn ->
343                         if (doFadeIn) {
344                             emit(true)
345                         }
346                     }
347                 }
348             }
349             .stateIn(
350                 scope = applicationScope,
351                 started = SharingStarted.WhileSubscribed(),
352                 initialValue = false,
353             )
354             .dumpValue("shadeCollapseFadeIn")
355 
356     /**
357      * The container occupies the entire screen, and must be positioned relative to other elements.
358      *
359      * On keyguard, this generally fits below the clock and above the lock icon, or in split shade,
360      * the top of the screen to the lock icon.
361      *
362      * When the shade is expanding, the position is controlled by... the shade.
363      */
364     val bounds: StateFlow<NotificationContainerBounds> by lazy {
365         SceneContainerFlag.assertInLegacyMode()
366         combine(
367                 isOnLockscreenWithoutShade,
368                 keyguardInteractor.notificationContainerBounds,
369                 paddingTopDimen,
370                 interactor.topPosition
371                     .sampleCombine(
372                         keyguardTransitionInteractor.isInTransitionToAnyState,
373                         shadeInteractor.qsExpansion,
374                     )
375                     .onStart { emit(Triple(0f, false, 0f)) }
376             ) { onLockscreen, bounds, paddingTop, (top, isInTransitionToAnyState, qsExpansion) ->
377                 if (onLockscreen) {
378                     bounds.copy(top = bounds.top - paddingTop)
379                 } else {
380                     // When QS expansion > 0, it should directly set the top padding so do not
381                     // animate it
382                     val animate = qsExpansion == 0f && !isInTransitionToAnyState
383                     bounds.copy(
384                         top = top,
385                         isAnimated = animate,
386                     )
387                 }
388             }
389             .stateIn(
390                 scope = applicationScope,
391                 started = SharingStarted.Lazily,
392                 initialValue = NotificationContainerBounds(),
393             )
394             .dumpValue("bounds")
395     }
396 
397     /**
398      * Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out
399      * notifications unless in splitshade.
400      */
401     private val alphaForShadeAndQsExpansion: Flow<Float> =
402         interactor.configurationBasedDimensions
403             .flatMapLatest { configurationBasedDimensions ->
404                 combineTransform(
405                     shadeInteractor.shadeExpansion,
406                     shadeInteractor.qsExpansion,
407                 ) { shadeExpansion, qsExpansion ->
408                     if (shadeExpansion > 0f || qsExpansion > 0f) {
409                         if (configurationBasedDimensions.useSplitShade) {
410                             emit(1f)
411                         } else if (qsExpansion == 1f) {
412                             // Ensure HUNs will be visible in QS shade (at least while unlocked)
413                             emit(1f)
414                         } else {
415                             // Fade as QS shade expands
416                             emit(1f - qsExpansion)
417                         }
418                     }
419                 }
420             }
421             .onStart { emit(1f) }
422             .dumpWhileCollecting("alphaForShadeAndQsExpansion")
423 
424     private fun toFlowArray(
425         states: Set<KeyguardState>,
426         flow: (KeyguardState) -> Flow<Boolean>
427     ): Array<Flow<Boolean>> {
428         return states.map { flow(it) }.toTypedArray()
429     }
430 
431     private val isTransitioningToHiddenKeyguard: Flow<Boolean> =
432         flow {
433                 while (currentCoroutineContext().isActive) {
434                     emit(false)
435                     // Ensure states are inactive to start
436                     allOf(
437                             *toFlowArray(statesForHiddenKeyguard) { state ->
438                                 keyguardTransitionInteractor.transitionValue(state).map { it == 0f }
439                             }
440                         )
441                         .first { it }
442                     // Wait for a qualifying transition to begin
443                     anyOf(
444                             *toFlowArray(statesForHiddenKeyguard) { state ->
445                                 keyguardTransitionInteractor
446                                     .transition(Edge.create(to = state))
447                                     .map { it.value > 0f && it.transitionState == RUNNING }
448                                     .onStart { emit(false) }
449                             }
450                         )
451                         .first { it }
452                     emit(true)
453                     // Now await the signal that SHADE state has been reached or the transition was
454                     // reversed. Until SHADE state has been replaced it is the only source of when
455                     // it is considered safe to reset alpha to 1f for HUNs.
456                     combine(
457                             keyguardInteractor.statusBarState,
458                             allOf(
459                                 *toFlowArray(statesForHiddenKeyguard) { state ->
460                                     keyguardTransitionInteractor.transitionValue(state).map {
461                                         it == 0f
462                                     }
463                                 }
464                             )
465                         ) { statusBarState, stateIsReversed ->
466                             statusBarState == SHADE || stateIsReversed
467                         }
468                         .first { it }
469                 }
470             }
471             .dumpWhileCollecting("isTransitioningToHiddenKeyguard")
472 
473     fun keyguardAlpha(viewState: ViewStateAccessor): Flow<Float> {
474         // All transition view models are mututally exclusive, and safe to merge
475         val alphaTransitions =
476             merge(
477                 keyguardInteractor.dismissAlpha.dumpWhileCollecting(
478                     "keyguardInteractor.dismissAlpha"
479                 ),
480                 alternateBouncerToGoneTransitionViewModel.notificationAlpha(viewState),
481                 aodToGoneTransitionViewModel.notificationAlpha(viewState),
482                 aodToLockscreenTransitionViewModel.notificationAlpha,
483                 aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
484                 dozingToLockscreenTransitionViewModel.lockscreenAlpha,
485                 dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
486                 dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
487                 goneToAodTransitionViewModel.notificationAlpha,
488                 goneToDreamingTransitionViewModel.lockscreenAlpha,
489                 goneToDozingTransitionViewModel.notificationAlpha,
490                 goneToLockscreenTransitionViewModel.lockscreenAlpha,
491                 lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
492                 lockscreenToGoneTransitionViewModel.notificationAlpha(viewState),
493                 lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
494                 lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
495                 occludedToAodTransitionViewModel.lockscreenAlpha,
496                 occludedToGoneTransitionViewModel.notificationAlpha(viewState),
497                 occludedToLockscreenTransitionViewModel.lockscreenAlpha,
498                 primaryBouncerToGoneTransitionViewModel.notificationAlpha,
499                 primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha,
500             )
501 
502         return merge(
503                 alphaTransitions,
504                 // These remaining cases handle alpha changes within an existing state, such as
505                 // shade expansion or swipe to dismiss
506                 combineTransform(
507                     isTransitioningToHiddenKeyguard,
508                     alphaForShadeAndQsExpansion,
509                 ) { isTransitioningToHiddenKeyguard, alphaForShadeAndQsExpansion ->
510                     if (!isTransitioningToHiddenKeyguard) {
511                         emit(alphaForShadeAndQsExpansion)
512                     }
513                 },
514             )
515             .distinctUntilChanged()
516             .dumpWhileCollecting("keyguardAlpha")
517     }
518 
519     /**
520      * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB or
521      * DREAMING<->GLANCEABLE_HUB transition or idle on the hub.
522      *
523      * Must return 1.0f when not controlling the alpha since notifications does a min of all the
524      * alpha sources.
525      */
526     val glanceableHubAlpha: Flow<Float> =
527         combineTransform(
528                 isOnGlanceableHubWithoutShade,
529                 isOnLockscreen,
530                 isDreamingWithoutShade,
531                 merge(
532                         lockscreenToGlanceableHubTransitionViewModel.notificationAlpha,
533                         glanceableHubToLockscreenTransitionViewModel.notificationAlpha,
534                     )
535                     // Manually emit on start because [notificationAlpha] only starts emitting
536                     // when transitions start.
537                     .onStart { emit(1f) }
538             ) { isOnGlanceableHubWithoutShade, isOnLockscreen, isDreamingWithoutShade, alpha,
539                 ->
540                 if ((isOnGlanceableHubWithoutShade || isDreamingWithoutShade) && !isOnLockscreen) {
541                     // Notifications should not be visible on the glanceable hub.
542                     // TODO(b/321075734): implement a way to actually set the notifications to
543                     // gone while on the hub instead of just adjusting alpha
544                     emit(0f)
545                 } else if (isOnGlanceableHubWithoutShade) {
546                     // We are transitioning between hub and lockscreen, so set the alpha for the
547                     // transition animation.
548                     emit(alpha)
549                 } else {
550                     // Not on the hub and no transitions running, return full visibility so we
551                     // don't block the notifications from showing.
552                     emit(1f)
553                 }
554             }
555             .distinctUntilChanged()
556             .dumpWhileCollecting("glanceableHubAlpha")
557 
558     /**
559      * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be
560      * translated as the keyguard fades out.
561      */
562     fun translationY(params: BurnInParameters): Flow<Float> {
563         // with SceneContainer, x translation is handled by views, y is handled by compose
564         SceneContainerFlag.assertInLegacyMode()
565         return combine(
566                 aodBurnInViewModel
567                     .movement(params)
568                     .map { it.translationY.toFloat() }
569                     .onStart { emit(0f) },
570                 isOnLockscreenWithoutShade,
571                 merge(
572                     keyguardInteractor.keyguardTranslationY,
573                     occludedToLockscreenTransitionViewModel.lockscreenTranslationY,
574                 )
575             ) { burnInY, isOnLockscreenWithoutShade, translationY ->
576                 if (isOnLockscreenWithoutShade) {
577                     burnInY + translationY
578                 } else {
579                     0f
580                 }
581             }
582             .dumpWhileCollecting("translationY")
583     }
584 
585     /** Horizontal translation to apply to the container. */
586     val translationX: Flow<Float> =
587         merge(
588                 // The container may need to be translated along the X axis as the keyguard fades
589                 // out, such as when swiping open the glanceable hub from the lockscreen.
590                 lockscreenToGlanceableHubTransitionViewModel.notificationTranslationX,
591                 glanceableHubToLockscreenTransitionViewModel.notificationTranslationX,
592                 if (SceneContainerFlag.isEnabled) {
593                     // The container may need to be translated along the X axis as the unfolded
594                     // foldable is folded slightly.
595                     unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false)
596                 } else {
597                     emptyFlow()
598                 }
599             )
600             .dumpWhileCollecting("translationX")
601 
602     private val availableHeight: Flow<Float> =
603         if (SceneContainerFlag.isEnabled) {
604                 notificationStackAppearanceInteractor.constrainedAvailableSpace.map { it.toFloat() }
605             } else {
606                 bounds.map { it.bottom - it.top }
607             }
608             .distinctUntilChanged()
609             .dumpWhileCollecting("availableHeight")
610 
611     /**
612      * When on keyguard, there is limited space to display notifications so calculate how many could
613      * be shown. Otherwise, there is no limit since the vertical space will be scrollable.
614      *
615      * When expanding or when the user is interacting with the shade, keep the count stable; do not
616      * emit a value.
617      */
618     fun getMaxNotifications(calculateSpace: (Float, Boolean) -> Int): Flow<Int> {
619         val showLimitedNotifications = isOnLockscreenWithoutShade
620         val showUnlimitedNotifications =
621             combine(
622                 isOnLockscreen,
623                 keyguardInteractor.statusBarState,
624                 merge(
625                         primaryBouncerToGoneTransitionViewModel.showAllNotifications,
626                         alternateBouncerToGoneTransitionViewModel.showAllNotifications,
627                     )
628                     .onStart { emit(false) }
629             ) { isOnLockscreen, statusBarState, showAllNotifications ->
630                 statusBarState == SHADE_LOCKED || !isOnLockscreen || showAllNotifications
631             }
632 
633         return combineTransform(
634                 showLimitedNotifications,
635                 showUnlimitedNotifications,
636                 shadeInteractor.isUserInteracting,
637                 availableHeight,
638                 interactor.notificationStackChanged,
639                 interactor.useExtraShelfSpace,
640             ) { flows ->
641                 val showLimitedNotifications = flows[0] as Boolean
642                 val showUnlimitedNotifications = flows[1] as Boolean
643                 val isUserInteracting = flows[2] as Boolean
644                 val availableHeight = flows[3] as Float
645                 val useExtraShelfSpace = flows[5] as Boolean
646 
647                 if (!isUserInteracting) {
648                     if (showLimitedNotifications) {
649                         emit(calculateSpace(availableHeight, useExtraShelfSpace))
650                     } else if (showUnlimitedNotifications) {
651                         emit(-1)
652                     }
653                 }
654             }
655             .distinctUntilChanged()
656             .dumpWhileCollecting("maxNotifications")
657     }
658 
659     fun notificationStackChanged() {
660         interactor.notificationStackChanged()
661     }
662 
663     data class ConfigurationBasedDimensions(
664         val marginStart: Int,
665         val marginTop: Int,
666         val marginEnd: Int,
667         val marginBottom: Int,
668         val useSplitShade: Boolean,
669     )
670 }
671