• 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 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
19 
20 import android.content.Context
21 import androidx.annotation.VisibleForTesting
22 import com.android.app.tracing.coroutines.flow.flowName
23 import com.android.systemui.Flags.glanceableHubV2
24 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
25 import com.android.systemui.common.shared.model.NotificationContainerBounds
26 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
27 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
28 import com.android.systemui.communal.shared.model.CommunalScenes
29 import com.android.systemui.dagger.SysUISingleton
30 import com.android.systemui.dagger.qualifiers.Application
31 import com.android.systemui.dump.DumpManager
32 import com.android.systemui.kairos.awaitClose
33 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
34 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
35 import com.android.systemui.keyguard.shared.model.Edge
36 import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
37 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
38 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
39 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
40 import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
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.KeyguardState.OCCLUDED
44 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
45 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE
46 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
47 import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
48 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel
49 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel
50 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
51 import com.android.systemui.keyguard.ui.viewmodel.AodToGlanceableHubTransitionViewModel
52 import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel
53 import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
54 import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel
55 import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel
56 import com.android.systemui.keyguard.ui.viewmodel.DozingToGlanceableHubTransitionViewModel
57 import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel
58 import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel
59 import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel
60 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
61 import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToAodTransitionViewModel
62 import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
63 import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel
64 import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel
65 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel
66 import com.android.systemui.keyguard.ui.viewmodel.GoneToLockscreenTransitionViewModel
67 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel
68 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel
69 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGoneTransitionViewModel
70 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
71 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTransitionViewModel
72 import com.android.systemui.keyguard.ui.viewmodel.OccludedToAodTransitionViewModel
73 import com.android.systemui.keyguard.ui.viewmodel.OccludedToGoneTransitionViewModel
74 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
75 import com.android.systemui.keyguard.ui.viewmodel.OffToLockscreenTransitionViewModel
76 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
77 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel
78 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
79 import com.android.systemui.res.R
80 import com.android.systemui.scene.shared.flag.SceneContainerFlag
81 import com.android.systemui.scene.shared.model.Overlays
82 import com.android.systemui.scene.shared.model.Scenes
83 import com.android.systemui.shade.LargeScreenHeaderHelper
84 import com.android.systemui.shade.ShadeDisplayAware
85 import com.android.systemui.shade.domain.interactor.ShadeInteractor
86 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
87 import com.android.systemui.shade.shared.model.ShadeMode.Dual
88 import com.android.systemui.shade.shared.model.ShadeMode.Single
89 import com.android.systemui.shade.shared.model.ShadeMode.Split
90 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
91 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
92 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
93 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
94 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
95 import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
96 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
97 import com.android.systemui.util.kotlin.FlowDumperImpl
98 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
99 import com.android.systemui.util.kotlin.sample
100 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
101 import dagger.Lazy
102 import javax.inject.Inject
103 import kotlinx.coroutines.CoroutineScope
104 import kotlinx.coroutines.ExperimentalCoroutinesApi
105 import kotlinx.coroutines.currentCoroutineContext
106 import kotlinx.coroutines.flow.Flow
107 import kotlinx.coroutines.flow.SharingStarted
108 import kotlinx.coroutines.flow.StateFlow
109 import kotlinx.coroutines.flow.combine
110 import kotlinx.coroutines.flow.combineTransform
111 import kotlinx.coroutines.flow.distinctUntilChanged
112 import kotlinx.coroutines.flow.emptyFlow
113 import kotlinx.coroutines.flow.filterNotNull
114 import kotlinx.coroutines.flow.first
115 import kotlinx.coroutines.flow.flatMapLatest
116 import kotlinx.coroutines.flow.flow
117 import kotlinx.coroutines.flow.map
118 import kotlinx.coroutines.flow.merge
119 import kotlinx.coroutines.flow.onStart
120 import kotlinx.coroutines.flow.stateIn
121 import kotlinx.coroutines.flow.transformWhile
122 import kotlinx.coroutines.isActive
123 
124 /** View-model for the shared notification container, used by both the shade and keyguard spaces */
125 @OptIn(ExperimentalCoroutinesApi::class)
126 @SysUISingleton
127 class SharedNotificationContainerViewModel
128 @Inject
129 constructor(
130     private val interactor: SharedNotificationContainerInteractor,
131     dumpManager: DumpManager,
132     @Application applicationScope: CoroutineScope,
133     @ShadeDisplayAware private val context: Context,
134     @ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
135     private val keyguardInteractor: KeyguardInteractor,
136     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
137     private val shadeInteractor: ShadeInteractor,
138     private val bouncerInteractor: BouncerInteractor,
139     shadeModeInteractor: ShadeModeInteractor,
140     notificationStackAppearanceInteractor: NotificationStackAppearanceInteractor,
141     private val alternateBouncerToGoneTransitionViewModel:
142         AlternateBouncerToGoneTransitionViewModel,
143     private val alternateBouncerToPrimaryBouncerTransitionViewModel:
144         AlternateBouncerToPrimaryBouncerTransitionViewModel,
145     private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel,
146     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
147     private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
148     private val aodToGlanceableHubTransitionViewModel: AodToGlanceableHubTransitionViewModel,
149     private val aodToPrimaryBouncerTransitionViewModel: AodToPrimaryBouncerTransitionViewModel,
150     dozingToGlanceableHubTransitionViewModel: DozingToGlanceableHubTransitionViewModel,
151     private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
152     private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
153     private val dozingToPrimaryBouncerTransitionViewModel:
154         DozingToPrimaryBouncerTransitionViewModel,
155     private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
156     private val glanceableHubToLockscreenTransitionViewModel:
157         GlanceableHubToLockscreenTransitionViewModel,
158     private val glanceableHubToAodTransitionViewModel: GlanceableHubToAodTransitionViewModel,
159     private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
160     private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel,
161     private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel,
162     private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel,
163     private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
164     private val lockscreenToGlanceableHubTransitionViewModel:
165         LockscreenToGlanceableHubTransitionViewModel,
166     private val lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel,
167     private val lockscreenToPrimaryBouncerTransitionViewModel:
168         LockscreenToPrimaryBouncerTransitionViewModel,
169     private val lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
170     private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
171     private val occludedToGoneTransitionViewModel: OccludedToGoneTransitionViewModel,
172     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
173     private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
174     private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
175     private val primaryBouncerToLockscreenTransitionViewModel:
176         PrimaryBouncerToLockscreenTransitionViewModel,
177     private val primaryBouncerTransitions: Set<@JvmSuppressWildcards PrimaryBouncerTransition>,
178     aodBurnInViewModel: AodBurnInViewModel,
179     private val communalSceneInteractor: CommunalSceneInteractor,
180     // Lazy because it's only used in the SceneContainer + Dual Shade configuration.
181     headsUpNotificationInteractor: Lazy<HeadsUpNotificationInteractor>,
182     private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
183     unfoldTransitionInteractor: UnfoldTransitionInteractor,
184 ) : FlowDumperImpl(dumpManager) {
185 
186     /**
187      * Is either shade/qs expanded? This intentionally does not use the [ShadeInteractor] version,
188      * as the legacy implementation has extra logic that produces incorrect results.
189      */
190     private val isAnyExpanded =
191         combine(
192                 shadeInteractor.shadeExpansion.map { it > 0f },
193                 shadeInteractor.qsExpansion.map { it > 0f },
194             ) { shadeExpansion, qsExpansion ->
195                 shadeExpansion || qsExpansion
196             }
197             .flowName("isAnyExpanded")
198             .stateIn(
199                 scope = applicationScope,
200                 started = SharingStarted.Eagerly,
201                 initialValue = false,
202             )
203 
204     /**
205      * Shade locked is a legacy concept, but necessary to mimic current functionality. Listen for
206      * both SHADE_LOCKED and shade/qs expansion in order to determine lock state, as one can arrive
207      * before the other.
208      */
209     private val isShadeLocked: Flow<Boolean> =
210         combine(keyguardInteractor.statusBarState.map { it == SHADE_LOCKED }, isAnyExpanded) {
211                 isShadeLocked,
212                 isAnyExpanded ->
213                 isShadeLocked && isAnyExpanded
214             }
215             .flowName("isShadeLocked")
216             .stateIn(
217                 scope = applicationScope,
218                 started = SharingStarted.Eagerly,
219                 initialValue = false,
220             )
221             .dumpWhileCollecting("isShadeLocked")
222 
223     @VisibleForTesting
224     val paddingTopDimen: Flow<Int> =
225         if (SceneContainerFlag.isEnabled) {
226                 configurationInteractor.onAnyConfigurationChange.map {
227                     with(context.resources) {
228                         val useLargeScreenHeader =
229                             getBoolean(R.bool.config_use_large_screen_shade_header)
230                         if (useLargeScreenHeader) {
231                             largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight()
232                         } else {
233                             getDimensionPixelSize(R.dimen.notification_panel_margin_top)
234                         }
235                     }
236                 }
237             } else {
238                 interactor.configurationBasedDimensions.map {
239                     when {
240                         it.useLargeScreenHeader -> it.marginTopLargeScreen
241                         else -> it.marginTop
242                     }
243                 }
244             }
245             .distinctUntilChanged()
246             .dumpWhileCollecting("paddingTopDimen")
247 
248     val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
249         if (SceneContainerFlag.isEnabled) {
250                 combine(
251                     shadeModeInteractor.isShadeLayoutWide,
252                     shadeModeInteractor.shadeMode,
253                     configurationInteractor.onAnyConfigurationChange,
254                 ) { isShadeLayoutWide, shadeMode, _ ->
255                     with(context.resources) {
256                         val marginHorizontal =
257                             getDimensionPixelSize(
258                                 if (shadeMode is Dual) {
259                                     R.dimen.shade_panel_margin_horizontal
260                                 } else {
261                                     R.dimen.notification_panel_margin_horizontal
262                                 }
263                             )
264 
265                         val horizontalPosition =
266                             when (shadeMode) {
267                                 Single -> HorizontalPosition.EdgeToEdge
268                                 Split -> HorizontalPosition.MiddleToEdge(ratio = 0.5f)
269                                 Dual ->
270                                     if (isShadeLayoutWide) {
271                                         HorizontalPosition.FloatAtStart(
272                                             width = getDimensionPixelSize(R.dimen.shade_panel_width)
273                                         )
274                                     } else {
275                                         HorizontalPosition.EdgeToEdge
276                                     }
277                             }
278 
279                         ConfigurationBasedDimensions(
280                             horizontalPosition = horizontalPosition,
281                             marginStart = if (shadeMode is Split) 0 else marginHorizontal,
282                             marginEnd = marginHorizontal,
283                             marginBottom =
284                                 getDimensionPixelSize(R.dimen.notification_panel_margin_bottom),
285                             // y position of the NSSL in the window needs to be 0 under scene
286                             // container
287                             marginTop = 0,
288                         )
289                     }
290                 }
291             } else {
292                 interactor.configurationBasedDimensions.map {
293                     ConfigurationBasedDimensions(
294                         horizontalPosition =
295                             if (it.useSplitShade) HorizontalPosition.MiddleToEdge()
296                             else HorizontalPosition.EdgeToEdge,
297                         marginStart = if (it.useSplitShade) 0 else it.marginHorizontal,
298                         marginEnd = it.marginHorizontal,
299                         marginBottom = it.marginBottom,
300                         marginTop =
301                             if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop,
302                     )
303                 }
304             }
305             .distinctUntilChanged()
306             .dumpWhileCollecting("configurationBasedDimensions")
307 
308     private val isOnAnyBouncer: Flow<Boolean> =
309         anyOf(
310             keyguardTransitionInteractor.transitionValue(ALTERNATE_BOUNCER).map { it > 0f },
311             keyguardTransitionInteractor
312                 .transitionValue(
313                     content = Overlays.Bouncer,
314                     stateWithoutSceneContainer = PRIMARY_BOUNCER,
315                 )
316                 .map { it > 0f },
317         )
318 
319     /** If the user is visually on one of the unoccluded lockscreen states. */
320     val isOnLockscreen: Flow<Boolean> =
321         if (glanceableHubV2()) {
322                 anyOf(
323                     keyguardTransitionInteractor.transitionValue(AOD).map { it > 0f },
324                     keyguardTransitionInteractor.transitionValue(DOZING).map { it > 0f },
325                     keyguardTransitionInteractor.transitionValue(LOCKSCREEN).map { it > 0f },
326                     allOf(
327                         // Exclude bouncer showing over communal hub, as this should not be
328                         // considered
329                         // "lockscreen"
330                         not(communalSceneInteractor.isCommunalVisible),
331                         isOnAnyBouncer,
332                     ),
333                 )
334             } else {
335                 anyOf(
336                     keyguardTransitionInteractor.transitionValue(AOD).map { it > 0f },
337                     keyguardTransitionInteractor.transitionValue(DOZING).map { it > 0f },
338                     keyguardTransitionInteractor.transitionValue(LOCKSCREEN).map { it > 0f },
339                     isOnAnyBouncer,
340                 )
341             }
342             .flowName("isOnLockscreen")
343             .stateIn(
344                 scope = applicationScope,
345                 started = SharingStarted.Eagerly,
346                 initialValue = false,
347             )
348             .dumpValue("isOnLockscreen")
349 
350     /** Are we purely on the keyguard without the shade/qs? */
351     val isOnLockscreenWithoutShade: Flow<Boolean> =
352         combine(isOnLockscreen, isAnyExpanded) { isKeyguard, isAnyExpanded ->
353                 isKeyguard && !isAnyExpanded
354             }
355             .flowName("isOnLockscreenWithoutShade")
356             .stateIn(
357                 scope = applicationScope,
358                 started = SharingStarted.Eagerly,
359                 initialValue = false,
360             )
361             .dumpValue("isOnLockscreenWithoutShade")
362 
363     private val aboutToTransitionToHub: Flow<Unit> =
364         if (SceneContainerFlag.isEnabled) {
365             emptyFlow()
366         } else {
367             conflatedCallbackFlow {
368                 val callback =
369                     CommunalSceneInteractor.OnSceneAboutToChangeListener { toScene, _ ->
370                         if (toScene == CommunalScenes.Communal) {
371                             trySend(Unit)
372                         }
373                     }
374                 communalSceneInteractor.registerSceneStateProcessor(callback)
375                 awaitClose { communalSceneInteractor.unregisterSceneStateProcessor(callback) }
376             }
377         }
378 
379     /** If the user is visually on the glanceable hub or transitioning to/from it */
380     private val isOnGlanceableHub: Flow<Boolean> =
381         merge(
382                 aboutToTransitionToHub.map { true },
383                 anyOf(
384                     keyguardTransitionInteractor.isFinishedIn(
385                         content = Scenes.Communal,
386                         stateWithoutSceneContainer = GLANCEABLE_HUB,
387                     ),
388                     keyguardTransitionInteractor.isInTransition(
389                         edge = Edge.create(to = Scenes.Communal),
390                         edgeWithoutSceneContainer = Edge.create(to = GLANCEABLE_HUB),
391                     ),
392                     keyguardTransitionInteractor.isInTransition(
393                         edge = Edge.create(from = Scenes.Communal),
394                         edgeWithoutSceneContainer = Edge.create(from = GLANCEABLE_HUB),
395                     ),
396                 ),
397             )
398             .distinctUntilChanged()
399             .dumpWhileCollecting("isOnGlanceableHub")
400 
401     /** Are we purely on the glanceable hub without the shade/qs? */
402     val isOnGlanceableHubWithoutShade: Flow<Boolean> =
403         combine(isOnGlanceableHub, isAnyExpanded) { isGlanceableHub, isAnyExpanded ->
404                 isGlanceableHub && !isAnyExpanded
405             }
406             .flowName("isOnGlanceableHubWithoutShade")
407             .stateIn(
408                 scope = applicationScope,
409                 started = SharingStarted.Eagerly,
410                 initialValue = false,
411             )
412             .dumpValue("isOnGlanceableHubWithoutShade")
413 
414     /** Are we on the dream without the shade/qs? */
415     private val isDreamingWithoutShade: Flow<Boolean> =
416         combine(keyguardTransitionInteractor.isFinishedIn(DREAMING), isAnyExpanded) {
417                 isDreaming,
418                 isAnyExpanded ->
419                 isDreaming && !isAnyExpanded
420             }
421             .flowName("isDreamingWithoutShade")
422             .stateIn(
423                 scope = applicationScope,
424                 started = SharingStarted.Eagerly,
425                 initialValue = false,
426             )
427             .dumpValue("isDreamingWithoutShade")
428 
429     /**
430      * Fade in if the user swipes the shade back up, not if collapsed by going to AOD. This is
431      * needed due to the lack of a SHADE state with existing keyguard transitions.
432      */
433     private fun awaitCollapse(): Flow<Boolean> {
434         var aodTransitionIsComplete = true
435         return combine(
436                 isOnLockscreenWithoutShade,
437                 keyguardTransitionInteractor.isInTransition(
438                     edge = Edge.create(from = LOCKSCREEN, to = AOD)
439                 ),
440                 ::Pair,
441             )
442             .transformWhile { (isOnLockscreenWithoutShade, aodTransitionIsRunning) ->
443                 // Wait until the AOD transition is complete before terminating
444                 if (!aodTransitionIsComplete && !aodTransitionIsRunning) {
445                     aodTransitionIsComplete = true
446                     emit(false) // do not fade in
447                     false
448                 } else if (aodTransitionIsRunning) {
449                     aodTransitionIsComplete = false
450                     true
451                 } else if (isOnLockscreenWithoutShade) {
452                     // Shade is closed, fade in and terminate
453                     emit(true)
454                     false
455                 } else {
456                     true
457                 }
458             }
459     }
460 
461     /** Fade in only for use after the shade collapses */
462     val shadeCollapseFadeIn: Flow<Boolean> =
463         flow {
464                 while (currentCoroutineContext().isActive) {
465                     // Ensure shade is collapsed
466                     isShadeLocked.first { !it }
467                     emit(false)
468                     // Wait for shade to be fully expanded
469                     isShadeLocked.first { it }
470                     // ... and then for it to be collapsed OR a transition to AOD begins.
471                     // If AOD, do not fade in (a fade out occurs instead).
472                     awaitCollapse().collect { doFadeIn ->
473                         if (doFadeIn) {
474                             emit(true)
475                         }
476                     }
477                 }
478             }
479             .flowName("shadeCollapseFadeIn")
480             .stateIn(
481                 scope = applicationScope,
482                 started = SharingStarted.WhileSubscribed(),
483                 initialValue = false,
484             )
485             .dumpValue("shadeCollapseFadeIn")
486 
487     /**
488      * The container occupies the entire screen, and must be positioned relative to other elements.
489      *
490      * On keyguard, this generally fits below the clock and above the lock icon, or in split shade,
491      * the top of the screen to the lock icon.
492      *
493      * When the shade is expanding, the position is controlled by... the shade.
494      */
495     val bounds: StateFlow<NotificationContainerBounds> by lazy {
496         SceneContainerFlag.assertInLegacyMode()
497         combine(
498                 isOnLockscreenWithoutShade,
499                 keyguardInteractor.notificationContainerBounds,
500                 paddingTopDimen,
501                 interactor.topPosition
502                     .sampleCombine(
503                         keyguardTransitionInteractor.isInTransition,
504                         shadeInteractor.qsExpansion,
505                     )
506                     .onStart { emit(Triple(0f, false, 0f)) },
507             ) { onLockscreen, bounds, paddingTop, (top, isInTransitionToAnyState, qsExpansion) ->
508                 if (onLockscreen) {
509                     bounds.copy(top = bounds.top - paddingTop)
510                 } else {
511                     // When QS expansion > 0, it should directly set the top padding so do not
512                     // animate it
513                     val animate = qsExpansion == 0f && !isInTransitionToAnyState
514                     bounds.copy(top = top, isAnimated = animate)
515                 }
516             }
517             .flowName("bounds")
518             .stateIn(
519                 scope = applicationScope,
520                 started = SharingStarted.Lazily,
521                 initialValue = NotificationContainerBounds(),
522             )
523             .dumpValue("bounds")
524     }
525 
526     /**
527      * Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out
528      * notifications unless it's a large screen.
529      */
530     private val alphaForShadeAndQsExpansion: Flow<Float> =
531         if (SceneContainerFlag.isEnabled) {
532                 shadeModeInteractor.shadeMode.flatMapLatest { shadeMode ->
533                     when (shadeMode) {
534                         Single ->
535                             combineTransform(
536                                 shadeInteractor.shadeExpansion,
537                                 shadeInteractor.qsExpansion,
538                                 bouncerInteractor.bouncerExpansion,
539                             ) { shadeExpansion, qsExpansion, bouncerExpansion ->
540                                 if (bouncerExpansion == 1f) {
541                                     emit(0f)
542                                 } else if (bouncerExpansion > 0f) {
543                                     emit(1 - bouncerExpansion)
544                                 } else if (qsExpansion == 1f) {
545                                     // Ensure HUNs will be visible in QS shade (at least while
546                                     // unlocked)
547                                     emit(1f)
548                                 } else if (shadeExpansion > 0f || qsExpansion > 0f) {
549                                     // Fade as QS shade expands
550                                     emit(1f - qsExpansion)
551                                 }
552                             }
553 
554                         Split ->
555                             combineTransform(isAnyExpanded, bouncerInteractor.bouncerExpansion) {
556                                 isAnyExpanded,
557                                 bouncerExpansion ->
558                                 if (bouncerExpansion == 1f) {
559                                     emit(0f)
560                                 } else if (bouncerExpansion > 0f) {
561                                     emit(1 - bouncerExpansion)
562                                 } else if (isAnyExpanded) {
563                                     emit(1f)
564                                 }
565                             }
566 
567                         Dual ->
568                             combineTransform(
569                                 shadeModeInteractor.isShadeLayoutWide,
570                                 headsUpNotificationInteractor.get().isHeadsUpOrAnimatingAway,
571                                 shadeInteractor.shadeExpansion,
572                                 shadeInteractor.qsExpansion,
573                                 bouncerInteractor.bouncerExpansion,
574                             ) {
575                                 isShadeLayoutWide,
576                                 isHeadsUpOrAnimatingAway,
577                                 shadeExpansion,
578                                 qsExpansion,
579                                 bouncerExpansion ->
580                                 if (bouncerExpansion == 1f) {
581                                     emit(0f)
582                                 } else if (bouncerExpansion > 0f) {
583                                     emit(1 - bouncerExpansion)
584                                 } else if (isShadeLayoutWide) {
585                                     if (shadeExpansion > 0f) {
586                                         emit(1f)
587                                     }
588                                 } else if (isHeadsUpOrAnimatingAway) {
589                                     // Ensure HUNs will be visible in QS shade (at least while
590                                     // unlocked)
591                                     emit(1f)
592                                 } else if (shadeExpansion > 0f || qsExpansion > 0f) {
593                                     // On a narrow screen, the QS shade overlaps with lockscreen
594                                     // notifications. Fade them out as the QS shade expands.
595                                     emit(1f - qsExpansion)
596                                 }
597                             }
598                     }
599                 }
600             } else {
601                 interactor.configurationBasedDimensions.flatMapLatest { configurationBasedDimensions
602                     ->
603                     combineTransform(shadeInteractor.shadeExpansion, shadeInteractor.qsExpansion) {
604                         shadeExpansion,
605                         qsExpansion ->
606                         if (shadeExpansion > 0f || qsExpansion > 0f) {
607                             if (configurationBasedDimensions.useSplitShade) {
608                                 emit(1f)
609                             } else if (qsExpansion == 1f) {
610                                 // Ensure HUNs will be visible in QS shade (at least while
611                                 // unlocked)
612                                 emit(1f)
613                             } else {
614                                 // Fade as QS shade expands
615                                 emit(1f - qsExpansion)
616                             }
617                         }
618                     }
619                 }
620             }
621             .onStart { emit(1f) }
622             .dumpWhileCollecting("alphaForShadeAndQsExpansion")
623 
624     val panelAlpha = keyguardInteractor.panelAlpha
625 
626     private fun bouncerToGoneNotificationAlpha(viewState: ViewStateAccessor): Flow<Float> =
627         merge(
628                 primaryBouncerToGoneTransitionViewModel.notificationAlpha(viewState),
629                 alternateBouncerToGoneTransitionViewModel.notificationAlpha(viewState),
630             )
631             .sample(communalSceneInteractor.isCommunalVisible) { alpha, isCommunalVisible ->
632                 // when glanceable hub is visible, hide notifications during the transition to GONE
633                 if (isCommunalVisible) 0f else alpha
634             }
635             .dumpWhileCollecting("bouncerToGoneNotificationAlpha")
636 
637     private fun alphaForTransitions(viewState: ViewStateAccessor): Flow<Float> {
638         return merge(
639             keyguardInteractor.dismissAlpha.dumpWhileCollecting("keyguardInteractor.dismissAlpha"),
640             // All transition view models are mutually exclusive, and safe to merge
641             bouncerToGoneNotificationAlpha(viewState),
642             aodToGoneTransitionViewModel.notificationAlpha(viewState),
643             aodToLockscreenTransitionViewModel.notificationAlpha,
644             aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
645             aodToGlanceableHubTransitionViewModel.lockscreenAlpha(viewState),
646             aodToPrimaryBouncerTransitionViewModel.notificationAlpha,
647             dozingToLockscreenTransitionViewModel.lockscreenAlpha,
648             dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
649             dozingToPrimaryBouncerTransitionViewModel.notificationAlpha,
650             dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
651             goneToAodTransitionViewModel.notificationAlpha,
652             goneToDreamingTransitionViewModel.lockscreenAlpha,
653             goneToDozingTransitionViewModel.notificationAlpha,
654             goneToLockscreenTransitionViewModel.lockscreenAlpha,
655             lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
656             lockscreenToGoneTransitionViewModel.notificationAlpha(viewState),
657             lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
658             lockscreenToPrimaryBouncerTransitionViewModel.notificationAlpha,
659             alternateBouncerToPrimaryBouncerTransitionViewModel.notificationAlpha,
660             occludedToAodTransitionViewModel.lockscreenAlpha,
661             occludedToGoneTransitionViewModel.notificationAlpha(viewState),
662             occludedToLockscreenTransitionViewModel.lockscreenAlpha,
663             offToLockscreenTransitionViewModel.lockscreenAlpha,
664             primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
665             glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
666             glanceableHubToAodTransitionViewModel.lockscreenAlpha,
667             lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
668         )
669     }
670 
671     fun keyguardAlpha(viewState: ViewStateAccessor, scope: CoroutineScope): Flow<Float> {
672         val isKeyguardOccluded =
673             keyguardTransitionInteractor.transitionValue(OCCLUDED).map { it == 1f }
674 
675         val isKeyguardNotVisibleInState =
676             if (SceneContainerFlag.isEnabled) {
677                 isKeyguardOccluded
678             } else {
679                 anyOf(
680                     isKeyguardOccluded,
681                     keyguardTransitionInteractor
682                         .transitionValue(content = Scenes.Gone, stateWithoutSceneContainer = GONE)
683                         .map { it == 1f },
684                 )
685             }
686 
687         // Transitions are not (yet) authoritative for NSSL; they still rely on StatusBarState to
688         // help determine when the device has fully moved to GONE or OCCLUDED state. Once SHADE
689         // state has been set, let shade alpha take over
690         val isKeyguardNotVisible =
691             combine(isKeyguardNotVisibleInState, keyguardInteractor.statusBarState) {
692                 isKeyguardNotVisibleInState,
693                 statusBarState ->
694                 isKeyguardNotVisibleInState && statusBarState == SHADE
695             }
696 
697         // This needs to continue collecting the current value so that when it is selected in the
698         // flatMapLatest below, the last value gets emitted, to avoid the randomness of `merge`.
699         val alphaForTransitionsAndShade =
700             merge(alphaForTransitions(viewState), alphaForShadeAndQsExpansion)
701                 .flowName("alphaForTransitionsAndShade")
702                 .stateIn(
703                     // Use view-level scope instead of ApplicationScope, to prevent collection that
704                     // never stops
705                     scope = scope,
706                     started = SharingStarted.Eagerly,
707                     initialValue = 1f,
708                 )
709                 .dumpValue("alphaForTransitionsAndShade")
710 
711         return isKeyguardNotVisible
712             .flatMapLatest { isKeyguardNotVisible ->
713                 if (isKeyguardNotVisible) {
714                     alphaForShadeAndQsExpansion
715                 } else {
716                     alphaForTransitionsAndShade
717                 }
718             }
719             .distinctUntilChanged()
720             .dumpWhileCollecting("keyguardAlpha")
721     }
722 
723     val blurRadius =
724         primaryBouncerTransitions
725             .map { transition -> transition.notificationBlurRadius }
726             .merge()
727             .dumpWhileCollecting("blurRadius")
728 
729     /**
730      * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB or
731      * DREAMING<->GLANCEABLE_HUB transition or idle on the hub.
732      *
733      * Must return 1.0f when not controlling the alpha since notifications does a min of all the
734      * alpha sources.
735      */
736     val glanceableHubAlpha: Flow<Float> =
737         combineTransform(
738                 isOnGlanceableHubWithoutShade,
739                 isOnLockscreen,
740                 isDreamingWithoutShade,
741                 merge(
742                         lockscreenToGlanceableHubTransitionViewModel.notificationAlpha,
743                         glanceableHubToLockscreenTransitionViewModel.notificationAlpha,
744                         dozingToGlanceableHubTransitionViewModel.notificationAlpha,
745                     )
746                     // Manually emit on start because [notificationAlpha] only starts emitting
747                     // when transitions start.
748                     .onStart { emit(1f) },
749             ) { isOnGlanceableHubWithoutShade, isOnLockscreen, isDreamingWithoutShade, alpha ->
750                 if ((isOnGlanceableHubWithoutShade || isDreamingWithoutShade) && !isOnLockscreen) {
751                     // Notifications should not be visible on the glanceable hub.
752                     // TODO(b/321075734): implement a way to actually set the notifications to
753                     // gone while on the hub instead of just adjusting alpha
754                     emit(0f)
755                 } else if (isOnGlanceableHubWithoutShade) {
756                     // We are transitioning between hub and lockscreen, so set the alpha for the
757                     // transition animation.
758                     emit(alpha)
759                 } else {
760                     // Not on the hub and no transitions running, return full visibility so we
761                     // don't block the notifications from showing.
762                     emit(1f)
763                 }
764             }
765             .distinctUntilChanged()
766             .dumpWhileCollecting("glanceableHubAlpha")
767 
768     /**
769      * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be
770      * translated as the keyguard fades out.
771      */
772     val translationY: Flow<Float> =
773         combine(
774                 aodBurnInViewModel.movement.map { it.translationY.toFloat() }.onStart { emit(0f) },
775                 isOnLockscreenWithoutShade,
776                 merge(
777                     keyguardInteractor.keyguardTranslationY,
778                     occludedToLockscreenTransitionViewModel.lockscreenTranslationY,
779                 ),
780             ) { burnInY, isOnLockscreenWithoutShade, translationY ->
781                 // with SceneContainer, x translation is handled by views, y is handled by compose
782                 SceneContainerFlag.assertInLegacyMode()
783 
784                 if (isOnLockscreenWithoutShade) {
785                     burnInY + translationY
786                 } else {
787                     0f
788                 }
789             }
790             .dumpWhileCollecting("translationY")
791 
792     /** Horizontal translation to apply to the container. */
793     val translationX: Flow<Float> =
794         merge(
795                 // The container may need to be translated along the X axis as the keyguard fades
796                 // out, such as when swiping open the glanceable hub from the lockscreen.
797                 lockscreenToGlanceableHubTransitionViewModel.notificationTranslationX,
798                 glanceableHubToLockscreenTransitionViewModel.notificationTranslationX,
799                 if (SceneContainerFlag.isEnabled) {
800                     // The container may need to be translated along the X axis as the unfolded
801                     // foldable is folded slightly.
802                     unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false)
803                 } else {
804                     emptyFlow()
805                 },
806             )
807             .dumpWhileCollecting("translationX")
808 
809     private val availableHeight: Flow<Float> =
810         if (SceneContainerFlag.isEnabled) {
811                 notificationStackAppearanceInteractor.constrainedAvailableSpace.map { it.toFloat() }
812             } else {
813                 bounds.map { it.bottom - it.top }
814             }
815             .distinctUntilChanged()
816             .dumpWhileCollecting("availableHeight")
817 
818     /**
819      * When on keyguard, there is limited space to display notifications so calculate how many could
820      * be shown. Otherwise, there is no limit since the vertical space will be scrollable.
821      *
822      * When expanding or when the user is interacting with the shade, keep the count stable; do not
823      * emit a value.
824      */
825     fun getLockscreenDisplayConfig(
826         calculateSpace: (Float, Boolean) -> Int
827     ): Flow<LockscreenDisplayConfig> {
828         val showLimitedNotifications = isOnLockscreenWithoutShade
829         val showUnlimitedNotificationsAndIsOnLockScreen =
830             combine(
831                 isOnLockscreen,
832                 keyguardInteractor.statusBarState,
833                 merge(
834                         primaryBouncerToGoneTransitionViewModel.showAllNotifications,
835                         alternateBouncerToGoneTransitionViewModel.showAllNotifications,
836                     )
837                     .onStart { emit(false) },
838             ) { isOnLockscreen, statusBarState, showAllNotifications ->
839                 (statusBarState == SHADE_LOCKED || !isOnLockscreen || showAllNotifications) to
840                     isOnLockscreen
841             }
842 
843         @Suppress("UNCHECKED_CAST")
844         return combineTransform(
845                 showLimitedNotifications,
846                 showUnlimitedNotificationsAndIsOnLockScreen,
847                 shadeInteractor.isUserInteracting,
848                 availableHeight,
849                 interactor.notificationStackChanged,
850                 interactor.useExtraShelfSpace,
851             ) { flows ->
852                 val showLimitedNotifications = flows[0] as Boolean
853                 val (showUnlimitedNotifications, isOnLockscreen) =
854                     flows[1] as Pair<Boolean, Boolean>
855                 val isUserInteracting = flows[2] as Boolean
856                 val availableHeight = flows[3] as Float
857                 val useExtraShelfSpace = flows[5] as Boolean
858 
859                 if (!isUserInteracting) {
860                     if (showLimitedNotifications) {
861                         emit(
862                             LockscreenDisplayConfig(
863                                 isOnLockscreen = isOnLockscreen,
864                                 maxNotifications =
865                                     calculateSpace(availableHeight, useExtraShelfSpace),
866                             )
867                         )
868                     } else if (showUnlimitedNotifications) {
869                         emit(
870                             LockscreenDisplayConfig(
871                                 isOnLockscreen = isOnLockscreen,
872                                 maxNotifications = -1,
873                             )
874                         )
875                     }
876                 }
877             }
878             .distinctUntilChanged()
879             .dumpWhileCollecting("maxNotifications")
880     }
881 
882     /**
883      * Wallpaper focal area needs the absolute bottom of notification stack to avoid occlusion. It
884      * should not change with notifications in shade.
885      *
886      * @param calculateMaxNotifications is required by getMaxNotifications as calculateSpace by
887      *   calling computeMaxKeyguardNotifications in NotificationStackSizeCalculator
888      * @param calculateHeight is calling computeHeight in NotificationStackSizeCalculator The edge
889      *   case is that when maxNotifications is 0, we won't take shelfHeight into account
890      */
891     fun getNotificationStackAbsoluteBottom(
892         calculateMaxNotifications: (Float, Boolean) -> Int,
893         calculateHeight: (Int) -> Float,
894         shelfHeight: Float,
895     ): Flow<Float> {
896         SceneContainerFlag.assertInLegacyMode()
897 
898         return combine(
899                 getLockscreenDisplayConfig(calculateMaxNotifications).map { (_, maxNotifications) ->
900                     val height = calculateHeight(maxNotifications)
901                     if (maxNotifications == 0) {
902                         height - shelfHeight
903                     } else {
904                         height
905                     }
906                 },
907                 bounds.map { it.top },
908                 isOnLockscreenWithoutShade,
909             ) { height, top, isOnLockscreenWithoutShade ->
910                 if (isOnLockscreenWithoutShade) {
911                     top + height
912                 } else {
913                     null
914                 }
915             }
916             .filterNotNull()
917     }
918 
919     fun notificationStackChanged() {
920         interactor.notificationStackChanged()
921     }
922 
923     data class ConfigurationBasedDimensions(
924         val horizontalPosition: HorizontalPosition,
925         val marginStart: Int,
926         val marginTop: Int,
927         val marginEnd: Int,
928         val marginBottom: Int,
929     )
930 
931     /** Specifies the horizontal layout constraints for the notification container. */
932     sealed interface HorizontalPosition {
933         /** The container is using the full width of the screen (minus any margins). */
934         data object EdgeToEdge : HorizontalPosition
935 
936         /** The container is laid out from the given [ratio] of the screen width to the end edge. */
937         data class MiddleToEdge(val ratio: Float = 0.5f) : HorizontalPosition
938 
939         /**
940          * The container has a fixed [width] and is aligned to the start of the screen. In this
941          * layout, the end edge of the container is floating, i.e. unconstrained.
942          */
943         data class FloatAtStart(val width: Int) : HorizontalPosition
944     }
945 
946     /**
947      * Data class representing a configuration for displaying Notifications on the Lockscreen.
948      *
949      * @param isOnLockscreen is the user on the lockscreen
950      * @param maxNotifications Limit for the max number of top-level Notifications to be displayed.
951      *   A value of -1 indicates no limit.
952      */
953     data class LockscreenDisplayConfig(val isOnLockscreen: Boolean, val maxNotifications: Int)
954 }
955