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 com.android.compose.animation.scene.ObservableTransitionState
21 import com.android.compose.animation.scene.SceneKey
22 import com.android.systemui.dagger.SysUISingleton
23 import com.android.systemui.dump.DumpManager
24 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
25 import com.android.systemui.scene.domain.interactor.SceneInteractor
26 import com.android.systemui.scene.shared.flag.SceneContainerFlag
27 import com.android.systemui.scene.shared.model.SceneFamilies
28 import com.android.systemui.scene.shared.model.Scenes
29 import com.android.systemui.shade.domain.interactor.ShadeInteractor
30 import com.android.systemui.shade.shared.model.ShadeMode
31 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
32 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimClipping
33 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
34 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_DELAYED_STACK_FADE_IN
35 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
36 import com.android.systemui.util.kotlin.FlowDumperImpl
37 import dagger.Lazy
38 import javax.inject.Inject
39 import kotlinx.coroutines.flow.Flow
40 import kotlinx.coroutines.flow.combine
41 import kotlinx.coroutines.flow.distinctUntilChanged
42 import kotlinx.coroutines.flow.flowOf
43 import kotlinx.coroutines.flow.map
44 
45 /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
46 @SysUISingleton
47 class NotificationScrollViewModel
48 @Inject
49 constructor(
50     dumpManager: DumpManager,
51     stackAppearanceInteractor: NotificationStackAppearanceInteractor,
52     shadeInteractor: ShadeInteractor,
53     private val sceneInteractor: SceneInteractor,
54     // TODO(b/336364825) Remove Lazy when SceneContainerFlag is released -
55     // while the flag is off, creating this object too early results in a crash
56     keyguardInteractor: Lazy<KeyguardInteractor>,
57 ) : FlowDumperImpl(dumpManager) {
58     /**
59      * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning
60      * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while
61      * transitioning from Shade to QuickSettings scenes.
62      */
63     val expandFraction: Flow<Float> =
64         combine(
65                 shadeInteractor.shadeExpansion,
66                 shadeInteractor.shadeMode,
67                 shadeInteractor.qsExpansion,
68                 sceneInteractor.transitionState,
69                 sceneInteractor.resolveSceneFamily(SceneFamilies.QuickSettings),
70             ) { shadeExpansion, shadeMode, qsExpansion, transitionState, quickSettingsScene ->
71                 when (transitionState) {
72                     is ObservableTransitionState.Idle -> {
73                         if (transitionState.currentScene == Scenes.Lockscreen) {
74                             1f
75                         } else {
76                             shadeExpansion
77                         }
78                     }
79                     is ObservableTransitionState.Transition -> {
80                         if (
81                             (transitionState.fromScene in SceneFamilies.NotifShade &&
82                                 transitionState.toScene == quickSettingsScene) ||
83                                 (transitionState.fromScene in quickSettingsScene &&
84                                     transitionState.toScene in SceneFamilies.NotifShade) ||
85                                 (transitionState.fromScene == Scenes.Lockscreen &&
86                                     transitionState.toScene in SceneFamilies.NotifShade) ||
87                                 (transitionState.fromScene in SceneFamilies.NotifShade &&
88                                     transitionState.toScene == Scenes.Lockscreen)
89                         ) {
90                             1f
91                         } else if (
92                             shadeMode != ShadeMode.Split &&
93                                 transitionState.fromScene in SceneFamilies.Home &&
94                                 transitionState.toScene in quickSettingsScene
95                         ) {
96                             // during QS expansion, increase fraction at same rate as scrim alpha,
97                             // but start when scrim alpha is at EXPANSION_FOR_DELAYED_STACK_FADE_IN.
98                             (qsExpansion / EXPANSION_FOR_MAX_SCRIM_ALPHA -
99                                     EXPANSION_FOR_DELAYED_STACK_FADE_IN)
100                                 .coerceIn(0f, 1f)
101                         } else {
102                             shadeExpansion
103                         }
104                     }
105                 }
106             }
107             .distinctUntilChanged()
108             .dumpWhileCollecting("expandFraction")
109 
110     private operator fun SceneKey.contains(scene: SceneKey) =
111         sceneInteractor.isSceneInFamily(scene, this)
112 
113     /** The bounds of the notification stack in the current scene. */
114     private val shadeScrimClipping: Flow<ShadeScrimClipping?> =
115         combine(
116                 stackAppearanceInteractor.shadeScrimBounds,
117                 stackAppearanceInteractor.shadeScrimRounding,
118             ) { bounds, rounding ->
119                 bounds?.let { ShadeScrimClipping(it, rounding) }
120             }
121             .dumpWhileCollecting("stackClipping")
122 
123     fun shadeScrimShape(
124         cornerRadius: Flow<Int>,
125         viewLeftOffset: Flow<Int>
126     ): Flow<ShadeScrimShape?> =
127         combine(shadeScrimClipping, cornerRadius, viewLeftOffset) { clipping, radius, leftOffset ->
128                 if (clipping == null) return@combine null
129                 ShadeScrimShape(
130                     bounds = clipping.bounds.minus(leftOffset = leftOffset),
131                     topRadius = radius.takeIf { clipping.rounding.isTopRounded } ?: 0,
132                     bottomRadius = radius.takeIf { clipping.rounding.isBottomRounded } ?: 0
133                 )
134             }
135             .dumpWhileCollecting("shadeScrimShape")
136 
137     /**
138      * Max alpha to apply directly to the view based on the compose placeholder.
139      *
140      * TODO(b/338590620): Migrate alphas from [SharedNotificationContainerViewModel] into this flow
141      */
142     val maxAlpha: Flow<Float> =
143         stackAppearanceInteractor.alphaForBrightnessMirror.dumpValue("maxAlpha")
144 
145     /**
146      * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
147      * further.
148      */
149     val scrolledToTop: Flow<Boolean> =
150         stackAppearanceInteractor.scrolledToTop.dumpValue("scrolledToTop")
151 
152     /** Receives the amount (px) that the stack should scroll due to internal expansion. */
153     val syntheticScrollConsumer: (Float) -> Unit = stackAppearanceInteractor::setSyntheticScroll
154 
155     /**
156      * Receives whether the current touch gesture is overscroll as it has already been consumed by
157      * the stack.
158      */
159     val currentGestureOverscrollConsumer: (Boolean) -> Unit =
160         stackAppearanceInteractor::setCurrentGestureOverscroll
161 
162     /** Whether the notification stack is scrollable or not. */
163     val isScrollable: Flow<Boolean> = sceneInteractor.currentScene.map {
164         sceneInteractor.isSceneInFamily(it, SceneFamilies.NotifShade) || it == Scenes.Lockscreen
165     }.dumpWhileCollecting("isScrollable")
166 
167     /** Whether the notification stack is displayed in doze mode. */
168     val isDozing: Flow<Boolean> by lazy {
169         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
170             flowOf(false)
171         } else {
172             keyguardInteractor.get().isDozing.dumpWhileCollecting("isDozing")
173         }
174     }
175 }
176