• 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 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
18 
19 import com.android.systemui.dagger.qualifiers.Background
20 import com.android.systemui.dump.DumpManager
21 import com.android.systemui.scene.shared.flag.SceneContainerFlag
22 import com.android.systemui.shade.domain.interactor.ShadeInteractor
23 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel
24 import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
25 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
26 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
27 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
28 import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
29 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
30 import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
31 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
32 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor
33 import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
34 import com.android.systemui.util.kotlin.FlowDumperImpl
35 import com.android.systemui.util.kotlin.combine
36 import com.android.systemui.util.kotlin.sample
37 import com.android.systemui.util.ui.AnimatableEvent
38 import com.android.systemui.util.ui.AnimatedValue
39 import com.android.systemui.util.ui.toAnimatedValueFlow
40 import java.util.Optional
41 import javax.inject.Inject
42 import kotlinx.coroutines.CoroutineDispatcher
43 import kotlinx.coroutines.flow.Flow
44 import kotlinx.coroutines.flow.combine
45 import kotlinx.coroutines.flow.distinctUntilChanged
46 import kotlinx.coroutines.flow.flowOf
47 import kotlinx.coroutines.flow.flowOn
48 import kotlinx.coroutines.flow.map
49 import kotlinx.coroutines.flow.onStart
50 
51 /**
52  * ViewModel for the list of notifications, including child elements like the Clear all/Manage
53  * button at the bottom (the footer) and the "No notifications" text (the empty shade).
54  */
55 class NotificationListViewModel
56 @Inject
57 constructor(
58     val shelf: NotificationShelfViewModel,
59     val hideListViewModel: HideListViewModel,
60     val ongoingActivityChipsViewModel: OngoingActivityChipsViewModel,
61     val footerViewModelFactory: FooterViewModel.Factory,
62     val emptyShadeViewModelFactory: EmptyShadeViewModel.Factory,
63     val logger: Optional<NotificationLoggerViewModel>,
64     activeNotificationsInteractor: ActiveNotificationsInteractor,
65     notificationStackInteractor: NotificationStackInteractor,
66     private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
67     remoteInputInteractor: RemoteInputInteractor,
68     shadeInteractor: ShadeInteractor,
69     userSetupInteractor: UserSetupInteractor,
70     @Background bgDispatcher: CoroutineDispatcher,
71     dumpManager: DumpManager,
72 ) : FlowDumperImpl(dumpManager) {
73     /**
74      * We want the NSSL to be unimportant for accessibility when there are no notifications in it
75      * while the device is on lock screen, to avoid an unlabelled NSSL view in TalkBack. Otherwise,
76      * we want it to be important for accessibility to enable accessibility auto-scrolling in NSSL.
77      * See b/242235264 for more details.
78      */
79     val isImportantForAccessibility: Flow<Boolean> =
80         combine(
81                 activeNotificationsInteractor.areAnyNotificationsPresent,
82                 notificationStackInteractor.isShowingOnLockscreen,
83             ) { hasNotifications, isShowingOnLockscreen ->
84                 hasNotifications || !isShowingOnLockscreen
85             }
86             .distinctUntilChanged()
87             .dumpWhileCollecting("isImportantForAccessibility")
88             .flowOn(bgDispatcher)
89 
90     val shouldShowEmptyShadeView: Flow<Boolean> by lazy {
91         ModesEmptyShadeFix.assertInLegacyMode()
92         combine(
93                 activeNotificationsInteractor.areAnyNotificationsPresent,
94                 shadeInteractor.isQsFullscreen,
95                 notificationStackInteractor.isShowingOnLockscreen,
96             ) { hasNotifications, isQsFullScreen, isShowingOnLockscreen ->
97                 when {
98                     hasNotifications -> false
99                     isQsFullScreen -> false
100                     // Do not show the empty shade if the lockscreen is visible (including AOD
101                     // b/228790482 and bouncer b/267060171), except if the shade is opened on
102                     // top.
103                     isShowingOnLockscreen -> false
104                     else -> true
105                 }
106             }
107             .distinctUntilChanged()
108             .dumpWhileCollecting("shouldShowEmptyShadeView")
109             .flowOn(bgDispatcher)
110     }
111 
112     val shouldShowEmptyShadeViewAnimated: Flow<AnimatedValue<Boolean>> by lazy {
113         if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
114             flowOf(AnimatedValue.NotAnimating(false))
115         } else {
116             combine(
117                     activeNotificationsInteractor.areAnyNotificationsPresent,
118                     shadeInteractor.isQsFullscreen,
119                     notificationStackInteractor.isShowingOnLockscreen,
120                 ) { hasNotifications, isQsFullScreen, isShowingOnLockscreen ->
121                     when {
122                         hasNotifications -> false
123                         isQsFullScreen -> false
124                         // Do not show the empty shade if the lockscreen is visible (including AOD
125                         // b/228790482 and bouncer b/267060171), except if the shade is opened on
126                         // top.
127                         isShowingOnLockscreen -> false
128                         else -> true
129                     }
130                 }
131                 .distinctUntilChanged()
132                 .sample(
133                     // TODO(b/322167853): This check is currently duplicated in FooterViewModel
134                     //  but instead it should be a field in ShadeAnimationInteractor.
135                     combine(
136                             shadeInteractor.isShadeFullyExpanded,
137                             shadeInteractor.isShadeTouchable,
138                             ::Pair,
139                         )
140                         .onStart { emit(Pair(false, false)) }
141                 ) { visible, (isShadeFullyExpanded, animationsEnabled) ->
142                     val shouldAnimate = isShadeFullyExpanded && animationsEnabled
143                     AnimatableEvent(visible, shouldAnimate)
144                 }
145                 .toAnimatedValueFlow()
146                 .dumpWhileCollecting("shouldShowEmptyShadeViewAnimated")
147                 .flowOn(bgDispatcher)
148         }
149     }
150 
151     /**
152      * Whether the footer should not be visible for the user, even if it's present in the list (as
153      * per [shouldIncludeFooterView] below).
154      *
155      * This essentially corresponds to having the view set to INVISIBLE.
156      */
157     val shouldHideFooterView: Flow<Boolean> by lazy {
158         SceneContainerFlag.assertInLegacyMode()
159         // When the shade is closed, the footer is still present in the list, but not visible.
160         // This prevents the footer from being shown when a HUN is present, while still allowing
161         // the footer to be counted as part of the shade for measurements.
162         shadeInteractor.shadeExpansion
163             .map { it == 0f }
164             .distinctUntilChanged()
165             .dumpWhileCollecting("shouldHideFooterView")
166             .flowOn(bgDispatcher)
167     }
168 
169     /**
170      * Whether the footer should be part of the list or not, and whether the transition from one
171      * state to another should be animated. This essentially corresponds to transitioning the view
172      * visibility from VISIBLE to GONE and vice versa.
173      *
174      * Note that this value being true doesn't necessarily mean that the footer is visible. It could
175      * be hidden by another condition (see [shouldHideFooterView] above).
176      */
177     val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy {
178         SceneContainerFlag.assertInLegacyMode()
179         combine(
180                 activeNotificationsInteractor.areAnyNotificationsPresent,
181                 userSetupInteractor.isUserSetUp,
182                 notificationStackInteractor.isShowingOnLockscreen,
183                 shadeInteractor.isQsFullscreen,
184                 remoteInputInteractor.isRemoteInputActive,
185             ) {
186                 hasNotifications,
187                 isUserSetUp,
188                 isShowingOnLockscreen,
189                 qsFullScreen,
190                 isRemoteInputActive ->
191                 when {
192                     !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
193                     // Hide the footer until the user setup is complete, to prevent access
194                     // to settings (b/193149550).
195                     !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
196                     // Do not show the footer if the lockscreen is visible (incl. AOD),
197                     // except if the shade is opened on top. See also b/219680200.
198                     // Do not animate, as that makes the footer appear briefly when
199                     // transitioning between the shade and keyguard.
200                     isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION
201                     // Do not show the footer if quick settings are fully expanded (except
202                     // for the foldable split shade view). See b/201427195 && b/222699879.
203                     qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
204                     // Hide the footer if remote input is active (i.e. user is replying to a
205                     // notification). See b/75984847.
206                     isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
207                     else -> VisibilityChange.APPEAR_WITH_ANIMATION
208                 }
209             }
210             .distinctUntilChanged(
211                 // Equivalent unless visibility changes
212                 areEquivalent = { a: VisibilityChange, b: VisibilityChange ->
213                     a.visible == b.visible
214                 }
215             )
216             // Should we animate the visibility change?
217             .sample(
218                 // TODO(b/322167853): This check is currently duplicated in FooterViewModel,
219                 //  but instead it should be a field in ShadeAnimationInteractor.
220                 combine(
221                         shadeInteractor.isShadeFullyExpanded,
222                         shadeInteractor.isShadeTouchable,
223                         ::Pair,
224                     )
225                     .onStart { emit(Pair(false, false)) }
226             ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
227                 // Animate if the shade is interactive, but NOT on the lockscreen. Having
228                 // animations enabled while on the lockscreen makes the footer appear briefly
229                 // when transitioning between the shade and keyguard.
230                 val shouldAnimate =
231                     isShadeFullyExpanded && animationsEnabled && visibilityChange.canAnimate
232                 AnimatableEvent(visibilityChange.visible, shouldAnimate)
233             }
234             .toAnimatedValueFlow()
235             .dumpWhileCollecting("shouldIncludeFooterView")
236             .flowOn(bgDispatcher)
237     }
238 
239     // This flow replaces shouldHideFooterView+shouldIncludeFooterView in flexiglass.
240     val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy {
241         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
242             flowOf(AnimatedValue.NotAnimating(false))
243         } else {
244             combine(
245                     activeNotificationsInteractor.areAnyNotificationsPresent,
246                     userSetupInteractor.isUserSetUp,
247                     notificationStackInteractor.isShowingOnLockscreen,
248                     shadeInteractor.isQsFullscreen,
249                     remoteInputInteractor.isRemoteInputActive,
250                     shadeInteractor.shadeExpansion.map { it < 0.5f }.distinctUntilChanged(),
251                 ) {
252                     hasNotifications,
253                     isUserSetUp,
254                     isShowingOnLockscreen,
255                     qsFullScreen,
256                     isRemoteInputActive,
257                     shadeLessThanHalfwayExpanded ->
258                     when {
259                         !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
260                         // Hide the footer until the user setup is complete, to prevent access
261                         // to settings (b/193149550).
262                         !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
263                         // Do not show the footer if the lockscreen is visible (incl. AOD),
264                         // except if the shade is opened on top. See also b/219680200.
265                         // Do not animate, as that makes the footer appear briefly when
266                         // transitioning between the shade and keyguard.
267                         isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION
268                         // Do not show the footer if quick settings are fully expanded (except
269                         // for the foldable split shade view). See b/201427195 && b/222699879.
270                         qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
271                         // Hide the footer if remote input is active (i.e. user is replying to a
272                         // notification). See b/75984847.
273                         isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
274                         // If the shade is not expanded enough, the footer shouldn't be visible.
275                         shadeLessThanHalfwayExpanded -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
276                         else -> VisibilityChange.APPEAR_WITH_ANIMATION
277                     }
278                 }
279                 .distinctUntilChanged(
280                     // Equivalent unless visibility changes
281                     areEquivalent = { a: VisibilityChange, b: VisibilityChange ->
282                         a.visible == b.visible
283                     }
284                 )
285                 // Should we animate the visibility change?
286                 .sample(
287                     // TODO(b/322167853): This check is currently duplicated in FooterViewModel,
288                     //  but instead it should be a field in ShadeAnimationInteractor.
289                     combine(
290                             shadeInteractor.isShadeFullyExpanded,
291                             shadeInteractor.isShadeTouchable,
292                             ::Pair,
293                         )
294                         .onStart { emit(Pair(false, false)) }
295                 ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
296                     // Animate if the shade is interactive, but NOT on the lockscreen. Having
297                     // animations enabled while on the lockscreen makes the footer appear briefly
298                     // when transitioning between the shade and keyguard.
299                     val shouldAnimate =
300                         isShadeFullyExpanded && animationsEnabled && visibilityChange.canAnimate
301                     AnimatableEvent(visibilityChange.visible, shouldAnimate)
302                 }
303                 .toAnimatedValueFlow()
304                 .dumpWhileCollecting("shouldShowFooterView")
305                 .flowOn(bgDispatcher)
306         }
307     }
308 
309     enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) {
310         DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false),
311         DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true),
312         APPEAR_WITH_ANIMATION(visible = true, canAnimate = true),
313     }
314 
315     val hasClearableAlertingNotifications: Flow<Boolean> =
316         activeNotificationsInteractor.hasClearableAlertingNotifications.dumpWhileCollecting(
317             "hasClearableAlertingNotifications"
318         )
319 
320     val hasNonClearableSilentNotifications: Flow<Boolean> =
321         activeNotificationsInteractor.hasNonClearableSilentNotifications.dumpWhileCollecting(
322             "hasNonClearableSilentNotifications"
323         )
324 
325     val topHeadsUpRow: Flow<HeadsUpRowKey?> by lazy {
326         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
327             flowOf(null)
328         } else {
329             headsUpNotificationInteractor.topHeadsUpRow.dumpWhileCollecting("topHeadsUpRow")
330         }
331     }
332 
333     val activeHeadsUpRowKeys: Flow<Set<HeadsUpRowKey>> by lazy {
334         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
335             flowOf(emptySet())
336         } else {
337             headsUpNotificationInteractor.activeHeadsUpRowKeys.dumpWhileCollecting(
338                 "pinnedHeadsUpRows"
339             )
340         }
341     }
342 
343     val pinnedHeadsUpRowKeys: Flow<Set<HeadsUpRowKey>> by lazy {
344         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
345             flowOf(emptySet())
346         } else {
347             headsUpNotificationInteractor.pinnedHeadsUpRowKeys.dumpWhileCollecting(
348                 "pinnedHeadsUpRows"
349             )
350         }
351     }
352 
353     val headsUpAnimationsEnabled: Flow<Boolean> by lazy {
354         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
355             flowOf(false)
356         } else {
357             flowOf(true).dumpWhileCollecting("headsUpAnimationsEnabled")
358         }
359     }
360 
361     val hasPinnedHeadsUpRow: Flow<Boolean> by lazy {
362         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
363             flowOf(false)
364         } else {
365             headsUpNotificationInteractor.hasPinnedRows.dumpWhileCollecting("hasPinnedHeadsUpRow")
366         }
367     }
368 
369     /**
370      * A list of keys for the visible status bar chips.
371      *
372      * Note that this list can contain both notification keys, as well as keys for other types of
373      * chips like screen recording.
374      */
375     val visibleStatusBarChipKeys = ongoingActivityChipsViewModel.visibleChipKeys
376 
377     // TODO(b/325936094) use it for the text displayed in the StatusBar
378     fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowViewModel =
379         HeadsUpRowViewModel(headsUpNotificationInteractor.headsUpRow(key))
380 
381     fun elementKeyFor(key: HeadsUpRowKey): Any = headsUpNotificationInteractor.elementKeyFor(key)
382 
383     fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
384         headsUpNotificationInteractor.setHeadsUpAnimatingAway(animatingAway)
385     }
386 }
387