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 18 @file:OptIn(ExperimentalCoroutinesApi::class) 19 20 package com.android.systemui.statusbar.notification.stack.ui.viewmodel 21 22 import com.android.compose.animation.scene.ContentKey 23 import com.android.compose.animation.scene.ObservableTransitionState 24 import com.android.compose.animation.scene.ObservableTransitionState.Idle 25 import com.android.compose.animation.scene.ObservableTransitionState.Transition 26 import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeScene 27 import com.android.compose.animation.scene.OverlayKey 28 import com.android.compose.animation.scene.SceneKey 29 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor 30 import com.android.systemui.dump.DumpManager 31 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 32 import com.android.systemui.lifecycle.ExclusiveActivatable 33 import com.android.systemui.scene.domain.interactor.SceneInteractor 34 import com.android.systemui.scene.shared.flag.SceneContainerFlag 35 import com.android.systemui.scene.shared.model.Overlays 36 import com.android.systemui.scene.shared.model.Scenes 37 import com.android.systemui.shade.domain.interactor.ShadeInteractor 38 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor 39 import com.android.systemui.shade.shared.model.ShadeMode 40 import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor 41 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor 42 import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent 43 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimClipping 44 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape 45 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrollState 46 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_DELAYED_STACK_FADE_IN 47 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA 48 import com.android.systemui.util.kotlin.ActivatableFlowDumper 49 import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl 50 import dagger.Lazy 51 import dagger.assisted.AssistedFactory 52 import dagger.assisted.AssistedInject 53 import kotlinx.coroutines.ExperimentalCoroutinesApi 54 import kotlinx.coroutines.flow.Flow 55 import kotlinx.coroutines.flow.MutableStateFlow 56 import kotlinx.coroutines.flow.StateFlow 57 import kotlinx.coroutines.flow.combine 58 import kotlinx.coroutines.flow.combineTransform 59 import kotlinx.coroutines.flow.distinctUntilChanged 60 import kotlinx.coroutines.flow.flatMapLatest 61 import kotlinx.coroutines.flow.flowOf 62 import kotlinx.coroutines.flow.map 63 import kotlinx.coroutines.flow.mapNotNull 64 65 private typealias ShadeScrimShapeConsumer = (ShadeScrimShape?) -> Unit 66 67 /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */ 68 class NotificationScrollViewModel 69 @AssistedInject 70 constructor( 71 dumpManager: DumpManager, 72 private val stackAppearanceInteractor: NotificationStackAppearanceInteractor, 73 shadeInteractor: ShadeInteractor, 74 shadeModeInteractor: ShadeModeInteractor, 75 bouncerInteractor: BouncerInteractor, 76 private val remoteInputInteractor: RemoteInputInteractor, 77 private val sceneInteractor: SceneInteractor, 78 // TODO(b/336364825) Remove Lazy when SceneContainerFlag is released - 79 // while the flag is off, creating this object too early results in a crash 80 keyguardInteractor: Lazy<KeyguardInteractor>, 81 ) : 82 ActivatableFlowDumper by ActivatableFlowDumperImpl(dumpManager, "NotificationScrollViewModel"), 83 ExclusiveActivatable() { 84 85 override suspend fun onActivated(): Nothing { 86 activateFlowDumper() 87 } 88 89 private fun expandedInScene(scene: SceneKey): Boolean { 90 return when (scene) { 91 Scenes.Lockscreen, 92 Scenes.Shade, 93 Scenes.QuickSettings -> true 94 else -> false 95 } 96 } 97 98 private fun fullyExpandedDuringSceneChange(change: ChangeScene): Boolean { 99 // The lockscreen stack is visible during all transitions away from the lockscreen, so keep 100 // the stack expanded until those transitions finish. 101 return if (change.isTransitioning(from = Scenes.Lockscreen)) { 102 true 103 } else if (change.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen)) { 104 false 105 } else { 106 (expandedInScene(change.fromScene) && expandedInScene(change.toScene)) 107 } 108 } 109 110 private fun expandFractionDuringSceneChange( 111 change: ChangeScene, 112 shadeExpansion: Float, 113 qsExpansion: Float, 114 ): Float { 115 return if (fullyExpandedDuringSceneChange(change)) { 116 1f 117 } else if ( 118 change.isTransitioningBetween(Scenes.Gone, Scenes.Shade) || 119 change.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) 120 ) { 121 shadeExpansion 122 } else if (change.isTransitioningBetween(Scenes.Gone, Scenes.QuickSettings)) { 123 // during QS expansion, increase fraction at same rate as scrim alpha, 124 // but start when scrim alpha is at EXPANSION_FOR_DELAYED_STACK_FADE_IN. 125 (qsExpansion / EXPANSION_FOR_MAX_SCRIM_ALPHA - EXPANSION_FOR_DELAYED_STACK_FADE_IN) 126 .coerceIn(0f, 1f) 127 } else { 128 // TODO(b/356596436): If notification shade overlay is open, we'll reach this point and 129 // the expansion fraction in that case should be `shadeExpansion`. 130 0f 131 } 132 } 133 134 private fun expandFractionDuringOverlayTransition( 135 transition: Transition, 136 currentScene: SceneKey, 137 currentOverlays: Set<OverlayKey>, 138 shadeExpansion: Float, 139 ): Float { 140 return if (currentScene == Scenes.Lockscreen) { 141 1f 142 } else if (transition.isTransitioningFromOrTo(Overlays.NotificationsShade)) { 143 shadeExpansion 144 } else if (Overlays.NotificationsShade in currentOverlays) { 145 1f 146 } else { 147 0f 148 } 149 } 150 151 /** Are notification stack height updates suppressed? */ 152 val suppressHeightUpdates: Flow<Boolean> = 153 sceneInteractor.transitionState.map { transition: ObservableTransitionState -> 154 transition is Transition && 155 transition.fromContent == Scenes.Lockscreen && 156 (transition.toContent == Overlays.Bouncer || transition.toContent == Scenes.Gone) 157 } 158 159 /** 160 * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning 161 * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while 162 * transitioning from Shade to QuickSettings scenes. 163 */ 164 val expandFraction: Flow<Float> = 165 combine( 166 shadeInteractor.shadeExpansion, 167 shadeInteractor.qsExpansion, 168 shadeModeInteractor.shadeMode, 169 sceneInteractor.transitionState, 170 sceneInteractor.currentOverlays, 171 ) { shadeExpansion, qsExpansion, _, transitionState, currentOverlays -> 172 when (transitionState) { 173 is Idle -> 174 if ( 175 expandedInScene(transitionState.currentScene) || 176 Overlays.NotificationsShade in currentOverlays 177 ) { 178 1f 179 } else { 180 0f 181 } 182 is ChangeScene -> 183 expandFractionDuringSceneChange( 184 change = transitionState, 185 shadeExpansion = shadeExpansion, 186 qsExpansion = qsExpansion, 187 ) 188 is Transition.ShowOrHideOverlay -> 189 expandFractionDuringOverlayTransition( 190 transition = transitionState, 191 currentScene = transitionState.currentScene, 192 currentOverlays = currentOverlays, 193 shadeExpansion = shadeExpansion, 194 ) 195 is Transition.ReplaceOverlay -> 196 expandFractionDuringOverlayTransition( 197 transition = transitionState, 198 currentScene = transitionState.currentScene, 199 currentOverlays = currentOverlays, 200 shadeExpansion = shadeExpansion, 201 ) 202 } 203 } 204 .distinctUntilChanged() 205 .dumpWhileCollecting("expandFraction") 206 207 val qsExpandFraction: Flow<Float> = 208 shadeInteractor.qsExpansion.dumpWhileCollecting("qsExpandFraction") 209 210 val isOccluded: Flow<Boolean> = 211 bouncerInteractor.bouncerExpansion 212 .map { it == 1f } 213 .distinctUntilChanged() 214 .dumpWhileCollecting("isOccluded") 215 216 /** Blur radius to be applied to Notifications. */ 217 fun blurRadius(maxBlurRadius: Flow<Int>) = 218 combine(blurFraction, maxBlurRadius) { fraction, maxRadius -> fraction * maxRadius } 219 220 /** 221 * Scale of the blur effect that should be applied to Notifications. 222 * 223 * 0 -> don't blur (default, removes all blur render effects) 1 -> do the full blur (apply a 224 * render effect with the max blur radius) 225 */ 226 private val blurFraction: Flow<Float> = 227 if (SceneContainerFlag.isEnabled) { 228 shadeModeInteractor.shadeMode.flatMapLatest { shadeMode -> 229 when (shadeMode) { 230 ShadeMode.Dual -> 231 combineTransform( 232 shadeInteractor.shadeExpansion, 233 shadeInteractor.qsExpansion, 234 ) { notificationShadeExpansion, qsExpansion -> 235 if (notificationShadeExpansion == 0f) { 236 // Blur out notifications as the QS overlay panel expands 237 emit(qsExpansion) 238 } 239 } 240 else -> flowOf(0f) 241 } 242 } 243 } else { 244 flowOf(0f) 245 } 246 247 /** Whether we should close any open notification guts. */ 248 val shouldCloseGuts: Flow<Boolean> = stackAppearanceInteractor.shouldCloseGuts 249 250 /** Whether the Notification Stack is visibly on the lockscreen scene. */ 251 val isShowingStackOnLockscreen: Flow<Boolean> = 252 sceneInteractor.transitionState 253 .mapNotNull { state -> 254 state.isIdle(Scenes.Lockscreen) || 255 state.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) 256 } 257 .distinctUntilChanged() 258 259 /** The alpha of the Notification Stack for lockscreen fade-in */ 260 val alphaForLockscreenFadeIn = stackAppearanceInteractor.alphaForLockscreenFadeIn 261 262 private operator fun SceneKey.contains(scene: SceneKey) = 263 sceneInteractor.isSceneInFamily(scene, this) 264 265 private val qsAllowsClipping: Flow<Boolean> = 266 combine(shadeModeInteractor.shadeMode, shadeInteractor.qsExpansion) { shadeMode, qsExpansion 267 -> 268 when (shadeMode) { 269 is ShadeMode.Dual, 270 is ShadeMode.Split -> true 271 is ShadeMode.Single -> qsExpansion < 0.5f 272 } 273 } 274 .distinctUntilChanged() 275 276 /** The bounds of the notification stack in the current scene. */ 277 private val shadeScrimClipping: Flow<ShadeScrimClipping?> = 278 combine( 279 qsAllowsClipping, 280 stackAppearanceInteractor.notificationShadeScrimBounds, 281 stackAppearanceInteractor.shadeScrimRounding, 282 ) { qsAllowsClipping, bounds, rounding -> 283 bounds?.takeIf { qsAllowsClipping }?.let { ShadeScrimClipping(it, rounding) } 284 } 285 .distinctUntilChanged() 286 .dumpWhileCollecting("stackClipping") 287 288 fun notificationScrimShape( 289 cornerRadius: Flow<Int>, 290 viewLeftOffset: Flow<Int>, 291 ): Flow<ShadeScrimShape?> = 292 combine(shadeScrimClipping, cornerRadius, viewLeftOffset) { clipping, radius, leftOffset -> 293 if (clipping == null) return@combine null 294 ShadeScrimShape( 295 bounds = clipping.bounds.minus(leftOffset = leftOffset), 296 topRadius = radius.takeIf { clipping.rounding.isTopRounded } ?: 0, 297 bottomRadius = radius.takeIf { clipping.rounding.isBottomRounded } ?: 0, 298 ) 299 } 300 .dumpWhileCollecting("shadeScrimShape") 301 302 /** 303 * Sets a consumer to be notified when the QuickSettings Overlay panel changes size or position. 304 */ 305 fun setQsScrimShapeConsumer(consumer: ShadeScrimShapeConsumer?) { 306 stackAppearanceInteractor.setQsPanelShapeConsumer(consumer) 307 } 308 309 /** 310 * Max alpha to apply directly to the view based on the compose placeholder. 311 * 312 * TODO(b/338590620): Migrate alphas from [SharedNotificationContainerViewModel] into this flow 313 */ 314 val maxAlpha: Flow<Float> = 315 stackAppearanceInteractor.alphaForBrightnessMirror.dumpValue("maxAlpha") 316 317 /** Scroll state of the notification shade. */ 318 val shadeScrollState: Flow<ShadeScrollState> = stackAppearanceInteractor.shadeScrollState 319 320 /** Receives the amount (px) that the stack should scroll due to internal expansion. */ 321 val syntheticScrollConsumer: (Float) -> Unit = stackAppearanceInteractor::setSyntheticScroll 322 323 /** Receives an event to scroll the stack up or down. */ 324 val accessibilityScrollEventConsumer: (AccessibilityScrollEvent) -> Unit = 325 stackAppearanceInteractor::sendAccessibilityScrollEvent 326 327 /** 328 * Receives whether the current touch gesture is overscroll as it has already been consumed by 329 * the stack. 330 */ 331 val currentGestureOverscrollConsumer: (Boolean) -> Unit = 332 stackAppearanceInteractor::setCurrentGestureOverscroll 333 334 /** Receives whether the current touch gesture is inside any open guts. */ 335 val currentGestureInGutsConsumer: (Boolean) -> Unit = 336 stackAppearanceInteractor::setCurrentGestureInGuts 337 338 /** Receives the bottom bound of the currently focused remote input notification row. */ 339 val remoteInputRowBottomBoundConsumer: (Float?) -> Unit = 340 remoteInputInteractor::setRemoteInputRowBottomBound 341 342 /** Whether the notification stack is scrollable or not. */ 343 val isScrollable: Flow<Boolean> = 344 combine(sceneInteractor.currentScene, sceneInteractor.currentOverlays) { 345 currentScene, 346 currentOverlays -> 347 currentScene.showsNotifications() || currentOverlays.any { it.showsNotifications() } 348 } 349 .dumpWhileCollecting("isScrollable") 350 351 /** Whether the notification stack is displayed in doze mode. */ 352 val isDozing: Flow<Boolean> by lazy { 353 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { 354 flowOf(false) 355 } else { 356 keyguardInteractor.get().isDozing.dumpWhileCollecting("isDozing") 357 } 358 } 359 360 /** Whether the notification stack is displayed in pulsing mode. */ 361 val isPulsing: Flow<Boolean> by lazy { 362 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { 363 flowOf(false) 364 } else { 365 keyguardInteractor.get().isPulsing.dumpWhileCollecting("isPulsing") 366 } 367 } 368 369 val shouldAnimatePulse: StateFlow<Boolean> by lazy { 370 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { 371 MutableStateFlow(false) 372 } else { 373 keyguardInteractor.get().isAodAvailable 374 } 375 } 376 377 private fun ContentKey.showsNotifications(): Boolean { 378 return when (this) { 379 Overlays.NotificationsShade, 380 Scenes.Lockscreen, 381 Scenes.Shade -> true 382 else -> false 383 } 384 } 385 386 @AssistedFactory 387 interface Factory { 388 fun create(): NotificationScrollViewModel 389 } 390 } 391