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 androidx.annotation.VisibleForTesting 23 import com.android.systemui.common.shared.model.NotificationContainerBounds 24 import com.android.systemui.dagger.SysUISingleton 25 import com.android.systemui.dagger.qualifiers.Application 26 import com.android.systemui.dump.DumpManager 27 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 28 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 29 import com.android.systemui.keyguard.shared.model.Edge 30 import com.android.systemui.keyguard.shared.model.KeyguardState 31 import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER 32 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD 33 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING 34 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING 35 import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB 36 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE 37 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN 38 import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED 39 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER 40 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE 41 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED 42 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING 43 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel 44 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel 45 import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel 46 import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel 47 import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel 48 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters 49 import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel 50 import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel 51 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel 52 import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel 53 import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel 54 import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel 55 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel 56 import com.android.systemui.keyguard.ui.viewmodel.GoneToLockscreenTransitionViewModel 57 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel 58 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel 59 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGoneTransitionViewModel 60 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel 61 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTransitionViewModel 62 import com.android.systemui.keyguard.ui.viewmodel.OccludedToAodTransitionViewModel 63 import com.android.systemui.keyguard.ui.viewmodel.OccludedToGoneTransitionViewModel 64 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel 65 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel 66 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel 67 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor 68 import com.android.systemui.scene.shared.flag.SceneContainerFlag 69 import com.android.systemui.scene.shared.model.Scenes 70 import com.android.systemui.shade.domain.interactor.ShadeInteractor 71 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor 72 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor 73 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor 74 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf 75 import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf 76 import com.android.systemui.util.kotlin.FlowDumperImpl 77 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine 78 import javax.inject.Inject 79 import kotlinx.coroutines.CoroutineScope 80 import kotlinx.coroutines.ExperimentalCoroutinesApi 81 import kotlinx.coroutines.currentCoroutineContext 82 import kotlinx.coroutines.flow.Flow 83 import kotlinx.coroutines.flow.SharingStarted 84 import kotlinx.coroutines.flow.StateFlow 85 import kotlinx.coroutines.flow.combine 86 import kotlinx.coroutines.flow.combineTransform 87 import kotlinx.coroutines.flow.distinctUntilChanged 88 import kotlinx.coroutines.flow.emptyFlow 89 import kotlinx.coroutines.flow.first 90 import kotlinx.coroutines.flow.flatMapLatest 91 import kotlinx.coroutines.flow.flow 92 import kotlinx.coroutines.flow.map 93 import kotlinx.coroutines.flow.merge 94 import kotlinx.coroutines.flow.onStart 95 import kotlinx.coroutines.flow.stateIn 96 import kotlinx.coroutines.flow.transformWhile 97 import kotlinx.coroutines.isActive 98 99 /** View-model for the shared notification container, used by both the shade and keyguard spaces */ 100 @SysUISingleton 101 class SharedNotificationContainerViewModel 102 @Inject 103 constructor( 104 private val interactor: SharedNotificationContainerInteractor, 105 dumpManager: DumpManager, 106 @Application applicationScope: CoroutineScope, 107 private val keyguardInteractor: KeyguardInteractor, 108 private val keyguardTransitionInteractor: KeyguardTransitionInteractor, 109 private val shadeInteractor: ShadeInteractor, 110 private val notificationStackAppearanceInteractor: NotificationStackAppearanceInteractor, 111 private val alternateBouncerToGoneTransitionViewModel: 112 AlternateBouncerToGoneTransitionViewModel, 113 private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel, 114 private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, 115 private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, 116 private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, 117 private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel, 118 private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, 119 private val glanceableHubToLockscreenTransitionViewModel: 120 GlanceableHubToLockscreenTransitionViewModel, 121 private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel, 122 private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel, 123 private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel, 124 private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel, 125 private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel, 126 private val lockscreenToGlanceableHubTransitionViewModel: 127 LockscreenToGlanceableHubTransitionViewModel, 128 private val lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel, 129 private val lockscreenToPrimaryBouncerTransitionViewModel: 130 LockscreenToPrimaryBouncerTransitionViewModel, 131 private val lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel, 132 private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel, 133 private val occludedToGoneTransitionViewModel: OccludedToGoneTransitionViewModel, 134 private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, 135 private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, 136 private val primaryBouncerToLockscreenTransitionViewModel: 137 PrimaryBouncerToLockscreenTransitionViewModel, 138 private val aodBurnInViewModel: AodBurnInViewModel, 139 unfoldTransitionInteractor: UnfoldTransitionInteractor, 140 ) : FlowDumperImpl(dumpManager) { 141 private val statesForConstrainedNotifications: Set<KeyguardState> = 142 setOf(AOD, LOCKSCREEN, DOZING, ALTERNATE_BOUNCER, PRIMARY_BOUNCER) 143 private val statesForHiddenKeyguard: Set<KeyguardState> = setOf(GONE, OCCLUDED) 144 145 /** 146 * Is either shade/qs expanded? This intentionally does not use the [ShadeInteractor] version, 147 * as the legacy implementation has extra logic that produces incorrect results. 148 */ 149 private val isAnyExpanded = 150 combine( 151 shadeInteractor.shadeExpansion.map { it > 0f }, 152 shadeInteractor.qsExpansion.map { it > 0f }, 153 ) { shadeExpansion, qsExpansion -> 154 shadeExpansion || qsExpansion 155 } 156 .stateIn( 157 scope = applicationScope, 158 started = SharingStarted.Eagerly, 159 initialValue = false, 160 ) 161 162 /** 163 * Shade locked is a legacy concept, but necessary to mimic current functionality. Listen for 164 * both SHADE_LOCKED and shade/qs expansion in order to determine lock state, as one can arrive 165 * before the other. 166 */ 167 private val isShadeLocked: Flow<Boolean> = 168 combine( 169 keyguardInteractor.statusBarState.map { it == SHADE_LOCKED }, 170 isAnyExpanded, 171 ) { isShadeLocked, isAnyExpanded -> 172 isShadeLocked && isAnyExpanded 173 } 174 .stateIn( 175 scope = applicationScope, 176 started = SharingStarted.Eagerly, 177 initialValue = false, 178 ) 179 .dumpWhileCollecting("isShadeLocked") 180 181 @VisibleForTesting 182 val paddingTopDimen: Flow<Int> = 183 interactor.configurationBasedDimensions 184 .map { 185 when { 186 !it.useSplitShade -> 0 187 it.useLargeScreenHeader -> it.marginTopLargeScreen 188 else -> it.marginTop 189 } 190 } 191 .distinctUntilChanged() 192 .dumpWhileCollecting("paddingTopDimen") 193 194 val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = 195 interactor.configurationBasedDimensions 196 .map { 197 val marginTop = 198 when { 199 // y position of the NSSL in the window needs to be 0 under scene container 200 SceneContainerFlag.isEnabled -> 0 201 it.useLargeScreenHeader -> it.marginTopLargeScreen 202 else -> it.marginTop 203 } 204 ConfigurationBasedDimensions( 205 marginStart = if (it.useSplitShade) 0 else it.marginHorizontal, 206 marginEnd = it.marginHorizontal, 207 marginBottom = it.marginBottom, 208 marginTop = marginTop, 209 useSplitShade = it.useSplitShade, 210 ) 211 } 212 .distinctUntilChanged() 213 .dumpWhileCollecting("configurationBasedDimensions") 214 215 /** If the user is visually on one of the unoccluded lockscreen states. */ 216 val isOnLockscreen: Flow<Boolean> = 217 combine( 218 keyguardTransitionInteractor.finishedKeyguardState.map { 219 statesForConstrainedNotifications.contains(it) 220 }, 221 keyguardTransitionInteractor.transitionValue(LOCKSCREEN).map { it > 0f }, 222 ) { constrainedNotificationState, transitioningToOrFromLockscreen -> 223 constrainedNotificationState || transitioningToOrFromLockscreen 224 } 225 .stateIn( 226 scope = applicationScope, 227 started = SharingStarted.Eagerly, 228 initialValue = false 229 ) 230 .dumpValue("isOnLockscreen") 231 232 /** Are we purely on the keyguard without the shade/qs? */ 233 val isOnLockscreenWithoutShade: Flow<Boolean> = 234 combine( 235 isOnLockscreen, 236 isAnyExpanded, 237 ) { isKeyguard, isAnyExpanded -> 238 isKeyguard && !isAnyExpanded 239 } 240 .stateIn( 241 scope = applicationScope, 242 started = SharingStarted.Eagerly, 243 initialValue = false, 244 ) 245 .dumpValue("isOnLockscreenWithoutShade") 246 247 /** If the user is visually on the glanceable hub or transitioning to/from it */ 248 private val isOnGlanceableHub: Flow<Boolean> = 249 combine( 250 keyguardTransitionInteractor.finishedKeyguardState.map { state -> 251 state == GLANCEABLE_HUB 252 }, 253 anyOf( 254 keyguardTransitionInteractor.isInTransition( 255 edge = Edge.create(to = Scenes.Communal), 256 edgeWithoutSceneContainer = Edge.create(to = GLANCEABLE_HUB) 257 ), 258 keyguardTransitionInteractor.isInTransition( 259 edge = Edge.create(from = Scenes.Communal), 260 edgeWithoutSceneContainer = Edge.create(from = GLANCEABLE_HUB) 261 ), 262 ), 263 ) { isOnGlanceableHub, transitioningToOrFromHub -> 264 isOnGlanceableHub || transitioningToOrFromHub 265 } 266 .distinctUntilChanged() 267 .dumpWhileCollecting("isOnGlanceableHub") 268 269 /** Are we purely on the glanceable hub without the shade/qs? */ 270 val isOnGlanceableHubWithoutShade: Flow<Boolean> = 271 combine( 272 isOnGlanceableHub, 273 isAnyExpanded, 274 ) { isGlanceableHub, isAnyExpanded -> 275 isGlanceableHub && !isAnyExpanded 276 } 277 .stateIn( 278 scope = applicationScope, 279 started = SharingStarted.Eagerly, 280 initialValue = false, 281 ) 282 .dumpValue("isOnGlanceableHubWithoutShade") 283 284 /** Are we on the dream without the shade/qs? */ 285 private val isDreamingWithoutShade: Flow<Boolean> = 286 combine( 287 keyguardTransitionInteractor.isFinishedInState(DREAMING), 288 isAnyExpanded, 289 ) { isDreaming, isAnyExpanded -> 290 isDreaming && !isAnyExpanded 291 } 292 .stateIn( 293 scope = applicationScope, 294 started = SharingStarted.Eagerly, 295 initialValue = false, 296 ) 297 .dumpValue("isDreamingWithoutShade") 298 299 /** 300 * Fade in if the user swipes the shade back up, not if collapsed by going to AOD. This is 301 * needed due to the lack of a SHADE state with existing keyguard transitions. 302 */ 303 private fun awaitCollapse(): Flow<Boolean> { 304 var aodTransitionIsComplete = true 305 return combine( 306 isOnLockscreenWithoutShade, 307 keyguardTransitionInteractor.isInTransition( 308 edge = Edge.create(from = LOCKSCREEN, to = AOD) 309 ), 310 ::Pair 311 ) 312 .transformWhile { (isOnLockscreenWithoutShade, aodTransitionIsRunning) -> 313 // Wait until the AOD transition is complete before terminating 314 if (!aodTransitionIsComplete && !aodTransitionIsRunning) { 315 aodTransitionIsComplete = true 316 emit(false) // do not fade in 317 false 318 } else if (aodTransitionIsRunning) { 319 aodTransitionIsComplete = false 320 true 321 } else if (isOnLockscreenWithoutShade) { 322 // Shade is closed, fade in and terminate 323 emit(true) 324 false 325 } else { 326 true 327 } 328 } 329 } 330 331 /** Fade in only for use after the shade collapses */ 332 val shadeCollapseFadeIn: Flow<Boolean> = 333 flow { 334 while (currentCoroutineContext().isActive) { 335 // Ensure shade is collapsed 336 isShadeLocked.first { !it } 337 emit(false) 338 // Wait for shade to be fully expanded 339 isShadeLocked.first { it } 340 // ... and then for it to be collapsed OR a transition to AOD begins. 341 // If AOD, do not fade in (a fade out occurs instead). 342 awaitCollapse().collect { doFadeIn -> 343 if (doFadeIn) { 344 emit(true) 345 } 346 } 347 } 348 } 349 .stateIn( 350 scope = applicationScope, 351 started = SharingStarted.WhileSubscribed(), 352 initialValue = false, 353 ) 354 .dumpValue("shadeCollapseFadeIn") 355 356 /** 357 * The container occupies the entire screen, and must be positioned relative to other elements. 358 * 359 * On keyguard, this generally fits below the clock and above the lock icon, or in split shade, 360 * the top of the screen to the lock icon. 361 * 362 * When the shade is expanding, the position is controlled by... the shade. 363 */ 364 val bounds: StateFlow<NotificationContainerBounds> by lazy { 365 SceneContainerFlag.assertInLegacyMode() 366 combine( 367 isOnLockscreenWithoutShade, 368 keyguardInteractor.notificationContainerBounds, 369 paddingTopDimen, 370 interactor.topPosition 371 .sampleCombine( 372 keyguardTransitionInteractor.isInTransitionToAnyState, 373 shadeInteractor.qsExpansion, 374 ) 375 .onStart { emit(Triple(0f, false, 0f)) } 376 ) { onLockscreen, bounds, paddingTop, (top, isInTransitionToAnyState, qsExpansion) -> 377 if (onLockscreen) { 378 bounds.copy(top = bounds.top - paddingTop) 379 } else { 380 // When QS expansion > 0, it should directly set the top padding so do not 381 // animate it 382 val animate = qsExpansion == 0f && !isInTransitionToAnyState 383 bounds.copy( 384 top = top, 385 isAnimated = animate, 386 ) 387 } 388 } 389 .stateIn( 390 scope = applicationScope, 391 started = SharingStarted.Lazily, 392 initialValue = NotificationContainerBounds(), 393 ) 394 .dumpValue("bounds") 395 } 396 397 /** 398 * Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out 399 * notifications unless in splitshade. 400 */ 401 private val alphaForShadeAndQsExpansion: Flow<Float> = 402 interactor.configurationBasedDimensions 403 .flatMapLatest { configurationBasedDimensions -> 404 combineTransform( 405 shadeInteractor.shadeExpansion, 406 shadeInteractor.qsExpansion, 407 ) { shadeExpansion, qsExpansion -> 408 if (shadeExpansion > 0f || qsExpansion > 0f) { 409 if (configurationBasedDimensions.useSplitShade) { 410 emit(1f) 411 } else if (qsExpansion == 1f) { 412 // Ensure HUNs will be visible in QS shade (at least while unlocked) 413 emit(1f) 414 } else { 415 // Fade as QS shade expands 416 emit(1f - qsExpansion) 417 } 418 } 419 } 420 } 421 .onStart { emit(1f) } 422 .dumpWhileCollecting("alphaForShadeAndQsExpansion") 423 424 private fun toFlowArray( 425 states: Set<KeyguardState>, 426 flow: (KeyguardState) -> Flow<Boolean> 427 ): Array<Flow<Boolean>> { 428 return states.map { flow(it) }.toTypedArray() 429 } 430 431 private val isTransitioningToHiddenKeyguard: Flow<Boolean> = 432 flow { 433 while (currentCoroutineContext().isActive) { 434 emit(false) 435 // Ensure states are inactive to start 436 allOf( 437 *toFlowArray(statesForHiddenKeyguard) { state -> 438 keyguardTransitionInteractor.transitionValue(state).map { it == 0f } 439 } 440 ) 441 .first { it } 442 // Wait for a qualifying transition to begin 443 anyOf( 444 *toFlowArray(statesForHiddenKeyguard) { state -> 445 keyguardTransitionInteractor 446 .transition(Edge.create(to = state)) 447 .map { it.value > 0f && it.transitionState == RUNNING } 448 .onStart { emit(false) } 449 } 450 ) 451 .first { it } 452 emit(true) 453 // Now await the signal that SHADE state has been reached or the transition was 454 // reversed. Until SHADE state has been replaced it is the only source of when 455 // it is considered safe to reset alpha to 1f for HUNs. 456 combine( 457 keyguardInteractor.statusBarState, 458 allOf( 459 *toFlowArray(statesForHiddenKeyguard) { state -> 460 keyguardTransitionInteractor.transitionValue(state).map { 461 it == 0f 462 } 463 } 464 ) 465 ) { statusBarState, stateIsReversed -> 466 statusBarState == SHADE || stateIsReversed 467 } 468 .first { it } 469 } 470 } 471 .dumpWhileCollecting("isTransitioningToHiddenKeyguard") 472 473 fun keyguardAlpha(viewState: ViewStateAccessor): Flow<Float> { 474 // All transition view models are mututally exclusive, and safe to merge 475 val alphaTransitions = 476 merge( 477 keyguardInteractor.dismissAlpha.dumpWhileCollecting( 478 "keyguardInteractor.dismissAlpha" 479 ), 480 alternateBouncerToGoneTransitionViewModel.notificationAlpha(viewState), 481 aodToGoneTransitionViewModel.notificationAlpha(viewState), 482 aodToLockscreenTransitionViewModel.notificationAlpha, 483 aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), 484 dozingToLockscreenTransitionViewModel.lockscreenAlpha, 485 dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState), 486 dreamingToLockscreenTransitionViewModel.lockscreenAlpha, 487 goneToAodTransitionViewModel.notificationAlpha, 488 goneToDreamingTransitionViewModel.lockscreenAlpha, 489 goneToDozingTransitionViewModel.notificationAlpha, 490 goneToLockscreenTransitionViewModel.lockscreenAlpha, 491 lockscreenToDreamingTransitionViewModel.lockscreenAlpha, 492 lockscreenToGoneTransitionViewModel.notificationAlpha(viewState), 493 lockscreenToOccludedTransitionViewModel.lockscreenAlpha, 494 lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha, 495 occludedToAodTransitionViewModel.lockscreenAlpha, 496 occludedToGoneTransitionViewModel.notificationAlpha(viewState), 497 occludedToLockscreenTransitionViewModel.lockscreenAlpha, 498 primaryBouncerToGoneTransitionViewModel.notificationAlpha, 499 primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha, 500 ) 501 502 return merge( 503 alphaTransitions, 504 // These remaining cases handle alpha changes within an existing state, such as 505 // shade expansion or swipe to dismiss 506 combineTransform( 507 isTransitioningToHiddenKeyguard, 508 alphaForShadeAndQsExpansion, 509 ) { isTransitioningToHiddenKeyguard, alphaForShadeAndQsExpansion -> 510 if (!isTransitioningToHiddenKeyguard) { 511 emit(alphaForShadeAndQsExpansion) 512 } 513 }, 514 ) 515 .distinctUntilChanged() 516 .dumpWhileCollecting("keyguardAlpha") 517 } 518 519 /** 520 * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB or 521 * DREAMING<->GLANCEABLE_HUB transition or idle on the hub. 522 * 523 * Must return 1.0f when not controlling the alpha since notifications does a min of all the 524 * alpha sources. 525 */ 526 val glanceableHubAlpha: Flow<Float> = 527 combineTransform( 528 isOnGlanceableHubWithoutShade, 529 isOnLockscreen, 530 isDreamingWithoutShade, 531 merge( 532 lockscreenToGlanceableHubTransitionViewModel.notificationAlpha, 533 glanceableHubToLockscreenTransitionViewModel.notificationAlpha, 534 ) 535 // Manually emit on start because [notificationAlpha] only starts emitting 536 // when transitions start. 537 .onStart { emit(1f) } 538 ) { isOnGlanceableHubWithoutShade, isOnLockscreen, isDreamingWithoutShade, alpha, 539 -> 540 if ((isOnGlanceableHubWithoutShade || isDreamingWithoutShade) && !isOnLockscreen) { 541 // Notifications should not be visible on the glanceable hub. 542 // TODO(b/321075734): implement a way to actually set the notifications to 543 // gone while on the hub instead of just adjusting alpha 544 emit(0f) 545 } else if (isOnGlanceableHubWithoutShade) { 546 // We are transitioning between hub and lockscreen, so set the alpha for the 547 // transition animation. 548 emit(alpha) 549 } else { 550 // Not on the hub and no transitions running, return full visibility so we 551 // don't block the notifications from showing. 552 emit(1f) 553 } 554 } 555 .distinctUntilChanged() 556 .dumpWhileCollecting("glanceableHubAlpha") 557 558 /** 559 * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be 560 * translated as the keyguard fades out. 561 */ 562 fun translationY(params: BurnInParameters): Flow<Float> { 563 // with SceneContainer, x translation is handled by views, y is handled by compose 564 SceneContainerFlag.assertInLegacyMode() 565 return combine( 566 aodBurnInViewModel 567 .movement(params) 568 .map { it.translationY.toFloat() } 569 .onStart { emit(0f) }, 570 isOnLockscreenWithoutShade, 571 merge( 572 keyguardInteractor.keyguardTranslationY, 573 occludedToLockscreenTransitionViewModel.lockscreenTranslationY, 574 ) 575 ) { burnInY, isOnLockscreenWithoutShade, translationY -> 576 if (isOnLockscreenWithoutShade) { 577 burnInY + translationY 578 } else { 579 0f 580 } 581 } 582 .dumpWhileCollecting("translationY") 583 } 584 585 /** Horizontal translation to apply to the container. */ 586 val translationX: Flow<Float> = 587 merge( 588 // The container may need to be translated along the X axis as the keyguard fades 589 // out, such as when swiping open the glanceable hub from the lockscreen. 590 lockscreenToGlanceableHubTransitionViewModel.notificationTranslationX, 591 glanceableHubToLockscreenTransitionViewModel.notificationTranslationX, 592 if (SceneContainerFlag.isEnabled) { 593 // The container may need to be translated along the X axis as the unfolded 594 // foldable is folded slightly. 595 unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false) 596 } else { 597 emptyFlow() 598 } 599 ) 600 .dumpWhileCollecting("translationX") 601 602 private val availableHeight: Flow<Float> = 603 if (SceneContainerFlag.isEnabled) { 604 notificationStackAppearanceInteractor.constrainedAvailableSpace.map { it.toFloat() } 605 } else { 606 bounds.map { it.bottom - it.top } 607 } 608 .distinctUntilChanged() 609 .dumpWhileCollecting("availableHeight") 610 611 /** 612 * When on keyguard, there is limited space to display notifications so calculate how many could 613 * be shown. Otherwise, there is no limit since the vertical space will be scrollable. 614 * 615 * When expanding or when the user is interacting with the shade, keep the count stable; do not 616 * emit a value. 617 */ 618 fun getMaxNotifications(calculateSpace: (Float, Boolean) -> Int): Flow<Int> { 619 val showLimitedNotifications = isOnLockscreenWithoutShade 620 val showUnlimitedNotifications = 621 combine( 622 isOnLockscreen, 623 keyguardInteractor.statusBarState, 624 merge( 625 primaryBouncerToGoneTransitionViewModel.showAllNotifications, 626 alternateBouncerToGoneTransitionViewModel.showAllNotifications, 627 ) 628 .onStart { emit(false) } 629 ) { isOnLockscreen, statusBarState, showAllNotifications -> 630 statusBarState == SHADE_LOCKED || !isOnLockscreen || showAllNotifications 631 } 632 633 return combineTransform( 634 showLimitedNotifications, 635 showUnlimitedNotifications, 636 shadeInteractor.isUserInteracting, 637 availableHeight, 638 interactor.notificationStackChanged, 639 interactor.useExtraShelfSpace, 640 ) { flows -> 641 val showLimitedNotifications = flows[0] as Boolean 642 val showUnlimitedNotifications = flows[1] as Boolean 643 val isUserInteracting = flows[2] as Boolean 644 val availableHeight = flows[3] as Float 645 val useExtraShelfSpace = flows[5] as Boolean 646 647 if (!isUserInteracting) { 648 if (showLimitedNotifications) { 649 emit(calculateSpace(availableHeight, useExtraShelfSpace)) 650 } else if (showUnlimitedNotifications) { 651 emit(-1) 652 } 653 } 654 } 655 .distinctUntilChanged() 656 .dumpWhileCollecting("maxNotifications") 657 } 658 659 fun notificationStackChanged() { 660 interactor.notificationStackChanged() 661 } 662 663 data class ConfigurationBasedDimensions( 664 val marginStart: Int, 665 val marginTop: Int, 666 val marginEnd: Int, 667 val marginBottom: Int, 668 val useSplitShade: Boolean, 669 ) 670 } 671