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.shade.domain.interactor.ShadeInteractor
22 import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
23 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
24 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
25 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
26 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
27 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
28 import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
29 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
30 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
31 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor
32 import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
33 import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
34 import com.android.systemui.util.kotlin.FlowDumperImpl
35 import com.android.systemui.util.kotlin.sample
36 import com.android.systemui.util.ui.AnimatableEvent
37 import com.android.systemui.util.ui.AnimatedValue
38 import com.android.systemui.util.ui.toAnimatedValueFlow
39 import java.util.Optional
40 import javax.inject.Inject
41 import kotlinx.coroutines.CoroutineDispatcher
42 import kotlinx.coroutines.flow.Flow
43 import kotlinx.coroutines.flow.combine
44 import kotlinx.coroutines.flow.distinctUntilChanged
45 import kotlinx.coroutines.flow.flowOf
46 import kotlinx.coroutines.flow.flowOn
47 import kotlinx.coroutines.flow.map
48 import kotlinx.coroutines.flow.onStart
49 
50 /** ViewModel for the list of notifications. */
51 class NotificationListViewModel
52 @Inject
53 constructor(
54     val shelf: NotificationShelfViewModel,
55     val hideListViewModel: HideListViewModel,
56     val footer: Optional<FooterViewModel>,
57     val logger: Optional<NotificationLoggerViewModel>,
58     activeNotificationsInteractor: ActiveNotificationsInteractor,
59     notificationStackInteractor: NotificationStackInteractor,
60     private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
61     remoteInputInteractor: RemoteInputInteractor,
62     seenNotificationsInteractor: SeenNotificationsInteractor,
63     shadeInteractor: ShadeInteractor,
64     userSetupInteractor: UserSetupInteractor,
65     zenModeInteractor: ZenModeInteractor,
66     @Background bgDispatcher: CoroutineDispatcher,
67     dumpManager: DumpManager,
68 ) : FlowDumperImpl(dumpManager) {
69     /**
70      * We want the NSSL to be unimportant for accessibility when there are no notifications in it
71      * while the device is on lock screen, to avoid an unlabelled NSSL view in TalkBack. Otherwise,
72      * we want it to be important for accessibility to enable accessibility auto-scrolling in NSSL.
73      * See b/242235264 for more details.
74      */
75     val isImportantForAccessibility: Flow<Boolean> by lazy {
76         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
77             flowOf(true)
78         } else {
79             combine(
80                     activeNotificationsInteractor.areAnyNotificationsPresent,
81                     notificationStackInteractor.isShowingOnLockscreen,
82                 ) { hasNotifications, isShowingOnLockscreen ->
83                     hasNotifications || !isShowingOnLockscreen
84                 }
85                 .distinctUntilChanged()
86                 .dumpWhileCollecting("isImportantForAccessibility")
87                 .flowOn(bgDispatcher)
88         }
89     }
90 
91     val shouldShowEmptyShadeView: Flow<Boolean> by lazy {
92         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
93             flowOf(false)
94         } else {
95             combine(
96                     activeNotificationsInteractor.areAnyNotificationsPresent,
97                     shadeInteractor.isQsFullscreen,
98                     notificationStackInteractor.isShowingOnLockscreen,
99                 ) { hasNotifications, isQsFullScreen, isShowingOnLockscreen ->
100                     when {
101                         hasNotifications -> false
102                         isQsFullScreen -> false
103                         // Do not show the empty shade if the lockscreen is visible (including AOD
104                         // b/228790482 and bouncer b/267060171), except if the shade is opened on
105                         // top.
106                         isShowingOnLockscreen -> false
107                         else -> true
108                     }
109                 }
110                 .distinctUntilChanged()
111                 .dumpWhileCollecting("shouldShowEmptyShadeView")
112                 .flowOn(bgDispatcher)
113         }
114     }
115 
116     /**
117      * Whether the footer should not be visible for the user, even if it's present in the list (as
118      * per [shouldIncludeFooterView] below).
119      *
120      * This essentially corresponds to having the view set to INVISIBLE.
121      */
122     val shouldHideFooterView: Flow<Boolean> by lazy {
123         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
124             flowOf(false)
125         } else {
126             // When the shade is closed, the footer is still present in the list, but not visible.
127             // This prevents the footer from being shown when a HUN is present, while still allowing
128             // the footer to be counted as part of the shade for measurements.
129             shadeInteractor.shadeExpansion
130                 .map { it == 0f }
131                 .distinctUntilChanged()
132                 .dumpWhileCollecting("shouldHideFooterView")
133                 .flowOn(bgDispatcher)
134         }
135     }
136 
137     /**
138      * Whether the footer should be part of the list or not, and whether the transition from one
139      * state to another should be animated. This essentially corresponds to transitioning the view
140      * visibility from VISIBLE to GONE and vice versa.
141      *
142      * Note that this value being true doesn't necessarily mean that the footer is visible. It could
143      * be hidden by another condition (see [shouldHideFooterView] above).
144      */
145     val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy {
146         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
147             flowOf(AnimatedValue.NotAnimating(false))
148         } else {
149             combine(
150                     activeNotificationsInteractor.areAnyNotificationsPresent,
151                     userSetupInteractor.isUserSetUp,
152                     notificationStackInteractor.isShowingOnLockscreen,
153                     shadeInteractor.isQsFullscreen,
154                     remoteInputInteractor.isRemoteInputActive
155                 ) {
156                     hasNotifications,
157                     isUserSetUp,
158                     isShowingOnLockscreen,
159                     qsFullScreen,
160                     isRemoteInputActive ->
161                     when {
162                         !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
163                         // Hide the footer until the user setup is complete, to prevent access
164                         // to settings (b/193149550).
165                         !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
166                         // Do not show the footer if the lockscreen is visible (incl. AOD),
167                         // except if the shade is opened on top. See also b/219680200.
168                         // Do not animate, as that makes the footer appear briefly when
169                         // transitioning between the shade and keyguard.
170                         isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION
171                         // Do not show the footer if quick settings are fully expanded (except
172                         // for the foldable split shade view). See b/201427195 && b/222699879.
173                         qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
174                         // Hide the footer if remote input is active (i.e. user is replying to a
175                         // notification). See b/75984847.
176                         isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
177                         else -> VisibilityChange.APPEAR_WITH_ANIMATION
178                     }
179                 }
180                 .distinctUntilChanged(
181                     // Equivalent unless visibility changes
182                     areEquivalent = { a: VisibilityChange, b: VisibilityChange ->
183                         a.visible == b.visible
184                     }
185                 )
186                 // Should we animate the visibility change?
187                 .sample(
188                     // TODO(b/322167853): This check is currently duplicated in FooterViewModel,
189                     //  but instead it should be a field in ShadeAnimationInteractor.
190                     combine(
191                             shadeInteractor.isShadeFullyExpanded,
192                             shadeInteractor.isShadeTouchable,
193                             ::Pair
194                         )
195                         .onStart { emit(Pair(false, false)) }
196                 ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
197                     // Animate if the shade is interactive, but NOT on the lockscreen. Having
198                     // animations enabled while on the lockscreen makes the footer appear briefly
199                     // when transitioning between the shade and keyguard.
200                     val shouldAnimate =
201                         isShadeFullyExpanded && animationsEnabled && visibilityChange.canAnimate
202                     AnimatableEvent(visibilityChange.visible, shouldAnimate)
203                 }
204                 .toAnimatedValueFlow()
205                 .dumpWhileCollecting("shouldIncludeFooterView")
206                 .flowOn(bgDispatcher)
207         }
208     }
209 
210     enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) {
211         DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false),
212         DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true),
213         APPEAR_WITH_ANIMATION(visible = true, canAnimate = true)
214     }
215 
216     // TODO(b/308591475): This should be tracked separately by the empty shade.
217     val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
218         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
219             flowOf(false)
220         } else {
221             zenModeInteractor.areNotificationsHiddenInShade.dumpWhileCollecting(
222                 "areNotificationsHiddenInShade"
223             )
224         }
225     }
226 
227     // TODO(b/308591475): This should be tracked separately by the empty shade.
228     val hasFilteredOutSeenNotifications: Flow<Boolean> by lazy {
229         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
230             flowOf(false)
231         } else {
232             seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpWhileCollecting(
233                 "hasFilteredOutSeenNotifications"
234             )
235         }
236     }
237 
238     val hasClearableAlertingNotifications: Flow<Boolean> by lazy {
239         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
240             flowOf(false)
241         } else {
242             activeNotificationsInteractor.hasClearableAlertingNotifications.dumpWhileCollecting(
243                 "hasClearableAlertingNotifications"
244             )
245         }
246     }
247 
248     val hasNonClearableSilentNotifications: Flow<Boolean> by lazy {
249         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
250             flowOf(false)
251         } else {
252             activeNotificationsInteractor.hasNonClearableSilentNotifications.dumpWhileCollecting(
253                 "hasNonClearableSilentNotifications"
254             )
255         }
256     }
257 
258     val topHeadsUpRow: Flow<HeadsUpRowKey?> by lazy {
259         if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
260             flowOf(null)
261         } else {
262             headsUpNotificationInteractor.topHeadsUpRow.dumpWhileCollecting("topHeadsUpRow")
263         }
264     }
265 
266     val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
267         if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
268             flowOf(emptySet())
269         } else {
270             headsUpNotificationInteractor.pinnedHeadsUpRows.dumpWhileCollecting("pinnedHeadsUpRows")
271         }
272     }
273 
274     val headsUpAnimationsEnabled: Flow<Boolean> by lazy {
275         if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
276             flowOf(false)
277         } else {
278             combine(
279                 notificationStackInteractor.isShowingOnLockscreen,
280                 shadeInteractor.isShadeFullyCollapsed
281             ) { (isKeyguardShowing, isShadeFullyCollapsed) ->
282                 !isKeyguardShowing && isShadeFullyCollapsed
283             }
284                 .dumpWhileCollecting("headsUpAnimationsEnabled")
285         }
286     }
287 
288     val hasPinnedHeadsUpRow: Flow<Boolean> by lazy {
289         if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
290             flowOf(false)
291         } else {
292             headsUpNotificationInteractor.hasPinnedRows.dumpWhileCollecting("hasPinnedHeadsUpRow")
293         }
294     }
295 
296     // TODO(b/325936094) use it for the text displayed in the StatusBar
297     fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowViewModel =
298         HeadsUpRowViewModel(headsUpNotificationInteractor.headsUpRow(key))
299 
300     fun elementKeyFor(key: HeadsUpRowKey): Any = headsUpNotificationInteractor.elementKeyFor(key)
301 
302     fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
303         headsUpNotificationInteractor.setHeadsUpAnimatingAway(animatingAway)
304     }
305 }
306