1 /* <lambda>null2 * Copyright (C) 2022 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.keyguard.domain.interactor 21 22 import android.app.StatusBarManager 23 import android.graphics.Point 24 import android.util.MathUtils 25 import com.android.app.animation.Interpolators 26 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository 27 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging 28 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 29 import com.android.systemui.common.shared.model.NotificationContainerBounds 30 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor 31 import com.android.systemui.dagger.SysUISingleton 32 import com.android.systemui.dagger.qualifiers.Application 33 import com.android.systemui.keyguard.MigrateClocksToBlueprint 34 import com.android.systemui.keyguard.data.repository.KeyguardRepository 35 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel 36 import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel 37 import com.android.systemui.keyguard.shared.model.DozeStateModel 38 import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff 39 import com.android.systemui.keyguard.shared.model.DozeTransitionModel 40 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD 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.StatusBarState 44 import com.android.systemui.power.domain.interactor.PowerInteractor 45 import com.android.systemui.res.R 46 import com.android.systemui.scene.domain.interactor.SceneInteractor 47 import com.android.systemui.scene.shared.flag.SceneContainerFlag 48 import com.android.systemui.scene.shared.model.Scenes 49 import com.android.systemui.shade.data.repository.ShadeRepository 50 import com.android.systemui.statusbar.CommandQueue 51 import com.android.systemui.statusbar.notification.NotificationUtils.interpolate 52 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor 53 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine 54 import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter 55 import com.android.systemui.util.kotlin.pairwise 56 import com.android.systemui.util.kotlin.sample 57 import javax.inject.Inject 58 import javax.inject.Provider 59 import kotlinx.coroutines.CoroutineScope 60 import kotlinx.coroutines.ExperimentalCoroutinesApi 61 import kotlinx.coroutines.channels.awaitClose 62 import kotlinx.coroutines.delay 63 import kotlinx.coroutines.flow.Flow 64 import kotlinx.coroutines.flow.MutableStateFlow 65 import kotlinx.coroutines.flow.SharingStarted 66 import kotlinx.coroutines.flow.StateFlow 67 import kotlinx.coroutines.flow.asStateFlow 68 import kotlinx.coroutines.flow.combine 69 import kotlinx.coroutines.flow.combineTransform 70 import kotlinx.coroutines.flow.debounce 71 import kotlinx.coroutines.flow.distinctUntilChanged 72 import kotlinx.coroutines.flow.filter 73 import kotlinx.coroutines.flow.first 74 import kotlinx.coroutines.flow.flatMapLatest 75 import kotlinx.coroutines.flow.flow 76 import kotlinx.coroutines.flow.flowOf 77 import kotlinx.coroutines.flow.map 78 import kotlinx.coroutines.flow.merge 79 import kotlinx.coroutines.flow.onStart 80 import kotlinx.coroutines.flow.stateIn 81 import kotlinx.coroutines.flow.transform 82 83 /** 84 * Encapsulates business-logic related to the keyguard but not to a more specific part within it. 85 */ 86 @SysUISingleton 87 class KeyguardInteractor 88 @Inject 89 constructor( 90 private val repository: KeyguardRepository, 91 private val commandQueue: CommandQueue, 92 powerInteractor: PowerInteractor, 93 bouncerRepository: KeyguardBouncerRepository, 94 configurationInteractor: ConfigurationInteractor, 95 shadeRepository: ShadeRepository, 96 private val keyguardTransitionInteractor: KeyguardTransitionInteractor, 97 sceneInteractorProvider: Provider<SceneInteractor>, 98 private val fromGoneTransitionInteractor: Provider<FromGoneTransitionInteractor>, 99 private val fromLockscreenTransitionInteractor: Provider<FromLockscreenTransitionInteractor>, 100 sharedNotificationContainerInteractor: Provider<SharedNotificationContainerInteractor>, 101 @Application applicationScope: CoroutineScope, 102 ) { 103 // TODO(b/296118689): move to a repository 104 private val _notificationPlaceholderBounds = MutableStateFlow(NotificationContainerBounds()) 105 106 // When going to AOD, we interpolate bounds when receiving the new bounds 107 // When going back to LS, we'll apply new bounds directly 108 private val _nonSplitShadeNotifciationPlaceholderBounds = 109 _notificationPlaceholderBounds.pairwise().flatMapLatest { (oldBounds, newBounds) -> 110 val lastChangeStep = keyguardTransitionInteractor.transitionState.first() 111 if (lastChangeStep.to == AOD) { 112 keyguardTransitionInteractor.transitionState.map { step -> 113 val startingProgress = lastChangeStep.value 114 val progress = step.value 115 if (step.to == AOD && progress >= startingProgress && startingProgress < 1f) { 116 val adjustedProgress = 117 ((progress - startingProgress) / (1F - startingProgress)).coerceIn( 118 0F, 119 1F 120 ) 121 val top = interpolate(oldBounds.top, newBounds.top, adjustedProgress) 122 val bottom = 123 interpolate( 124 oldBounds.bottom, 125 newBounds.bottom, 126 adjustedProgress.coerceIn(0F, 1F) 127 ) 128 NotificationContainerBounds(top = top, bottom = bottom) 129 } else { 130 newBounds 131 } 132 } 133 } else { 134 flow { emit(newBounds) } 135 } 136 } 137 138 /** Bounds of the notification container. */ 139 val notificationContainerBounds: StateFlow<NotificationContainerBounds> by lazy { 140 SceneContainerFlag.assertInLegacyMode() 141 combine( 142 _notificationPlaceholderBounds, 143 _nonSplitShadeNotifciationPlaceholderBounds, 144 sharedNotificationContainerInteractor.get().configurationBasedDimensions, 145 ) { bounds, nonSplitShadeBounds: NotificationContainerBounds, cfg -> 146 // We offset the placeholder bounds by the configured top margin to account for 147 // legacy placement behavior within notifications for splitshade. 148 if (MigrateClocksToBlueprint.isEnabled) { 149 if (cfg.useSplitShade) { 150 bounds.copy(bottom = bounds.bottom - cfg.keyguardSplitShadeTopMargin) 151 } else { 152 nonSplitShadeBounds 153 } 154 } else bounds 155 } 156 .stateIn( 157 scope = applicationScope, 158 started = SharingStarted.WhileSubscribed(), 159 initialValue = NotificationContainerBounds(), 160 ) 161 } 162 163 fun setNotificationContainerBounds(position: NotificationContainerBounds) { 164 SceneContainerFlag.assertInLegacyMode() 165 _notificationPlaceholderBounds.value = position 166 } 167 168 /** 169 * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at 170 * all. 171 */ 172 val dozeAmount: Flow<Float> = repository.linearDozeAmount 173 174 /** Whether the system is in doze mode. */ 175 val isDozing: StateFlow<Boolean> = repository.isDozing 176 177 /** Receive an event for doze time tick */ 178 val dozeTimeTick: Flow<Long> = repository.dozeTimeTick 179 180 /** Whether Always-on Display mode is available. */ 181 val isAodAvailable: StateFlow<Boolean> = repository.isAodAvailable 182 183 /** Doze transition information. */ 184 val dozeTransitionModel: Flow<DozeTransitionModel> = repository.dozeTransitionModel 185 186 val isPulsing: Flow<Boolean> = dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING } 187 188 /** 189 * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true, 190 * but not vice-versa. 191 */ 192 val isDreaming: Flow<Boolean> = repository.isDreaming 193 194 /** Whether the system is dreaming with an overlay active */ 195 val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay 196 197 /** Whether the system is dreaming and the active dream is hosted in lockscreen */ 198 val isActiveDreamLockscreenHosted: StateFlow<Boolean> = repository.isActiveDreamLockscreenHosted 199 200 /** Event for when the camera gesture is detected */ 201 val onCameraLaunchDetected: Flow<CameraLaunchSourceModel> = conflatedCallbackFlow { 202 val callback = 203 object : CommandQueue.Callbacks { 204 override fun onCameraLaunchGestureDetected(source: Int) { 205 trySendWithFailureLogging( 206 cameraLaunchSourceIntToModel(source), 207 TAG, 208 "updated onCameraLaunchGestureDetected" 209 ) 210 } 211 } 212 213 commandQueue.addCallback(callback) 214 215 awaitClose { commandQueue.removeCallback(callback) } 216 } 217 218 /** 219 * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means 220 * that doze mode is not running and DREAMING is ok to commence. 221 * 222 * Allow a brief moment to prevent rapidly oscillating between true/false signals. 223 */ 224 val isAbleToDream: Flow<Boolean> = 225 merge(isDreaming, isDreamingWithOverlay) 226 .combine(dozeTransitionModel) { isDreaming, dozeTransitionModel -> 227 isDreaming && isDozeOff(dozeTransitionModel.to) 228 } 229 .sample(powerInteractor.isAwake) { isAbleToDream, isAwake -> isAbleToDream && isAwake } 230 .debounce(50L) 231 .stateIn( 232 scope = applicationScope, 233 started = SharingStarted.WhileSubscribed(), 234 initialValue = false, 235 ) 236 237 /** Whether the keyguard is showing or not. */ 238 @Deprecated("Use KeyguardTransitionInteractor + KeyguardState") 239 val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing 240 241 /** Whether the keyguard is dismissible or not. */ 242 val isKeyguardDismissible: StateFlow<Boolean> = repository.isKeyguardDismissible 243 244 /** Whether the keyguard is occluded (covered by an activity). */ 245 @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.OCCLUDED") 246 val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded 247 248 /** Whether the keyguard is going away. */ 249 @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.GONE") 250 val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway 251 252 /** Keyguard can be clipped at the top as the shade is dragged */ 253 val topClippingBounds: Flow<Int?> by lazy { 254 repository.topClippingBounds 255 .sampleFilter( 256 keyguardTransitionInteractor 257 .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE) 258 .onStart { emit(0f) } 259 ) { goneValue -> 260 goneValue != 1f 261 } 262 .distinctUntilChanged() 263 } 264 265 /** Last point that [KeyguardRootView] view was tapped */ 266 val lastRootViewTapPosition: Flow<Point?> = repository.lastRootViewTapPosition.asStateFlow() 267 268 /** Is the ambient indication area visible? */ 269 val ambientIndicationVisible: Flow<Boolean> = repository.ambientIndicationVisible.asStateFlow() 270 271 /** Whether the primary bouncer is showing or not. */ 272 @JvmField val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerShow 273 274 /** Whether the alternate bouncer is showing or not. */ 275 val alternateBouncerShowing: Flow<Boolean> = 276 bouncerRepository.alternateBouncerVisible.sample(isAbleToDream) { 277 alternateBouncerVisible, 278 isAbleToDream -> 279 if (isAbleToDream) { 280 // If the alternate bouncer will show over a dream, it is likely that the dream has 281 // requested a dismissal, which will stop the dream. By delaying this slightly, the 282 // DREAMING->LOCKSCREEN transition will now happen first, followed by 283 // LOCKSCREEN->ALTERNATE_BOUNCER. 284 delay(600L) 285 } 286 alternateBouncerVisible 287 } 288 289 /** Observable for the [StatusBarState] */ 290 val statusBarState: Flow<StatusBarState> = repository.statusBarState 291 292 /** Observable for [BiometricUnlockModel] when biometrics are used to unlock the device. */ 293 val biometricUnlockState: Flow<BiometricUnlockModel> = repository.biometricUnlockState 294 295 /** Keyguard is present and is not occluded. */ 296 val isKeyguardVisible: Flow<Boolean> = 297 combine(isKeyguardShowing, isKeyguardOccluded) { showing, occluded -> showing && !occluded } 298 299 /** Whether camera is launched over keyguard. */ 300 val isSecureCameraActive: Flow<Boolean> by lazy { 301 combine( 302 isKeyguardVisible, 303 primaryBouncerShowing, 304 onCameraLaunchDetected, 305 ) { isKeyguardVisible, isPrimaryBouncerShowing, cameraLaunchEvent -> 306 when { 307 isKeyguardVisible -> false 308 isPrimaryBouncerShowing -> false 309 else -> cameraLaunchEvent == CameraLaunchSourceModel.POWER_DOUBLE_TAP 310 } 311 } 312 .onStart { emit(false) } 313 } 314 315 /** The approximate location on the screen of the fingerprint sensor, if one is available. */ 316 val fingerprintSensorLocation: Flow<Point?> = repository.fingerprintSensorLocation 317 318 /** The approximate location on the screen of the face unlock sensor, if one is available. */ 319 val faceSensorLocation: Flow<Point?> = repository.faceSensorLocation 320 321 @Deprecated("Use the relevant TransitionViewModel") 322 val keyguardAlpha: Flow<Float> = repository.keyguardAlpha 323 324 /** 325 * When the lockscreen can be dismissed, emit an alpha value as the user swipes up. This is 326 * useful just before the code commits to moving to GONE. 327 * 328 * This uses legacyShadeExpansion to process swipe up events. In the future, the touch input 329 * signal should be sent directly to transitions. 330 */ 331 val dismissAlpha: Flow<Float> = 332 shadeRepository.legacyShadeExpansion 333 .sampleCombine( 334 statusBarState, 335 keyguardTransitionInteractor.currentKeyguardState, 336 keyguardTransitionInteractor.transitionState, 337 isKeyguardDismissible, 338 ) 339 .filter { (_, _, _, step, _) -> !step.transitionState.isTransitioning() } 340 .transform { 341 ( 342 legacyShadeExpansion, 343 statusBarState, 344 currentKeyguardState, 345 step, 346 isKeyguardDismissible) -> 347 if ( 348 statusBarState == StatusBarState.KEYGUARD && 349 isKeyguardDismissible && 350 currentKeyguardState == LOCKSCREEN && 351 legacyShadeExpansion != 1f 352 ) { 353 emit(MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, legacyShadeExpansion)) 354 } else if (legacyShadeExpansion == 0f || legacyShadeExpansion == 1f) { 355 // Resets alpha state 356 emit(1f) 357 } 358 } 359 .onStart { emit(1f) } 360 .distinctUntilChanged() 361 362 val keyguardTranslationY: Flow<Float> = 363 configurationInteractor 364 .dimensionPixelSize(R.dimen.keyguard_translate_distance_on_swipe_up) 365 .flatMapLatest { translationDistance -> 366 combineTransform( 367 shadeRepository.legacyShadeExpansion.onStart { emit(0f) }, 368 keyguardTransitionInteractor.transitionValue(GONE).onStart { emit(0f) }, 369 ) { legacyShadeExpansion, goneValue -> 370 val isLegacyShadeInResetPosition = 371 legacyShadeExpansion == 0f || legacyShadeExpansion == 1f 372 if (goneValue == 1f || (goneValue == 0f && isLegacyShadeInResetPosition)) { 373 // Reset the translation value 374 emit(0f) 375 } else if (!isLegacyShadeInResetPosition) { 376 // On swipe up, translate the keyguard to reveal the bouncer, OR a GONE 377 // transition is running, which means this is a swipe to dismiss. Values of 378 // 0f and 1f need to be ignored in the legacy shade expansion. These can 379 // flip arbitrarily as the legacy shade is reset, and would cause the 380 // translation value to jump around unexpectedly. 381 emit( 382 MathUtils.lerp( 383 translationDistance, 384 0, 385 Interpolators.FAST_OUT_LINEAR_IN.getInterpolation( 386 legacyShadeExpansion 387 ), 388 ) 389 ) 390 } 391 } 392 } 393 .stateIn( 394 scope = applicationScope, 395 started = SharingStarted.WhileSubscribed(), 396 initialValue = 0f, 397 ) 398 399 val clockShouldBeCentered: Flow<Boolean> = repository.clockShouldBeCentered 400 401 /** Whether to animate the next doze mode transition. */ 402 val animateDozingTransitions: Flow<Boolean> by lazy { 403 if (SceneContainerFlag.isEnabled) { 404 sceneInteractorProvider 405 .get() 406 .transitioningTo 407 .map { it == Scenes.Lockscreen } 408 .distinctUntilChanged() 409 .flatMapLatest { isTransitioningToLockscreenScene -> 410 if (isTransitioningToLockscreenScene) { 411 flowOf(false) 412 } else { 413 repository.animateBottomAreaDozingTransitions 414 } 415 } 416 } else { 417 repository.animateBottomAreaDozingTransitions 418 } 419 } 420 421 /** 422 * Whether the primary authentication is required for the given user due to lockdown or 423 * encryption after reboot. 424 */ 425 val isEncryptedOrLockdown: Flow<Boolean> = repository.isEncryptedOrLockdown 426 427 fun dozeTransitionTo(vararg states: DozeStateModel): Flow<DozeTransitionModel> { 428 return dozeTransitionModel.filter { states.contains(it.to) } 429 } 430 431 fun isKeyguardShowing(): Boolean { 432 return repository.isKeyguardShowing() 433 } 434 435 private fun cameraLaunchSourceIntToModel(value: Int): CameraLaunchSourceModel { 436 return when (value) { 437 StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE -> CameraLaunchSourceModel.WIGGLE 438 StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP -> 439 CameraLaunchSourceModel.POWER_DOUBLE_TAP 440 StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER -> 441 CameraLaunchSourceModel.LIFT_TRIGGER 442 StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE -> 443 CameraLaunchSourceModel.QUICK_AFFORDANCE 444 else -> throw IllegalArgumentException("Invalid CameraLaunchSourceModel value: $value") 445 } 446 } 447 448 fun setIsActiveDreamLockscreenHosted(isLockscreenHosted: Boolean) { 449 repository.setIsActiveDreamLockscreenHosted(isLockscreenHosted) 450 } 451 452 /** Sets whether quick settings or quick-quick settings is visible. */ 453 fun setQuickSettingsVisible(isVisible: Boolean) { 454 repository.setQuickSettingsVisible(isVisible) 455 } 456 457 fun setAlpha(alpha: Float) { 458 repository.setKeyguardAlpha(alpha) 459 } 460 461 fun setAnimateDozingTransitions(animate: Boolean) { 462 repository.setAnimateDozingTransitions(animate) 463 } 464 465 fun setClockShouldBeCentered(shouldBeCentered: Boolean) { 466 repository.setClockShouldBeCentered(shouldBeCentered) 467 } 468 469 fun setLastRootViewTapPosition(point: Point?) { 470 repository.lastRootViewTapPosition.value = point 471 } 472 473 fun setAmbientIndicationVisible(isVisible: Boolean) { 474 repository.ambientIndicationVisible.value = isVisible 475 } 476 477 fun keyguardDoneAnimationsFinished() { 478 repository.keyguardDoneAnimationsFinished() 479 } 480 481 fun setTopClippingBounds(top: Int?) { 482 repository.topClippingBounds.value = top 483 } 484 485 fun setDreaming(isDreaming: Boolean) { 486 repository.setDreaming(isDreaming) 487 } 488 489 /** Temporary shim, until [KeyguardWmStateRefactor] is enabled */ 490 fun showKeyguard() { 491 fromGoneTransitionInteractor.get().showKeyguard() 492 } 493 494 /** Temporary shim, until [KeyguardWmStateRefactor] is enabled */ 495 fun dismissKeyguard() { 496 fromLockscreenTransitionInteractor.get().dismissKeyguard() 497 } 498 499 companion object { 500 private const val TAG = "KeyguardInteractor" 501 } 502 } 503