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