1 /* <lambda>null2 * Copyright 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 @file:OptIn(ExperimentalCoroutinesApi::class) 18 19 package com.android.systemui.scene.domain.startable 20 21 import android.app.StatusBarManager 22 import com.android.compose.animation.scene.ObservableTransitionState 23 import com.android.compose.animation.scene.SceneKey 24 import com.android.internal.logging.UiEventLogger 25 import com.android.systemui.CoreStartable 26 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor 27 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel 28 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor 29 import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor 30 import com.android.systemui.bouncer.shared.logging.BouncerUiEvent 31 import com.android.systemui.classifier.FalsingCollector 32 import com.android.systemui.classifier.FalsingCollectorActual 33 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 34 import com.android.systemui.dagger.SysUISingleton 35 import com.android.systemui.dagger.qualifiers.Application 36 import com.android.systemui.dagger.qualifiers.DisplayId 37 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor 38 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor 39 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor 40 import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource 41 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 42 import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor 43 import com.android.systemui.model.SceneContainerPlugin 44 import com.android.systemui.model.SysUiState 45 import com.android.systemui.model.updateFlags 46 import com.android.systemui.plugins.FalsingManager 47 import com.android.systemui.plugins.FalsingManager.FalsingBeliefListener 48 import com.android.systemui.power.domain.interactor.PowerInteractor 49 import com.android.systemui.power.shared.model.WakeSleepReason 50 import com.android.systemui.scene.data.model.asIterable 51 import com.android.systemui.scene.domain.interactor.SceneBackInteractor 52 import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor 53 import com.android.systemui.scene.domain.interactor.SceneInteractor 54 import com.android.systemui.scene.session.shared.SessionStorage 55 import com.android.systemui.scene.shared.flag.SceneContainerFlag 56 import com.android.systemui.scene.shared.logger.SceneLogger 57 import com.android.systemui.scene.shared.model.Scenes 58 import com.android.systemui.shade.domain.interactor.ShadeInteractor 59 import com.android.systemui.statusbar.NotificationShadeWindowController 60 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor 61 import com.android.systemui.statusbar.phone.CentralSurfaces 62 import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor 63 import com.android.systemui.util.asIndenting 64 import com.android.systemui.util.kotlin.getOrNull 65 import com.android.systemui.util.kotlin.pairwise 66 import com.android.systemui.util.kotlin.sample 67 import com.android.systemui.util.printSection 68 import com.android.systemui.util.println 69 import dagger.Lazy 70 import java.io.PrintWriter 71 import java.util.Optional 72 import javax.inject.Inject 73 import kotlinx.coroutines.CoroutineScope 74 import kotlinx.coroutines.ExperimentalCoroutinesApi 75 import kotlinx.coroutines.channels.awaitClose 76 import kotlinx.coroutines.flow.SharingStarted 77 import kotlinx.coroutines.flow.collectLatest 78 import kotlinx.coroutines.flow.combine 79 import kotlinx.coroutines.flow.distinctUntilChanged 80 import kotlinx.coroutines.flow.distinctUntilChangedBy 81 import kotlinx.coroutines.flow.emptyFlow 82 import kotlinx.coroutines.flow.filter 83 import kotlinx.coroutines.flow.filterIsInstance 84 import kotlinx.coroutines.flow.filterNot 85 import kotlinx.coroutines.flow.first 86 import kotlinx.coroutines.flow.flatMapLatest 87 import kotlinx.coroutines.flow.flowOf 88 import kotlinx.coroutines.flow.map 89 import kotlinx.coroutines.flow.mapNotNull 90 import kotlinx.coroutines.flow.stateIn 91 import kotlinx.coroutines.launch 92 93 /** 94 * Hooks up business logic that manipulates the state of the [SceneInteractor] for the system UI 95 * scene container based on state from other systems. 96 */ 97 @SysUISingleton 98 class SceneContainerStartable 99 @Inject 100 constructor( 101 @Application private val applicationScope: CoroutineScope, 102 private val sceneInteractor: SceneInteractor, 103 private val deviceEntryInteractor: DeviceEntryInteractor, 104 private val deviceUnlockedInteractor: DeviceUnlockedInteractor, 105 private val bouncerInteractor: BouncerInteractor, 106 private val keyguardInteractor: KeyguardInteractor, 107 private val sysUiState: SysUiState, 108 @DisplayId private val displayId: Int, 109 private val sceneLogger: SceneLogger, 110 @FalsingCollectorActual private val falsingCollector: FalsingCollector, 111 private val falsingManager: FalsingManager, 112 private val powerInteractor: PowerInteractor, 113 private val simBouncerInteractor: Lazy<SimBouncerInteractor>, 114 private val authenticationInteractor: Lazy<AuthenticationInteractor>, 115 private val windowController: NotificationShadeWindowController, 116 private val deviceProvisioningInteractor: DeviceProvisioningInteractor, 117 private val centralSurfacesOptLazy: Lazy<Optional<CentralSurfaces>>, 118 private val headsUpInteractor: HeadsUpNotificationInteractor, 119 private val occlusionInteractor: SceneContainerOcclusionInteractor, 120 private val faceUnlockInteractor: DeviceEntryFaceAuthInteractor, 121 private val shadeInteractor: ShadeInteractor, 122 private val uiEventLogger: UiEventLogger, 123 private val sceneBackInteractor: SceneBackInteractor, 124 private val shadeSessionStorage: SessionStorage, 125 private val windowMgrLockscreenVisInteractor: WindowManagerLockscreenVisibilityInteractor, 126 ) : CoreStartable { 127 private val centralSurfaces: CentralSurfaces? 128 get() = centralSurfacesOptLazy.get().getOrNull() 129 130 override fun start() { 131 if (SceneContainerFlag.isEnabled) { 132 sceneLogger.logFrameworkEnabled(isEnabled = true) 133 hydrateVisibility() 134 automaticallySwitchScenes() 135 hydrateSystemUiState() 136 collectFalsingSignals() 137 respondToFalsingDetections() 138 hydrateInteractionState() 139 handleBouncerOverscroll() 140 hydrateWindowController() 141 hydrateBackStack() 142 resetShadeSessions() 143 } else { 144 sceneLogger.logFrameworkEnabled( 145 isEnabled = false, 146 reason = SceneContainerFlag.requirementDescription(), 147 ) 148 } 149 } 150 151 override fun dump(pw: PrintWriter, args: Array<out String>) = 152 pw.asIndenting().run { 153 printSection("SceneContainerFlag") { 154 println("isEnabled", SceneContainerFlag.isEnabled) 155 printSection("requirementDescription") { 156 println(SceneContainerFlag.requirementDescription()) 157 } 158 } 159 } 160 161 private fun resetShadeSessions() { 162 applicationScope.launch { 163 sceneBackInteractor.backStack 164 // We are in a session if either Shade or QuickSettings is on the back stack 165 .map { backStack -> 166 backStack.asIterable().any { it == Scenes.Shade || it == Scenes.QuickSettings } 167 } 168 .distinctUntilChanged() 169 // Once a session has ended, clear the session storage. 170 .filter { inSession -> !inSession } 171 .collect { shadeSessionStorage.clear() } 172 } 173 } 174 175 /** Updates the visibility of the scene container. */ 176 private fun hydrateVisibility() { 177 applicationScope.launch { 178 // TODO(b/296114544): Combine with some global hun state to make it visible! 179 deviceProvisioningInteractor.isDeviceProvisioned 180 .distinctUntilChanged() 181 .flatMapLatest { isAllowedToBeVisible -> 182 if (isAllowedToBeVisible) { 183 combine( 184 sceneInteractor.transitionState.mapNotNull { state -> 185 when (state) { 186 is ObservableTransitionState.Idle -> { 187 if (state.currentScene != Scenes.Gone) { 188 true to "scene is not Gone" 189 } else { 190 false to "scene is Gone" 191 } 192 } 193 is ObservableTransitionState.Transition -> { 194 if (state.fromScene == Scenes.Gone) { 195 true to "scene transitioning away from Gone" 196 } else { 197 null 198 } 199 } 200 } 201 }, 202 headsUpInteractor.isHeadsUpOrAnimatingAway, 203 occlusionInteractor.invisibleDueToOcclusion, 204 ) { 205 visibilityForTransitionState, 206 isHeadsUpOrAnimatingAway, 207 invisibleDueToOcclusion, 208 -> 209 when { 210 isHeadsUpOrAnimatingAway -> true to "showing a HUN" 211 invisibleDueToOcclusion -> false to "invisible due to occlusion" 212 else -> visibilityForTransitionState 213 } 214 } 215 .distinctUntilChanged() 216 } else { 217 flowOf(false to "Device not provisioned or Factory Reset Protection active") 218 } 219 } 220 .collect { (isVisible, loggingReason) -> 221 sceneInteractor.setVisible(isVisible, loggingReason) 222 } 223 } 224 } 225 226 /** Switches between scenes based on ever-changing application state. */ 227 private fun automaticallySwitchScenes() { 228 handleBouncerImeVisibility() 229 handleSimUnlock() 230 handleDeviceUnlockStatus() 231 handlePowerState() 232 handleShadeTouchability() 233 handleSurfaceBehindKeyguardVisibility() 234 } 235 236 private fun handleSurfaceBehindKeyguardVisibility() { 237 applicationScope.launch { 238 sceneInteractor.currentScene.collectLatest { currentScene -> 239 if (currentScene == Scenes.Lockscreen) { 240 // Wait for surface to become visible 241 windowMgrLockscreenVisInteractor.surfaceBehindVisibility.first { it } 242 // Make sure the device is actually unlocked before force-changing the scene 243 deviceUnlockedInteractor.deviceUnlockStatus.first { it.isUnlocked } 244 // Override the current transition, if any, by forcing the scene to Gone 245 sceneInteractor.changeScene( 246 toScene = Scenes.Gone, 247 loggingReason = "surface behind keyguard is visible", 248 ) 249 } 250 } 251 } 252 } 253 254 private fun handleBouncerImeVisibility() { 255 applicationScope.launch { 256 // TODO (b/308001302): Move this to a bouncer specific interactor. 257 bouncerInteractor.onImeHiddenByUser.collectLatest { 258 if (sceneInteractor.currentScene.value == Scenes.Bouncer) { 259 sceneInteractor.changeScene( 260 toScene = Scenes.Lockscreen, // TODO(b/336581871): add sceneState? 261 loggingReason = "IME hidden", 262 ) 263 } 264 } 265 } 266 } 267 268 private fun handleSimUnlock() { 269 applicationScope.launch { 270 simBouncerInteractor 271 .get() 272 .isAnySimSecure 273 .sample(deviceUnlockedInteractor.deviceUnlockStatus, ::Pair) 274 .collect { (isAnySimLocked, unlockStatus) -> 275 when { 276 isAnySimLocked -> { 277 switchToScene( 278 // TODO(b/336581871): add sceneState? 279 targetSceneKey = Scenes.Bouncer, 280 loggingReason = "Need to authenticate locked SIM card." 281 ) 282 } 283 unlockStatus.isUnlocked && 284 deviceEntryInteractor.canSwipeToEnter.value == false -> { 285 switchToScene( 286 // TODO(b/336581871): add sceneState? 287 targetSceneKey = Scenes.Gone, 288 loggingReason = 289 "All SIM cards unlocked and device already unlocked and " + 290 "lockscreen doesn't require a swipe to dismiss." 291 ) 292 } 293 else -> { 294 switchToScene( 295 // TODO(b/336581871): add sceneState? 296 targetSceneKey = Scenes.Lockscreen, 297 loggingReason = 298 "All SIM cards unlocked and device still locked" + 299 " or lockscreen still requires a swipe to dismiss." 300 ) 301 } 302 } 303 } 304 } 305 } 306 307 private fun handleDeviceUnlockStatus() { 308 applicationScope.launch { 309 // Track the previous scene (sans Bouncer), so that we know where to go when the device 310 // is unlocked whilst on the bouncer. 311 val previousScene = 312 sceneBackInteractor.backScene 313 .filterNot { it == Scenes.Bouncer } 314 .stateIn(this, SharingStarted.Eagerly, initialValue = null) 315 deviceUnlockedInteractor.deviceUnlockStatus 316 .mapNotNull { deviceUnlockStatus -> 317 val renderedScenes = 318 when (val transitionState = sceneInteractor.transitionState.value) { 319 is ObservableTransitionState.Idle -> setOf(transitionState.currentScene) 320 is ObservableTransitionState.Transition -> 321 setOf( 322 transitionState.fromScene, 323 transitionState.toScene, 324 ) 325 } 326 val isOnLockscreen = renderedScenes.contains(Scenes.Lockscreen) 327 val isOnBouncer = renderedScenes.contains(Scenes.Bouncer) 328 if (!deviceUnlockStatus.isUnlocked) { 329 return@mapNotNull if (isOnLockscreen || isOnBouncer) { 330 // Already on lockscreen or bouncer, no need to change scenes. 331 null 332 } else { 333 // The device locked while on a scene that's not Lockscreen or Bouncer, 334 // go to Lockscreen. 335 Scenes.Lockscreen to 336 "device locked in non-Lockscreen and non-Bouncer scene" 337 } 338 } 339 340 if ( 341 isOnBouncer && 342 deviceUnlockStatus.deviceUnlockSource == DeviceUnlockSource.TrustAgent 343 ) { 344 uiEventLogger.log(BouncerUiEvent.BOUNCER_DISMISS_EXTENDED_ACCESS) 345 } 346 when { 347 isOnBouncer -> 348 // When the device becomes unlocked in Bouncer, go to previous scene, 349 // or Gone. 350 if (previousScene.value == Scenes.Lockscreen) { 351 Scenes.Gone to "device was unlocked in Bouncer scene" 352 } else { 353 val prevScene = previousScene.value 354 (prevScene ?: Scenes.Gone) to 355 "device was unlocked in Bouncer scene, from sceneKey=$prevScene" 356 } 357 isOnLockscreen -> 358 // The lockscreen should be dismissed automatically in 2 scenarios: 359 // 1. When face auth bypass is enabled and authentication happens while 360 // the user is on the lockscreen. 361 // 2. Whenever the user authenticates using an active authentication 362 // mechanism like fingerprint auth. Since canSwipeToEnter is true 363 // when the user is passively authenticated, the false value here 364 // when the unlock state changes indicates this is an active 365 // authentication attempt. 366 when { 367 deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen == 368 true -> 369 Scenes.Gone to 370 "device has been unlocked on lockscreen with bypass " + 371 "enabled or using an active authentication " + 372 "mechanism: ${deviceUnlockStatus.deviceUnlockSource}" 373 else -> null 374 } 375 // Not on lockscreen or bouncer, so remain in the current scene. 376 else -> null 377 } 378 } 379 .collect { (targetSceneKey, loggingReason) -> 380 switchToScene( 381 targetSceneKey = targetSceneKey, 382 loggingReason = loggingReason, 383 ) 384 } 385 } 386 } 387 388 private fun handlePowerState() { 389 applicationScope.launch { 390 powerInteractor.detailedWakefulness.collect { wakefulness -> 391 // Detect a double-tap-power-button gesture that was started while the device was 392 // still awake. 393 if (wakefulness.isAsleep()) return@collect 394 if (!wakefulness.powerButtonLaunchGestureTriggered) return@collect 395 if (wakefulness.lastSleepReason != WakeSleepReason.POWER_BUTTON) return@collect 396 397 // If we're mid-transition from Gone to Lockscreen due to the first power button 398 // press, then return to Gone. 399 val transition: ObservableTransitionState.Transition = 400 sceneInteractor.transitionState.value as? ObservableTransitionState.Transition 401 ?: return@collect 402 if ( 403 transition.fromScene == Scenes.Gone && transition.toScene == Scenes.Lockscreen 404 ) { 405 switchToScene( 406 targetSceneKey = Scenes.Gone, 407 loggingReason = "double-tap power gesture", 408 ) 409 } 410 } 411 } 412 applicationScope.launch { 413 powerInteractor.isAsleep.collect { isAsleep -> 414 if (isAsleep) { 415 switchToScene( 416 // TODO(b/336581871): add sceneState? 417 targetSceneKey = Scenes.Lockscreen, 418 loggingReason = "device is starting to sleep", 419 ) 420 } else { 421 val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value 422 val isUnlocked = deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked 423 if (isUnlocked && canSwipeToEnter == false) { 424 val isTransitioningToLockscreen = 425 sceneInteractor.transitioningTo.value == Scenes.Lockscreen 426 if (!isTransitioningToLockscreen) { 427 switchToScene( 428 targetSceneKey = Scenes.Gone, 429 loggingReason = 430 "device is waking up while unlocked without the ability to" + 431 " swipe up on lockscreen to enter and not on or" + 432 " transitioning to, the lockscreen scene.", 433 ) 434 } 435 } else if ( 436 authenticationInteractor.get().getAuthenticationMethod() == 437 AuthenticationMethodModel.Sim 438 ) { 439 switchToScene( 440 targetSceneKey = Scenes.Bouncer, 441 loggingReason = "device is starting to wake up with a locked sim", 442 ) 443 } 444 } 445 } 446 } 447 } 448 449 private fun handleShadeTouchability() { 450 applicationScope.launch { 451 shadeInteractor.isShadeTouchable 452 .distinctUntilChanged() 453 .filter { !it } 454 .collect { 455 switchToScene( 456 targetSceneKey = Scenes.Lockscreen, 457 loggingReason = "device became non-interactive", 458 ) 459 } 460 } 461 } 462 463 /** Keeps [SysUiState] up-to-date */ 464 private fun hydrateSystemUiState() { 465 applicationScope.launch { 466 combine( 467 sceneInteractor.transitionState 468 .mapNotNull { it as? ObservableTransitionState.Idle } 469 .map { it.currentScene } 470 .distinctUntilChanged(), 471 occlusionInteractor.invisibleDueToOcclusion, 472 ) { sceneKey, invisibleDueToOcclusion -> 473 SceneContainerPlugin.SceneContainerPluginState( 474 scene = sceneKey, 475 invisibleDueToOcclusion = invisibleDueToOcclusion, 476 ) 477 } 478 .collect { sceneContainerPluginState -> 479 sysUiState.updateFlags( 480 displayId, 481 *SceneContainerPlugin.EvaluatorByFlag.map { (flag, evaluator) -> 482 flag to evaluator.invoke(sceneContainerPluginState) 483 } 484 .toTypedArray(), 485 ) 486 } 487 } 488 } 489 490 private fun hydrateWindowController() { 491 applicationScope.launch { 492 sceneInteractor.transitionState 493 .mapNotNull { transitionState -> 494 (transitionState as? ObservableTransitionState.Idle)?.currentScene 495 } 496 .distinctUntilChanged() 497 .collect { sceneKey -> 498 windowController.setNotificationShadeFocusable(sceneKey != Scenes.Gone) 499 } 500 } 501 502 applicationScope.launch { 503 deviceEntryInteractor.isDeviceEntered.collect { isDeviceEntered -> 504 windowController.setKeyguardShowing(!isDeviceEntered) 505 } 506 } 507 508 applicationScope.launch { 509 sceneInteractor.currentScene 510 .map { it == Scenes.Bouncer } 511 .distinctUntilChanged() 512 .collect { isBouncerShowing -> 513 windowController.setBouncerShowing(isBouncerShowing) 514 } 515 } 516 517 applicationScope.launch { 518 occlusionInteractor.invisibleDueToOcclusion.collect { invisibleDueToOcclusion -> 519 windowController.setKeyguardOccluded(invisibleDueToOcclusion) 520 } 521 } 522 } 523 524 /** Collects and reports signals into the falsing system. */ 525 private fun collectFalsingSignals() { 526 applicationScope.launch { 527 deviceEntryInteractor.isDeviceEntered.collect { isLockscreenDismissed -> 528 if (isLockscreenDismissed) { 529 falsingCollector.onSuccessfulUnlock() 530 } 531 } 532 } 533 534 applicationScope.launch { 535 keyguardInteractor.isDozing.collect { isDozing -> 536 falsingCollector.setShowingAod(isDozing) 537 } 538 } 539 540 applicationScope.launch { 541 keyguardInteractor.isAodAvailable 542 .flatMapLatest { isAodAvailable -> 543 if (!isAodAvailable) { 544 powerInteractor.detailedWakefulness 545 } else { 546 emptyFlow() 547 } 548 } 549 .distinctUntilChangedBy { it.isAwake() } 550 .collect { wakefulness -> 551 when { 552 wakefulness.isAwakeFromTouch() -> falsingCollector.onScreenOnFromTouch() 553 wakefulness.isAwake() -> falsingCollector.onScreenTurningOn() 554 wakefulness.isAsleep() -> falsingCollector.onScreenOff() 555 } 556 } 557 } 558 559 applicationScope.launch { 560 sceneInteractor.currentScene 561 .map { it == Scenes.Bouncer } 562 .distinctUntilChanged() 563 .collect { switchedToBouncerScene -> 564 if (switchedToBouncerScene) { 565 falsingCollector.onBouncerShown() 566 } else { 567 falsingCollector.onBouncerHidden() 568 } 569 } 570 } 571 } 572 573 /** Switches to the lockscreen when falsing is detected. */ 574 private fun respondToFalsingDetections() { 575 applicationScope.launch { 576 conflatedCallbackFlow { 577 val listener = FalsingBeliefListener { trySend(Unit) } 578 falsingManager.addFalsingBeliefListener(listener) 579 awaitClose { falsingManager.removeFalsingBeliefListener(listener) } 580 } 581 .collect { switchToScene(Scenes.Lockscreen, "Falsing detected.") } 582 } 583 } 584 585 /** Keeps the interaction state of [CentralSurfaces] up-to-date. */ 586 private fun hydrateInteractionState() { 587 applicationScope.launch { 588 deviceUnlockedInteractor.deviceUnlockStatus 589 .map { !it.isUnlocked } 590 .flatMapLatest { isDeviceLocked -> 591 if (isDeviceLocked) { 592 sceneInteractor.transitionState 593 .mapNotNull { it as? ObservableTransitionState.Idle } 594 .map { it.currentScene } 595 .distinctUntilChanged() 596 .map { sceneKey -> 597 when (sceneKey) { 598 // When locked, showing the lockscreen scene should be reported 599 // as "interacting" while showing other scenes should report as 600 // "not interacting". 601 // 602 // This is done here in order to match the legacy 603 // implementation. The real reason why is lost to lore and myth. 604 Scenes.Lockscreen -> true 605 Scenes.Bouncer -> false 606 Scenes.Shade -> false 607 Scenes.NotificationsShade -> false 608 else -> null 609 } 610 } 611 } else { 612 flowOf(null) 613 } 614 } 615 .collect { isInteractingOrNull -> 616 isInteractingOrNull?.let { isInteracting -> 617 centralSurfaces?.setInteracting( 618 StatusBarManager.WINDOW_STATUS_BAR, 619 isInteracting, 620 ) 621 } 622 } 623 } 624 } 625 626 private fun handleBouncerOverscroll() { 627 applicationScope.launch { 628 sceneInteractor.transitionState 629 // Only consider transitions. 630 .filterIsInstance<ObservableTransitionState.Transition>() 631 // Only consider user-initiated (e.g. drags) that go from bouncer to lockscreen. 632 .filter { transition -> 633 transition.fromScene == Scenes.Bouncer && 634 transition.toScene == Scenes.Lockscreen && 635 transition.isInitiatedByUserInput 636 } 637 .flatMapLatest { it.progress } 638 // Figure out the direction of scrolling. 639 .map { progress -> 640 when { 641 progress > 0 -> 1 642 progress < 0 -> -1 643 else -> 0 644 } 645 } 646 .distinctUntilChanged() 647 // Only consider negative scrolling, AKA overscroll. 648 .filter { it == -1 } 649 .collect { faceUnlockInteractor.onSwipeUpOnBouncer() } 650 } 651 } 652 653 private fun switchToScene(targetSceneKey: SceneKey, loggingReason: String) { 654 sceneInteractor.changeScene( 655 toScene = targetSceneKey, 656 loggingReason = loggingReason, 657 ) 658 } 659 660 private fun hydrateBackStack() { 661 applicationScope.launch { 662 sceneInteractor.currentScene.pairwise().collect { (from, to) -> 663 sceneBackInteractor.onSceneChange(from = from, to = to) 664 } 665 } 666 } 667 } 668