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