• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
18 
19 import androidx.compose.runtime.getValue
20 import com.android.app.tracing.coroutines.launchTraced as launch
21 import com.android.compose.animation.scene.ContentKey
22 import com.android.compose.animation.scene.ObservableTransitionState
23 import com.android.systemui.dump.DumpManager
24 import com.android.systemui.flags.FeatureFlagsClassic
25 import com.android.systemui.flags.Flags
26 import com.android.systemui.lifecycle.ExclusiveActivatable
27 import com.android.systemui.lifecycle.Hydrator
28 import com.android.systemui.scene.domain.interactor.SceneInteractor
29 import com.android.systemui.scene.shared.flag.SceneContainerFlag
30 import com.android.systemui.scene.shared.model.Overlays
31 import com.android.systemui.scene.shared.model.Scenes
32 import com.android.systemui.shade.domain.interactor.ShadeInteractor
33 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
34 import com.android.systemui.shade.shared.model.ShadeMode
35 import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
36 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
37 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
38 import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent
39 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
40 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding
41 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrollState
42 import com.android.systemui.util.kotlin.ActivatableFlowDumper
43 import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl
44 import dagger.assisted.AssistedFactory
45 import dagger.assisted.AssistedInject
46 import java.util.function.Consumer
47 import kotlinx.coroutines.coroutineScope
48 import kotlinx.coroutines.flow.Flow
49 import kotlinx.coroutines.flow.filter
50 import kotlinx.coroutines.flow.map
51 
52 /**
53  * ViewModel used by the Notification placeholders inside the scene container to update the
54  * [NotificationStackAppearanceInteractor], and by extension control the NSSL.
55  */
56 class NotificationsPlaceholderViewModel
57 @AssistedInject
58 constructor(
59     private val interactor: NotificationStackAppearanceInteractor,
60     private val sceneInteractor: SceneInteractor,
61     private val shadeInteractor: ShadeInteractor,
62     shadeModeInteractor: ShadeModeInteractor,
63     private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
64     remoteInputInteractor: RemoteInputInteractor,
65     featureFlags: FeatureFlagsClassic,
66     dumpManager: DumpManager,
67 ) :
68     ExclusiveActivatable(),
69     ActivatableFlowDumper by ActivatableFlowDumperImpl(
70         dumpManager = dumpManager,
71         tag = "NotificationsPlaceholderViewModel",
<lambda>null72     ) {
73 
74     private val hydrator = Hydrator("NotificationsPlaceholderViewModel")
75 
76     /** The content key to use for the notification shade. */
77     val notificationsShadeContentKey: ContentKey by
78         hydrator.hydratedStateOf(
79             traceName = "notificationsShadeContentKey",
80             initialValue = getNotificationsShadeContentKey(shadeModeInteractor.shadeMode.value),
81             source = shadeModeInteractor.shadeMode.map { getNotificationsShadeContentKey(it) },
82         )
83 
84     /** The content key to use for the quick settings shade. */
85     val quickSettingsShadeContentKey: ContentKey by
86         hydrator.hydratedStateOf(
87             traceName = "quickSettingsShadeContentKey",
88             initialValue = getQuickSettingsShadeContentKey(shadeModeInteractor.shadeMode.value),
89             source = shadeModeInteractor.shadeMode.map { getQuickSettingsShadeContentKey(it) },
90         )
91 
92     /**
93      * Whether the current touch gesture is overscroll. If true, it means the NSSL has already
94      * consumed part of the gesture.
95      */
96     val isCurrentGestureOverscroll: Boolean by
97         hydrator.hydratedStateOf(
98             traceName = "isCurrentGestureOverscroll",
99             initialValue = false,
100             source = interactor.isCurrentGestureOverscroll
101         )
102 
103     /** DEBUG: whether the placeholder should be made slightly visible for positional debugging. */
104     val isVisualDebuggingEnabled: Boolean = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES)
105 
106     /** DEBUG: whether the debug logging should be output. */
107     val isDebugLoggingEnabled: Boolean = SceneContainerFlag.isEnabled
108 
109     override suspend fun onActivated(): Nothing {
110         coroutineScope {
111             launch { hydrator.activate() }
112 
113             launch {
114                 shadeInteractor.isAnyExpanded
115                     .filter { it }
116                     .collect { headsUpNotificationInteractor.unpinAll(true) }
117             }
118 
119             launch {
120                 sceneInteractor.transitionState
121                     .filter { it is ObservableTransitionState.Idle }
122                     .collect { headsUpNotificationInteractor.onTransitionIdle() }
123             }
124         }
125         activateFlowDumper()
126     }
127 
128     /** Notifies that the bounds of the notification scrim have changed. */
129     fun onScrimBoundsChanged(bounds: ShadeScrimBounds?) {
130         interactor.setNotificationShadeScrimBounds(bounds)
131     }
132 
133     /** Sets the available space */
134     fun onConstrainedAvailableSpaceChanged(height: Int) {
135         interactor.setConstrainedAvailableSpace(height)
136     }
137 
138     /** Sets the content alpha for the current state of the brightness mirror */
139     fun setAlphaForBrightnessMirror(alpha: Float) {
140         interactor.setAlphaForBrightnessMirror(alpha)
141     }
142 
143     /** True when a HUN is pinned or animating away. */
144     val isHeadsUpOrAnimatingAway: Flow<Boolean> =
145         headsUpNotificationInteractor.isHeadsUpOrAnimatingAway
146 
147     /** Corner rounding of the stack */
148     val shadeScrimRounding: Flow<ShadeScrimRounding> =
149         interactor.shadeScrimRounding.dumpWhileCollecting("shadeScrimRounding")
150 
151     /**
152      * The amount [0-1] that the shade or quick settings has been opened. At 0, the shade is closed;
153      * at 1, either the shade or quick settings is open.
154      */
155     val expandFraction: Flow<Float> = shadeInteractor.anyExpansion.dumpValue("expandFraction")
156 
157     /**
158      * The amount [0-1] that quick settings has been opened. At 0, the shade may be open or closed;
159      * at 1, the quick settings are open.
160      */
161     val shadeToQsFraction: Flow<Float> = shadeInteractor.qsExpansion.dumpValue("shadeToQsFraction")
162 
163     /**
164      * The amount in px that the notification stack should scroll due to internal expansion. This
165      * should only happen when a notification expansion hits the bottom of the screen, so it is
166      * necessary to scroll up to keep expanding the notification.
167      */
168     val syntheticScroll: Flow<Float> =
169         interactor.syntheticScroll.dumpWhileCollecting("syntheticScroll")
170 
171     /** Whether remote input is currently active for any notification. */
172     val isRemoteInputActive = remoteInputInteractor.isRemoteInputActive
173 
174     /** The bottom bound of the currently focused remote input notification row. */
175     val remoteInputRowBottomBound = remoteInputInteractor.remoteInputRowBottomBound
176 
177     /** Updates the current scroll state of the notification shade. */
178     fun setScrollState(scrollState: ShadeScrollState) {
179         interactor.setScrollState(scrollState)
180     }
181 
182     /** Sets whether the heads up notification is animating away. */
183     fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
184         headsUpNotificationInteractor.setHeadsUpAnimatingAway(animatingAway)
185     }
186 
187     /** Snooze the currently pinned HUN. */
188     fun snoozeHun() {
189         headsUpNotificationInteractor.snooze()
190     }
191 
192     /** Set a consumer for accessibility events to be handled by the placeholder. */
193     fun setAccessibilityScrollEventConsumer(consumer: Consumer<AccessibilityScrollEvent>?) {
194         interactor.setAccessibilityScrollEventConsumer(consumer)
195     }
196 
197     private fun getNotificationsShadeContentKey(shadeMode: ShadeMode): ContentKey {
198         return if (shadeMode is ShadeMode.Dual) Overlays.NotificationsShade else Scenes.Shade
199     }
200 
201     private fun getQuickSettingsShadeContentKey(shadeMode: ShadeMode): ContentKey {
202         return when (shadeMode) {
203             is ShadeMode.Single -> Scenes.QuickSettings
204             is ShadeMode.Split -> Scenes.Shade
205             is ShadeMode.Dual -> Overlays.QuickSettingsShade
206         }
207     }
208 
209     @AssistedFactory
210     interface Factory {
211         fun create(): NotificationsPlaceholderViewModel
212     }
213 }
214 
215 // Expansion fraction thresholds (between 0-1f) at which the corresponding value should be
216 // at its maximum, given they are at their minimum value at expansion = 0f.
217 object NotificationTransitionThresholds {
218     const val EXPANSION_FOR_MAX_CORNER_RADIUS = 0.1f
219     const val EXPANSION_FOR_MAX_SCRIM_ALPHA = 0.3f
220     const val EXPANSION_FOR_DELAYED_STACK_FADE_IN = 0.5f
221 }
222