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 package com.android.systemui.statusbar.notification.stack.ui.viewmodel 19 20 import android.content.Context 21 import androidx.annotation.VisibleForTesting 22 import com.android.app.tracing.coroutines.flow.flowName 23 import com.android.systemui.Flags.glanceableHubV2 24 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor 25 import com.android.systemui.common.shared.model.NotificationContainerBounds 26 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor 27 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor 28 import com.android.systemui.communal.shared.model.CommunalScenes 29 import com.android.systemui.dagger.SysUISingleton 30 import com.android.systemui.dagger.qualifiers.Application 31 import com.android.systemui.dump.DumpManager 32 import com.android.systemui.kairos.awaitClose 33 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 34 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 35 import com.android.systemui.keyguard.shared.model.Edge 36 import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER 37 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD 38 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING 39 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING 40 import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB 41 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE 42 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN 43 import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED 44 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER 45 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE 46 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED 47 import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition 48 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel 49 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel 50 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel 51 import com.android.systemui.keyguard.ui.viewmodel.AodToGlanceableHubTransitionViewModel 52 import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel 53 import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel 54 import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel 55 import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel 56 import com.android.systemui.keyguard.ui.viewmodel.DozingToGlanceableHubTransitionViewModel 57 import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel 58 import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel 59 import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel 60 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel 61 import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToAodTransitionViewModel 62 import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel 63 import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel 64 import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel 65 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel 66 import com.android.systemui.keyguard.ui.viewmodel.GoneToLockscreenTransitionViewModel 67 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel 68 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel 69 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGoneTransitionViewModel 70 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel 71 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTransitionViewModel 72 import com.android.systemui.keyguard.ui.viewmodel.OccludedToAodTransitionViewModel 73 import com.android.systemui.keyguard.ui.viewmodel.OccludedToGoneTransitionViewModel 74 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel 75 import com.android.systemui.keyguard.ui.viewmodel.OffToLockscreenTransitionViewModel 76 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel 77 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel 78 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor 79 import com.android.systemui.res.R 80 import com.android.systemui.scene.shared.flag.SceneContainerFlag 81 import com.android.systemui.scene.shared.model.Overlays 82 import com.android.systemui.scene.shared.model.Scenes 83 import com.android.systemui.shade.LargeScreenHeaderHelper 84 import com.android.systemui.shade.ShadeDisplayAware 85 import com.android.systemui.shade.domain.interactor.ShadeInteractor 86 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor 87 import com.android.systemui.shade.shared.model.ShadeMode.Dual 88 import com.android.systemui.shade.shared.model.ShadeMode.Single 89 import com.android.systemui.shade.shared.model.ShadeMode.Split 90 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor 91 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor 92 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor 93 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor 94 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf 95 import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf 96 import com.android.systemui.util.kotlin.BooleanFlowOperators.not 97 import com.android.systemui.util.kotlin.FlowDumperImpl 98 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine 99 import com.android.systemui.util.kotlin.sample 100 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow 101 import dagger.Lazy 102 import javax.inject.Inject 103 import kotlinx.coroutines.CoroutineScope 104 import kotlinx.coroutines.ExperimentalCoroutinesApi 105 import kotlinx.coroutines.currentCoroutineContext 106 import kotlinx.coroutines.flow.Flow 107 import kotlinx.coroutines.flow.SharingStarted 108 import kotlinx.coroutines.flow.StateFlow 109 import kotlinx.coroutines.flow.combine 110 import kotlinx.coroutines.flow.combineTransform 111 import kotlinx.coroutines.flow.distinctUntilChanged 112 import kotlinx.coroutines.flow.emptyFlow 113 import kotlinx.coroutines.flow.filterNotNull 114 import kotlinx.coroutines.flow.first 115 import kotlinx.coroutines.flow.flatMapLatest 116 import kotlinx.coroutines.flow.flow 117 import kotlinx.coroutines.flow.map 118 import kotlinx.coroutines.flow.merge 119 import kotlinx.coroutines.flow.onStart 120 import kotlinx.coroutines.flow.stateIn 121 import kotlinx.coroutines.flow.transformWhile 122 import kotlinx.coroutines.isActive 123 124 /** View-model for the shared notification container, used by both the shade and keyguard spaces */ 125 @OptIn(ExperimentalCoroutinesApi::class) 126 @SysUISingleton 127 class SharedNotificationContainerViewModel 128 @Inject 129 constructor( 130 private val interactor: SharedNotificationContainerInteractor, 131 dumpManager: DumpManager, 132 @Application applicationScope: CoroutineScope, 133 @ShadeDisplayAware private val context: Context, 134 @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, 135 private val keyguardInteractor: KeyguardInteractor, 136 private val keyguardTransitionInteractor: KeyguardTransitionInteractor, 137 private val shadeInteractor: ShadeInteractor, 138 private val bouncerInteractor: BouncerInteractor, 139 shadeModeInteractor: ShadeModeInteractor, 140 notificationStackAppearanceInteractor: NotificationStackAppearanceInteractor, 141 private val alternateBouncerToGoneTransitionViewModel: 142 AlternateBouncerToGoneTransitionViewModel, 143 private val alternateBouncerToPrimaryBouncerTransitionViewModel: 144 AlternateBouncerToPrimaryBouncerTransitionViewModel, 145 private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel, 146 private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, 147 private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, 148 private val aodToGlanceableHubTransitionViewModel: AodToGlanceableHubTransitionViewModel, 149 private val aodToPrimaryBouncerTransitionViewModel: AodToPrimaryBouncerTransitionViewModel, 150 dozingToGlanceableHubTransitionViewModel: DozingToGlanceableHubTransitionViewModel, 151 private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, 152 private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel, 153 private val dozingToPrimaryBouncerTransitionViewModel: 154 DozingToPrimaryBouncerTransitionViewModel, 155 private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, 156 private val glanceableHubToLockscreenTransitionViewModel: 157 GlanceableHubToLockscreenTransitionViewModel, 158 private val glanceableHubToAodTransitionViewModel: GlanceableHubToAodTransitionViewModel, 159 private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel, 160 private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel, 161 private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel, 162 private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel, 163 private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel, 164 private val lockscreenToGlanceableHubTransitionViewModel: 165 LockscreenToGlanceableHubTransitionViewModel, 166 private val lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel, 167 private val lockscreenToPrimaryBouncerTransitionViewModel: 168 LockscreenToPrimaryBouncerTransitionViewModel, 169 private val lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel, 170 private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel, 171 private val occludedToGoneTransitionViewModel: OccludedToGoneTransitionViewModel, 172 private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, 173 private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel, 174 private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, 175 private val primaryBouncerToLockscreenTransitionViewModel: 176 PrimaryBouncerToLockscreenTransitionViewModel, 177 private val primaryBouncerTransitions: Set<@JvmSuppressWildcards PrimaryBouncerTransition>, 178 aodBurnInViewModel: AodBurnInViewModel, 179 private val communalSceneInteractor: CommunalSceneInteractor, 180 // Lazy because it's only used in the SceneContainer + Dual Shade configuration. 181 headsUpNotificationInteractor: Lazy<HeadsUpNotificationInteractor>, 182 private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, 183 unfoldTransitionInteractor: UnfoldTransitionInteractor, 184 ) : FlowDumperImpl(dumpManager) { 185 186 /** 187 * Is either shade/qs expanded? This intentionally does not use the [ShadeInteractor] version, 188 * as the legacy implementation has extra logic that produces incorrect results. 189 */ 190 private val isAnyExpanded = 191 combine( 192 shadeInteractor.shadeExpansion.map { it > 0f }, 193 shadeInteractor.qsExpansion.map { it > 0f }, 194 ) { shadeExpansion, qsExpansion -> 195 shadeExpansion || qsExpansion 196 } 197 .flowName("isAnyExpanded") 198 .stateIn( 199 scope = applicationScope, 200 started = SharingStarted.Eagerly, 201 initialValue = false, 202 ) 203 204 /** 205 * Shade locked is a legacy concept, but necessary to mimic current functionality. Listen for 206 * both SHADE_LOCKED and shade/qs expansion in order to determine lock state, as one can arrive 207 * before the other. 208 */ 209 private val isShadeLocked: Flow<Boolean> = 210 combine(keyguardInteractor.statusBarState.map { it == SHADE_LOCKED }, isAnyExpanded) { 211 isShadeLocked, 212 isAnyExpanded -> 213 isShadeLocked && isAnyExpanded 214 } 215 .flowName("isShadeLocked") 216 .stateIn( 217 scope = applicationScope, 218 started = SharingStarted.Eagerly, 219 initialValue = false, 220 ) 221 .dumpWhileCollecting("isShadeLocked") 222 223 @VisibleForTesting 224 val paddingTopDimen: Flow<Int> = 225 if (SceneContainerFlag.isEnabled) { 226 configurationInteractor.onAnyConfigurationChange.map { 227 with(context.resources) { 228 val useLargeScreenHeader = 229 getBoolean(R.bool.config_use_large_screen_shade_header) 230 if (useLargeScreenHeader) { 231 largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() 232 } else { 233 getDimensionPixelSize(R.dimen.notification_panel_margin_top) 234 } 235 } 236 } 237 } else { 238 interactor.configurationBasedDimensions.map { 239 when { 240 it.useLargeScreenHeader -> it.marginTopLargeScreen 241 else -> it.marginTop 242 } 243 } 244 } 245 .distinctUntilChanged() 246 .dumpWhileCollecting("paddingTopDimen") 247 248 val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = 249 if (SceneContainerFlag.isEnabled) { 250 combine( 251 shadeModeInteractor.isShadeLayoutWide, 252 shadeModeInteractor.shadeMode, 253 configurationInteractor.onAnyConfigurationChange, 254 ) { isShadeLayoutWide, shadeMode, _ -> 255 with(context.resources) { 256 val marginHorizontal = 257 getDimensionPixelSize( 258 if (shadeMode is Dual) { 259 R.dimen.shade_panel_margin_horizontal 260 } else { 261 R.dimen.notification_panel_margin_horizontal 262 } 263 ) 264 265 val horizontalPosition = 266 when (shadeMode) { 267 Single -> HorizontalPosition.EdgeToEdge 268 Split -> HorizontalPosition.MiddleToEdge(ratio = 0.5f) 269 Dual -> 270 if (isShadeLayoutWide) { 271 HorizontalPosition.FloatAtStart( 272 width = getDimensionPixelSize(R.dimen.shade_panel_width) 273 ) 274 } else { 275 HorizontalPosition.EdgeToEdge 276 } 277 } 278 279 ConfigurationBasedDimensions( 280 horizontalPosition = horizontalPosition, 281 marginStart = if (shadeMode is Split) 0 else marginHorizontal, 282 marginEnd = marginHorizontal, 283 marginBottom = 284 getDimensionPixelSize(R.dimen.notification_panel_margin_bottom), 285 // y position of the NSSL in the window needs to be 0 under scene 286 // container 287 marginTop = 0, 288 ) 289 } 290 } 291 } else { 292 interactor.configurationBasedDimensions.map { 293 ConfigurationBasedDimensions( 294 horizontalPosition = 295 if (it.useSplitShade) HorizontalPosition.MiddleToEdge() 296 else HorizontalPosition.EdgeToEdge, 297 marginStart = if (it.useSplitShade) 0 else it.marginHorizontal, 298 marginEnd = it.marginHorizontal, 299 marginBottom = it.marginBottom, 300 marginTop = 301 if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop, 302 ) 303 } 304 } 305 .distinctUntilChanged() 306 .dumpWhileCollecting("configurationBasedDimensions") 307 308 private val isOnAnyBouncer: Flow<Boolean> = 309 anyOf( 310 keyguardTransitionInteractor.transitionValue(ALTERNATE_BOUNCER).map { it > 0f }, 311 keyguardTransitionInteractor 312 .transitionValue( 313 content = Overlays.Bouncer, 314 stateWithoutSceneContainer = PRIMARY_BOUNCER, 315 ) 316 .map { it > 0f }, 317 ) 318 319 /** If the user is visually on one of the unoccluded lockscreen states. */ 320 val isOnLockscreen: Flow<Boolean> = 321 if (glanceableHubV2()) { 322 anyOf( 323 keyguardTransitionInteractor.transitionValue(AOD).map { it > 0f }, 324 keyguardTransitionInteractor.transitionValue(DOZING).map { it > 0f }, 325 keyguardTransitionInteractor.transitionValue(LOCKSCREEN).map { it > 0f }, 326 allOf( 327 // Exclude bouncer showing over communal hub, as this should not be 328 // considered 329 // "lockscreen" 330 not(communalSceneInteractor.isCommunalVisible), 331 isOnAnyBouncer, 332 ), 333 ) 334 } else { 335 anyOf( 336 keyguardTransitionInteractor.transitionValue(AOD).map { it > 0f }, 337 keyguardTransitionInteractor.transitionValue(DOZING).map { it > 0f }, 338 keyguardTransitionInteractor.transitionValue(LOCKSCREEN).map { it > 0f }, 339 isOnAnyBouncer, 340 ) 341 } 342 .flowName("isOnLockscreen") 343 .stateIn( 344 scope = applicationScope, 345 started = SharingStarted.Eagerly, 346 initialValue = false, 347 ) 348 .dumpValue("isOnLockscreen") 349 350 /** Are we purely on the keyguard without the shade/qs? */ 351 val isOnLockscreenWithoutShade: Flow<Boolean> = 352 combine(isOnLockscreen, isAnyExpanded) { isKeyguard, isAnyExpanded -> 353 isKeyguard && !isAnyExpanded 354 } 355 .flowName("isOnLockscreenWithoutShade") 356 .stateIn( 357 scope = applicationScope, 358 started = SharingStarted.Eagerly, 359 initialValue = false, 360 ) 361 .dumpValue("isOnLockscreenWithoutShade") 362 363 private val aboutToTransitionToHub: Flow<Unit> = 364 if (SceneContainerFlag.isEnabled) { 365 emptyFlow() 366 } else { 367 conflatedCallbackFlow { 368 val callback = 369 CommunalSceneInteractor.OnSceneAboutToChangeListener { toScene, _ -> 370 if (toScene == CommunalScenes.Communal) { 371 trySend(Unit) 372 } 373 } 374 communalSceneInteractor.registerSceneStateProcessor(callback) 375 awaitClose { communalSceneInteractor.unregisterSceneStateProcessor(callback) } 376 } 377 } 378 379 /** If the user is visually on the glanceable hub or transitioning to/from it */ 380 private val isOnGlanceableHub: Flow<Boolean> = 381 merge( 382 aboutToTransitionToHub.map { true }, 383 anyOf( 384 keyguardTransitionInteractor.isFinishedIn( 385 content = Scenes.Communal, 386 stateWithoutSceneContainer = GLANCEABLE_HUB, 387 ), 388 keyguardTransitionInteractor.isInTransition( 389 edge = Edge.create(to = Scenes.Communal), 390 edgeWithoutSceneContainer = Edge.create(to = GLANCEABLE_HUB), 391 ), 392 keyguardTransitionInteractor.isInTransition( 393 edge = Edge.create(from = Scenes.Communal), 394 edgeWithoutSceneContainer = Edge.create(from = GLANCEABLE_HUB), 395 ), 396 ), 397 ) 398 .distinctUntilChanged() 399 .dumpWhileCollecting("isOnGlanceableHub") 400 401 /** Are we purely on the glanceable hub without the shade/qs? */ 402 val isOnGlanceableHubWithoutShade: Flow<Boolean> = 403 combine(isOnGlanceableHub, isAnyExpanded) { isGlanceableHub, isAnyExpanded -> 404 isGlanceableHub && !isAnyExpanded 405 } 406 .flowName("isOnGlanceableHubWithoutShade") 407 .stateIn( 408 scope = applicationScope, 409 started = SharingStarted.Eagerly, 410 initialValue = false, 411 ) 412 .dumpValue("isOnGlanceableHubWithoutShade") 413 414 /** Are we on the dream without the shade/qs? */ 415 private val isDreamingWithoutShade: Flow<Boolean> = 416 combine(keyguardTransitionInteractor.isFinishedIn(DREAMING), isAnyExpanded) { 417 isDreaming, 418 isAnyExpanded -> 419 isDreaming && !isAnyExpanded 420 } 421 .flowName("isDreamingWithoutShade") 422 .stateIn( 423 scope = applicationScope, 424 started = SharingStarted.Eagerly, 425 initialValue = false, 426 ) 427 .dumpValue("isDreamingWithoutShade") 428 429 /** 430 * Fade in if the user swipes the shade back up, not if collapsed by going to AOD. This is 431 * needed due to the lack of a SHADE state with existing keyguard transitions. 432 */ 433 private fun awaitCollapse(): Flow<Boolean> { 434 var aodTransitionIsComplete = true 435 return combine( 436 isOnLockscreenWithoutShade, 437 keyguardTransitionInteractor.isInTransition( 438 edge = Edge.create(from = LOCKSCREEN, to = AOD) 439 ), 440 ::Pair, 441 ) 442 .transformWhile { (isOnLockscreenWithoutShade, aodTransitionIsRunning) -> 443 // Wait until the AOD transition is complete before terminating 444 if (!aodTransitionIsComplete && !aodTransitionIsRunning) { 445 aodTransitionIsComplete = true 446 emit(false) // do not fade in 447 false 448 } else if (aodTransitionIsRunning) { 449 aodTransitionIsComplete = false 450 true 451 } else if (isOnLockscreenWithoutShade) { 452 // Shade is closed, fade in and terminate 453 emit(true) 454 false 455 } else { 456 true 457 } 458 } 459 } 460 461 /** Fade in only for use after the shade collapses */ 462 val shadeCollapseFadeIn: Flow<Boolean> = 463 flow { 464 while (currentCoroutineContext().isActive) { 465 // Ensure shade is collapsed 466 isShadeLocked.first { !it } 467 emit(false) 468 // Wait for shade to be fully expanded 469 isShadeLocked.first { it } 470 // ... and then for it to be collapsed OR a transition to AOD begins. 471 // If AOD, do not fade in (a fade out occurs instead). 472 awaitCollapse().collect { doFadeIn -> 473 if (doFadeIn) { 474 emit(true) 475 } 476 } 477 } 478 } 479 .flowName("shadeCollapseFadeIn") 480 .stateIn( 481 scope = applicationScope, 482 started = SharingStarted.WhileSubscribed(), 483 initialValue = false, 484 ) 485 .dumpValue("shadeCollapseFadeIn") 486 487 /** 488 * The container occupies the entire screen, and must be positioned relative to other elements. 489 * 490 * On keyguard, this generally fits below the clock and above the lock icon, or in split shade, 491 * the top of the screen to the lock icon. 492 * 493 * When the shade is expanding, the position is controlled by... the shade. 494 */ 495 val bounds: StateFlow<NotificationContainerBounds> by lazy { 496 SceneContainerFlag.assertInLegacyMode() 497 combine( 498 isOnLockscreenWithoutShade, 499 keyguardInteractor.notificationContainerBounds, 500 paddingTopDimen, 501 interactor.topPosition 502 .sampleCombine( 503 keyguardTransitionInteractor.isInTransition, 504 shadeInteractor.qsExpansion, 505 ) 506 .onStart { emit(Triple(0f, false, 0f)) }, 507 ) { onLockscreen, bounds, paddingTop, (top, isInTransitionToAnyState, qsExpansion) -> 508 if (onLockscreen) { 509 bounds.copy(top = bounds.top - paddingTop) 510 } else { 511 // When QS expansion > 0, it should directly set the top padding so do not 512 // animate it 513 val animate = qsExpansion == 0f && !isInTransitionToAnyState 514 bounds.copy(top = top, isAnimated = animate) 515 } 516 } 517 .flowName("bounds") 518 .stateIn( 519 scope = applicationScope, 520 started = SharingStarted.Lazily, 521 initialValue = NotificationContainerBounds(), 522 ) 523 .dumpValue("bounds") 524 } 525 526 /** 527 * Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out 528 * notifications unless it's a large screen. 529 */ 530 private val alphaForShadeAndQsExpansion: Flow<Float> = 531 if (SceneContainerFlag.isEnabled) { 532 shadeModeInteractor.shadeMode.flatMapLatest { shadeMode -> 533 when (shadeMode) { 534 Single -> 535 combineTransform( 536 shadeInteractor.shadeExpansion, 537 shadeInteractor.qsExpansion, 538 bouncerInteractor.bouncerExpansion, 539 ) { shadeExpansion, qsExpansion, bouncerExpansion -> 540 if (bouncerExpansion == 1f) { 541 emit(0f) 542 } else if (bouncerExpansion > 0f) { 543 emit(1 - bouncerExpansion) 544 } else if (qsExpansion == 1f) { 545 // Ensure HUNs will be visible in QS shade (at least while 546 // unlocked) 547 emit(1f) 548 } else if (shadeExpansion > 0f || qsExpansion > 0f) { 549 // Fade as QS shade expands 550 emit(1f - qsExpansion) 551 } 552 } 553 554 Split -> 555 combineTransform(isAnyExpanded, bouncerInteractor.bouncerExpansion) { 556 isAnyExpanded, 557 bouncerExpansion -> 558 if (bouncerExpansion == 1f) { 559 emit(0f) 560 } else if (bouncerExpansion > 0f) { 561 emit(1 - bouncerExpansion) 562 } else if (isAnyExpanded) { 563 emit(1f) 564 } 565 } 566 567 Dual -> 568 combineTransform( 569 shadeModeInteractor.isShadeLayoutWide, 570 headsUpNotificationInteractor.get().isHeadsUpOrAnimatingAway, 571 shadeInteractor.shadeExpansion, 572 shadeInteractor.qsExpansion, 573 bouncerInteractor.bouncerExpansion, 574 ) { 575 isShadeLayoutWide, 576 isHeadsUpOrAnimatingAway, 577 shadeExpansion, 578 qsExpansion, 579 bouncerExpansion -> 580 if (bouncerExpansion == 1f) { 581 emit(0f) 582 } else if (bouncerExpansion > 0f) { 583 emit(1 - bouncerExpansion) 584 } else if (isShadeLayoutWide) { 585 if (shadeExpansion > 0f) { 586 emit(1f) 587 } 588 } else if (isHeadsUpOrAnimatingAway) { 589 // Ensure HUNs will be visible in QS shade (at least while 590 // unlocked) 591 emit(1f) 592 } else if (shadeExpansion > 0f || qsExpansion > 0f) { 593 // On a narrow screen, the QS shade overlaps with lockscreen 594 // notifications. Fade them out as the QS shade expands. 595 emit(1f - qsExpansion) 596 } 597 } 598 } 599 } 600 } else { 601 interactor.configurationBasedDimensions.flatMapLatest { configurationBasedDimensions 602 -> 603 combineTransform(shadeInteractor.shadeExpansion, shadeInteractor.qsExpansion) { 604 shadeExpansion, 605 qsExpansion -> 606 if (shadeExpansion > 0f || qsExpansion > 0f) { 607 if (configurationBasedDimensions.useSplitShade) { 608 emit(1f) 609 } else if (qsExpansion == 1f) { 610 // Ensure HUNs will be visible in QS shade (at least while 611 // unlocked) 612 emit(1f) 613 } else { 614 // Fade as QS shade expands 615 emit(1f - qsExpansion) 616 } 617 } 618 } 619 } 620 } 621 .onStart { emit(1f) } 622 .dumpWhileCollecting("alphaForShadeAndQsExpansion") 623 624 val panelAlpha = keyguardInteractor.panelAlpha 625 626 private fun bouncerToGoneNotificationAlpha(viewState: ViewStateAccessor): Flow<Float> = 627 merge( 628 primaryBouncerToGoneTransitionViewModel.notificationAlpha(viewState), 629 alternateBouncerToGoneTransitionViewModel.notificationAlpha(viewState), 630 ) 631 .sample(communalSceneInteractor.isCommunalVisible) { alpha, isCommunalVisible -> 632 // when glanceable hub is visible, hide notifications during the transition to GONE 633 if (isCommunalVisible) 0f else alpha 634 } 635 .dumpWhileCollecting("bouncerToGoneNotificationAlpha") 636 637 private fun alphaForTransitions(viewState: ViewStateAccessor): Flow<Float> { 638 return merge( 639 keyguardInteractor.dismissAlpha.dumpWhileCollecting("keyguardInteractor.dismissAlpha"), 640 // All transition view models are mutually exclusive, and safe to merge 641 bouncerToGoneNotificationAlpha(viewState), 642 aodToGoneTransitionViewModel.notificationAlpha(viewState), 643 aodToLockscreenTransitionViewModel.notificationAlpha, 644 aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), 645 aodToGlanceableHubTransitionViewModel.lockscreenAlpha(viewState), 646 aodToPrimaryBouncerTransitionViewModel.notificationAlpha, 647 dozingToLockscreenTransitionViewModel.lockscreenAlpha, 648 dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState), 649 dozingToPrimaryBouncerTransitionViewModel.notificationAlpha, 650 dreamingToLockscreenTransitionViewModel.lockscreenAlpha, 651 goneToAodTransitionViewModel.notificationAlpha, 652 goneToDreamingTransitionViewModel.lockscreenAlpha, 653 goneToDozingTransitionViewModel.notificationAlpha, 654 goneToLockscreenTransitionViewModel.lockscreenAlpha, 655 lockscreenToDreamingTransitionViewModel.lockscreenAlpha, 656 lockscreenToGoneTransitionViewModel.notificationAlpha(viewState), 657 lockscreenToOccludedTransitionViewModel.lockscreenAlpha, 658 lockscreenToPrimaryBouncerTransitionViewModel.notificationAlpha, 659 alternateBouncerToPrimaryBouncerTransitionViewModel.notificationAlpha, 660 occludedToAodTransitionViewModel.lockscreenAlpha, 661 occludedToGoneTransitionViewModel.notificationAlpha(viewState), 662 occludedToLockscreenTransitionViewModel.lockscreenAlpha, 663 offToLockscreenTransitionViewModel.lockscreenAlpha, 664 primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState), 665 glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, 666 glanceableHubToAodTransitionViewModel.lockscreenAlpha, 667 lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha, 668 ) 669 } 670 671 fun keyguardAlpha(viewState: ViewStateAccessor, scope: CoroutineScope): Flow<Float> { 672 val isKeyguardOccluded = 673 keyguardTransitionInteractor.transitionValue(OCCLUDED).map { it == 1f } 674 675 val isKeyguardNotVisibleInState = 676 if (SceneContainerFlag.isEnabled) { 677 isKeyguardOccluded 678 } else { 679 anyOf( 680 isKeyguardOccluded, 681 keyguardTransitionInteractor 682 .transitionValue(content = Scenes.Gone, stateWithoutSceneContainer = GONE) 683 .map { it == 1f }, 684 ) 685 } 686 687 // Transitions are not (yet) authoritative for NSSL; they still rely on StatusBarState to 688 // help determine when the device has fully moved to GONE or OCCLUDED state. Once SHADE 689 // state has been set, let shade alpha take over 690 val isKeyguardNotVisible = 691 combine(isKeyguardNotVisibleInState, keyguardInteractor.statusBarState) { 692 isKeyguardNotVisibleInState, 693 statusBarState -> 694 isKeyguardNotVisibleInState && statusBarState == SHADE 695 } 696 697 // This needs to continue collecting the current value so that when it is selected in the 698 // flatMapLatest below, the last value gets emitted, to avoid the randomness of `merge`. 699 val alphaForTransitionsAndShade = 700 merge(alphaForTransitions(viewState), alphaForShadeAndQsExpansion) 701 .flowName("alphaForTransitionsAndShade") 702 .stateIn( 703 // Use view-level scope instead of ApplicationScope, to prevent collection that 704 // never stops 705 scope = scope, 706 started = SharingStarted.Eagerly, 707 initialValue = 1f, 708 ) 709 .dumpValue("alphaForTransitionsAndShade") 710 711 return isKeyguardNotVisible 712 .flatMapLatest { isKeyguardNotVisible -> 713 if (isKeyguardNotVisible) { 714 alphaForShadeAndQsExpansion 715 } else { 716 alphaForTransitionsAndShade 717 } 718 } 719 .distinctUntilChanged() 720 .dumpWhileCollecting("keyguardAlpha") 721 } 722 723 val blurRadius = 724 primaryBouncerTransitions 725 .map { transition -> transition.notificationBlurRadius } 726 .merge() 727 .dumpWhileCollecting("blurRadius") 728 729 /** 730 * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB or 731 * DREAMING<->GLANCEABLE_HUB transition or idle on the hub. 732 * 733 * Must return 1.0f when not controlling the alpha since notifications does a min of all the 734 * alpha sources. 735 */ 736 val glanceableHubAlpha: Flow<Float> = 737 combineTransform( 738 isOnGlanceableHubWithoutShade, 739 isOnLockscreen, 740 isDreamingWithoutShade, 741 merge( 742 lockscreenToGlanceableHubTransitionViewModel.notificationAlpha, 743 glanceableHubToLockscreenTransitionViewModel.notificationAlpha, 744 dozingToGlanceableHubTransitionViewModel.notificationAlpha, 745 ) 746 // Manually emit on start because [notificationAlpha] only starts emitting 747 // when transitions start. 748 .onStart { emit(1f) }, 749 ) { isOnGlanceableHubWithoutShade, isOnLockscreen, isDreamingWithoutShade, alpha -> 750 if ((isOnGlanceableHubWithoutShade || isDreamingWithoutShade) && !isOnLockscreen) { 751 // Notifications should not be visible on the glanceable hub. 752 // TODO(b/321075734): implement a way to actually set the notifications to 753 // gone while on the hub instead of just adjusting alpha 754 emit(0f) 755 } else if (isOnGlanceableHubWithoutShade) { 756 // We are transitioning between hub and lockscreen, so set the alpha for the 757 // transition animation. 758 emit(alpha) 759 } else { 760 // Not on the hub and no transitions running, return full visibility so we 761 // don't block the notifications from showing. 762 emit(1f) 763 } 764 } 765 .distinctUntilChanged() 766 .dumpWhileCollecting("glanceableHubAlpha") 767 768 /** 769 * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be 770 * translated as the keyguard fades out. 771 */ 772 val translationY: Flow<Float> = 773 combine( 774 aodBurnInViewModel.movement.map { it.translationY.toFloat() }.onStart { emit(0f) }, 775 isOnLockscreenWithoutShade, 776 merge( 777 keyguardInteractor.keyguardTranslationY, 778 occludedToLockscreenTransitionViewModel.lockscreenTranslationY, 779 ), 780 ) { burnInY, isOnLockscreenWithoutShade, translationY -> 781 // with SceneContainer, x translation is handled by views, y is handled by compose 782 SceneContainerFlag.assertInLegacyMode() 783 784 if (isOnLockscreenWithoutShade) { 785 burnInY + translationY 786 } else { 787 0f 788 } 789 } 790 .dumpWhileCollecting("translationY") 791 792 /** Horizontal translation to apply to the container. */ 793 val translationX: Flow<Float> = 794 merge( 795 // The container may need to be translated along the X axis as the keyguard fades 796 // out, such as when swiping open the glanceable hub from the lockscreen. 797 lockscreenToGlanceableHubTransitionViewModel.notificationTranslationX, 798 glanceableHubToLockscreenTransitionViewModel.notificationTranslationX, 799 if (SceneContainerFlag.isEnabled) { 800 // The container may need to be translated along the X axis as the unfolded 801 // foldable is folded slightly. 802 unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false) 803 } else { 804 emptyFlow() 805 }, 806 ) 807 .dumpWhileCollecting("translationX") 808 809 private val availableHeight: Flow<Float> = 810 if (SceneContainerFlag.isEnabled) { 811 notificationStackAppearanceInteractor.constrainedAvailableSpace.map { it.toFloat() } 812 } else { 813 bounds.map { it.bottom - it.top } 814 } 815 .distinctUntilChanged() 816 .dumpWhileCollecting("availableHeight") 817 818 /** 819 * When on keyguard, there is limited space to display notifications so calculate how many could 820 * be shown. Otherwise, there is no limit since the vertical space will be scrollable. 821 * 822 * When expanding or when the user is interacting with the shade, keep the count stable; do not 823 * emit a value. 824 */ 825 fun getLockscreenDisplayConfig( 826 calculateSpace: (Float, Boolean) -> Int 827 ): Flow<LockscreenDisplayConfig> { 828 val showLimitedNotifications = isOnLockscreenWithoutShade 829 val showUnlimitedNotificationsAndIsOnLockScreen = 830 combine( 831 isOnLockscreen, 832 keyguardInteractor.statusBarState, 833 merge( 834 primaryBouncerToGoneTransitionViewModel.showAllNotifications, 835 alternateBouncerToGoneTransitionViewModel.showAllNotifications, 836 ) 837 .onStart { emit(false) }, 838 ) { isOnLockscreen, statusBarState, showAllNotifications -> 839 (statusBarState == SHADE_LOCKED || !isOnLockscreen || showAllNotifications) to 840 isOnLockscreen 841 } 842 843 @Suppress("UNCHECKED_CAST") 844 return combineTransform( 845 showLimitedNotifications, 846 showUnlimitedNotificationsAndIsOnLockScreen, 847 shadeInteractor.isUserInteracting, 848 availableHeight, 849 interactor.notificationStackChanged, 850 interactor.useExtraShelfSpace, 851 ) { flows -> 852 val showLimitedNotifications = flows[0] as Boolean 853 val (showUnlimitedNotifications, isOnLockscreen) = 854 flows[1] as Pair<Boolean, Boolean> 855 val isUserInteracting = flows[2] as Boolean 856 val availableHeight = flows[3] as Float 857 val useExtraShelfSpace = flows[5] as Boolean 858 859 if (!isUserInteracting) { 860 if (showLimitedNotifications) { 861 emit( 862 LockscreenDisplayConfig( 863 isOnLockscreen = isOnLockscreen, 864 maxNotifications = 865 calculateSpace(availableHeight, useExtraShelfSpace), 866 ) 867 ) 868 } else if (showUnlimitedNotifications) { 869 emit( 870 LockscreenDisplayConfig( 871 isOnLockscreen = isOnLockscreen, 872 maxNotifications = -1, 873 ) 874 ) 875 } 876 } 877 } 878 .distinctUntilChanged() 879 .dumpWhileCollecting("maxNotifications") 880 } 881 882 /** 883 * Wallpaper focal area needs the absolute bottom of notification stack to avoid occlusion. It 884 * should not change with notifications in shade. 885 * 886 * @param calculateMaxNotifications is required by getMaxNotifications as calculateSpace by 887 * calling computeMaxKeyguardNotifications in NotificationStackSizeCalculator 888 * @param calculateHeight is calling computeHeight in NotificationStackSizeCalculator The edge 889 * case is that when maxNotifications is 0, we won't take shelfHeight into account 890 */ 891 fun getNotificationStackAbsoluteBottom( 892 calculateMaxNotifications: (Float, Boolean) -> Int, 893 calculateHeight: (Int) -> Float, 894 shelfHeight: Float, 895 ): Flow<Float> { 896 SceneContainerFlag.assertInLegacyMode() 897 898 return combine( 899 getLockscreenDisplayConfig(calculateMaxNotifications).map { (_, maxNotifications) -> 900 val height = calculateHeight(maxNotifications) 901 if (maxNotifications == 0) { 902 height - shelfHeight 903 } else { 904 height 905 } 906 }, 907 bounds.map { it.top }, 908 isOnLockscreenWithoutShade, 909 ) { height, top, isOnLockscreenWithoutShade -> 910 if (isOnLockscreenWithoutShade) { 911 top + height 912 } else { 913 null 914 } 915 } 916 .filterNotNull() 917 } 918 919 fun notificationStackChanged() { 920 interactor.notificationStackChanged() 921 } 922 923 data class ConfigurationBasedDimensions( 924 val horizontalPosition: HorizontalPosition, 925 val marginStart: Int, 926 val marginTop: Int, 927 val marginEnd: Int, 928 val marginBottom: Int, 929 ) 930 931 /** Specifies the horizontal layout constraints for the notification container. */ 932 sealed interface HorizontalPosition { 933 /** The container is using the full width of the screen (minus any margins). */ 934 data object EdgeToEdge : HorizontalPosition 935 936 /** The container is laid out from the given [ratio] of the screen width to the end edge. */ 937 data class MiddleToEdge(val ratio: Float = 0.5f) : HorizontalPosition 938 939 /** 940 * The container has a fixed [width] and is aligned to the start of the screen. In this 941 * layout, the end edge of the container is floating, i.e. unconstrained. 942 */ 943 data class FloatAtStart(val width: Int) : HorizontalPosition 944 } 945 946 /** 947 * Data class representing a configuration for displaying Notifications on the Lockscreen. 948 * 949 * @param isOnLockscreen is the user on the lockscreen 950 * @param maxNotifications Limit for the max number of top-level Notifications to be displayed. 951 * A value of -1 indicates no limit. 952 */ 953 data class LockscreenDisplayConfig(val isOnLockscreen: Boolean, val maxNotifications: Int) 954 } 955