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 package com.android.systemui.scene.domain.interactor 18 19 import com.android.compose.animation.scene.ObservableTransitionState 20 import com.android.compose.animation.scene.SceneKey 21 import com.android.compose.animation.scene.TransitionKey 22 import com.android.systemui.dagger.SysUISingleton 23 import com.android.systemui.dagger.qualifiers.Application 24 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor 25 import com.android.systemui.scene.data.repository.SceneContainerRepository 26 import com.android.systemui.scene.domain.resolver.SceneResolver 27 import com.android.systemui.scene.shared.logger.SceneLogger 28 import com.android.systemui.scene.shared.model.SceneFamilies 29 import com.android.systemui.scene.shared.model.Scenes 30 import com.android.systemui.util.kotlin.pairwiseBy 31 import dagger.Lazy 32 import javax.inject.Inject 33 import kotlinx.coroutines.CoroutineScope 34 import kotlinx.coroutines.ExperimentalCoroutinesApi 35 import kotlinx.coroutines.flow.Flow 36 import kotlinx.coroutines.flow.SharingStarted 37 import kotlinx.coroutines.flow.StateFlow 38 import kotlinx.coroutines.flow.combine 39 import kotlinx.coroutines.flow.emitAll 40 import kotlinx.coroutines.flow.flatMapLatest 41 import kotlinx.coroutines.flow.flow 42 import kotlinx.coroutines.flow.flowOf 43 import kotlinx.coroutines.flow.map 44 import kotlinx.coroutines.flow.stateIn 45 46 /** 47 * Generic business logic and app state accessors for the scene framework. 48 * 49 * Note that this class should not depend on state or logic of other modules or features. Instead, 50 * other feature modules should depend on and call into this class when their parts of the 51 * application state change. 52 */ 53 @SysUISingleton 54 class SceneInteractor 55 @Inject 56 constructor( 57 @Application private val applicationScope: CoroutineScope, 58 private val repository: SceneContainerRepository, 59 private val logger: SceneLogger, 60 private val sceneFamilyResolvers: Lazy<Map<SceneKey, @JvmSuppressWildcards SceneResolver>>, 61 private val deviceUnlockedInteractor: DeviceUnlockedInteractor, 62 ) { 63 64 interface OnSceneAboutToChangeListener { 65 66 /** 67 * Notifies that the scene is about to change to [toScene]. 68 * 69 * The implementation can choose to consume the [sceneState] to prepare the incoming scene. 70 */ 71 fun onSceneAboutToChange(toScene: SceneKey, sceneState: Any?) 72 } 73 74 private val onSceneAboutToChangeListener = mutableSetOf<OnSceneAboutToChangeListener>() 75 76 /** 77 * The current scene. 78 * 79 * Note that during a transition between scenes, more than one scene might be rendered but only 80 * one is considered the committed/current scene. 81 */ 82 val currentScene: StateFlow<SceneKey> = 83 repository.currentScene 84 .pairwiseBy(initialValue = repository.currentScene.value) { from, to -> 85 logger.logSceneChangeCommitted( 86 from = from, 87 to = to, 88 ) 89 to 90 } 91 .stateIn( 92 scope = applicationScope, 93 started = SharingStarted.WhileSubscribed(), 94 initialValue = repository.currentScene.value, 95 ) 96 97 /** 98 * The current state of the transition. 99 * 100 * Consumers should use this state to know: 101 * 1. Whether there is an ongoing transition or if the system is at rest. 102 * 2. When transitioning, which scenes are being transitioned between. 103 * 3. When transitioning, what the progress of the transition is. 104 */ 105 val transitionState: StateFlow<ObservableTransitionState> = repository.transitionState 106 107 /** 108 * The key of the scene that the UI is currently transitioning to or `null` if there is no 109 * active transition at the moment. 110 * 111 * This is a convenience wrapper around [transitionState], meant for flow-challenged consumers 112 * like Java code. 113 */ 114 val transitioningTo: StateFlow<SceneKey?> = 115 transitionState 116 .map { state -> (state as? ObservableTransitionState.Transition)?.toScene } 117 .stateIn( 118 scope = applicationScope, 119 started = SharingStarted.WhileSubscribed(), 120 initialValue = null, 121 ) 122 123 /** 124 * Whether user input is ongoing for the current transition. For example, if the user is swiping 125 * their finger to transition between scenes, this value will be true while their finger is on 126 * the screen, then false for the rest of the transition. 127 */ 128 @OptIn(ExperimentalCoroutinesApi::class) 129 val isTransitionUserInputOngoing: StateFlow<Boolean> = 130 transitionState 131 .flatMapLatest { 132 when (it) { 133 is ObservableTransitionState.Transition -> it.isUserInputOngoing 134 is ObservableTransitionState.Idle -> flowOf(false) 135 } 136 } 137 .stateIn( 138 scope = applicationScope, 139 started = SharingStarted.WhileSubscribed(), 140 initialValue = false 141 ) 142 143 /** Whether the scene container is visible. */ 144 val isVisible: StateFlow<Boolean> = 145 combine( 146 repository.isVisible, 147 repository.isRemoteUserInteractionOngoing, 148 ) { isVisible, isRemoteUserInteractionOngoing -> 149 isVisibleInternal( 150 raw = isVisible, 151 isRemoteUserInteractionOngoing = isRemoteUserInteractionOngoing, 152 ) 153 } 154 .stateIn( 155 scope = applicationScope, 156 started = SharingStarted.WhileSubscribed(), 157 initialValue = isVisibleInternal() 158 ) 159 160 /** 161 * The amount of transition into or out of the given [scene]. 162 * 163 * The value will be `0` if not in this scene or `1` when fully in the given scene. 164 */ 165 fun transitionProgress(scene: SceneKey): Flow<Float> { 166 return transitionState.flatMapLatest { transition -> 167 when (transition) { 168 is ObservableTransitionState.Idle -> { 169 flowOf(if (transition.currentScene == scene) 1f else 0f) 170 } 171 is ObservableTransitionState.Transition -> { 172 when { 173 transition.toScene == scene -> transition.progress 174 transition.fromScene == scene -> transition.progress.map { 1f - it } 175 else -> flowOf(0f) 176 } 177 } 178 } 179 } 180 } 181 182 /** 183 * Returns the keys of all scenes in the container. 184 * 185 * The scenes will be sorted in z-order such that the last one is the one that should be 186 * rendered on top of all previous ones. 187 */ 188 fun allSceneKeys(): List<SceneKey> { 189 return repository.allSceneKeys() 190 } 191 192 fun registerSceneStateProcessor(processor: OnSceneAboutToChangeListener) { 193 onSceneAboutToChangeListener.add(processor) 194 } 195 196 /** 197 * Requests a scene change to the given scene. 198 * 199 * The change is animated. Therefore, it will be some time before the UI will switch to the 200 * desired scene. Once enough of the transition has occurred, the [currentScene] will become 201 * [toScene] (unless the transition is canceled by user action or another call to this method). 202 */ 203 @JvmOverloads 204 fun changeScene( 205 toScene: SceneKey, 206 loggingReason: String, 207 transitionKey: TransitionKey? = null, 208 sceneState: Any? = null, 209 ) { 210 val currentSceneKey = currentScene.value 211 val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene 212 if ( 213 !validateSceneChange( 214 from = currentSceneKey, 215 to = resolvedScene, 216 loggingReason = loggingReason, 217 ) 218 ) { 219 return 220 } 221 222 logger.logSceneChangeRequested( 223 from = currentSceneKey, 224 to = resolvedScene, 225 reason = loggingReason, 226 isInstant = false, 227 ) 228 229 onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(resolvedScene, sceneState) } 230 repository.changeScene(resolvedScene, transitionKey) 231 } 232 233 /** 234 * Requests a scene change to the given scene. 235 * 236 * The change is instantaneous and not animated; it will be observable in the next frame and 237 * there will be no transition animation. 238 */ 239 fun snapToScene( 240 toScene: SceneKey, 241 loggingReason: String, 242 ) { 243 val currentSceneKey = currentScene.value 244 val resolvedScene = 245 sceneFamilyResolvers.get()[toScene]?.let { familyResolver -> 246 if (familyResolver.includesScene(currentSceneKey)) { 247 return 248 } else { 249 familyResolver.resolvedScene.value 250 } 251 } ?: toScene 252 if ( 253 !validateSceneChange( 254 from = currentSceneKey, 255 to = resolvedScene, 256 loggingReason = loggingReason, 257 ) 258 ) { 259 return 260 } 261 262 logger.logSceneChangeRequested( 263 from = currentSceneKey, 264 to = resolvedScene, 265 reason = loggingReason, 266 isInstant = true, 267 ) 268 269 repository.snapToScene(resolvedScene) 270 } 271 272 /** 273 * Sets the visibility of the container. 274 * 275 * Please do not call this from outside of the scene framework. If you are trying to force the 276 * visibility to visible or invisible, prefer making changes to the existing caller of this 277 * method or to upstream state used to calculate [isVisible]; for an example of the latter, 278 * please see [onRemoteUserInteractionStarted] and [onUserInteractionFinished]. 279 */ 280 fun setVisible(isVisible: Boolean, loggingReason: String) { 281 val wasVisible = repository.isVisible.value 282 if (wasVisible == isVisible) { 283 return 284 } 285 286 logger.logVisibilityChange( 287 from = wasVisible, 288 to = isVisible, 289 reason = loggingReason, 290 ) 291 return repository.setVisible(isVisible) 292 } 293 294 /** 295 * Notifies that a remote user interaction has begun. 296 * 297 * This is a user interaction that originates outside of the UI of the scene container and 298 * possibly outside of the System UI process itself. 299 * 300 * As an example, consider the dragging that can happen in the launcher that expands the shade. 301 * This is a user interaction that begins remotely (as it starts in the launcher process) and is 302 * then rerouted by window manager to System UI. While the user interaction definitely continues 303 * within the System UI process and code, it also originates remotely. 304 */ 305 fun onRemoteUserInteractionStarted(loggingReason: String) { 306 logger.logRemoteUserInteractionStarted(loggingReason) 307 repository.isRemoteUserInteractionOngoing.value = true 308 } 309 310 /** 311 * Notifies that the current user interaction (internally or remotely started, see 312 * [onRemoteUserInteractionStarted]) has finished. 313 */ 314 fun onUserInteractionFinished() { 315 logger.logUserInteractionFinished() 316 repository.isRemoteUserInteractionOngoing.value = false 317 } 318 319 /** 320 * Binds the given flow so the system remembers it. 321 * 322 * Note that you must call is with `null` when the UI is done or risk a memory leak. 323 */ 324 fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) { 325 repository.setTransitionState(transitionState) 326 } 327 328 /** 329 * Returns the [concrete scene][Scenes] for [sceneKey] if it is a [scene family][SceneFamilies], 330 * otherwise returns a singleton [Flow] containing [sceneKey]. 331 */ 332 fun resolveSceneFamily(sceneKey: SceneKey): Flow<SceneKey> = flow { 333 emitAll(resolveSceneFamilyOrNull(sceneKey) ?: flowOf(sceneKey)) 334 } 335 336 /** 337 * Returns the [concrete scene][Scenes] for [sceneKey] if it is a [scene family][SceneFamilies], 338 * otherwise returns `null`. 339 */ 340 fun resolveSceneFamilyOrNull(sceneKey: SceneKey): StateFlow<SceneKey>? = 341 sceneFamilyResolvers.get()[sceneKey]?.resolvedScene 342 343 private fun isVisibleInternal( 344 raw: Boolean = repository.isVisible.value, 345 isRemoteUserInteractionOngoing: Boolean = repository.isRemoteUserInteractionOngoing.value, 346 ): Boolean { 347 return raw || isRemoteUserInteractionOngoing 348 } 349 350 /** 351 * Validates that the given scene change is allowed. 352 * 353 * Will throw a runtime exception for illegal states (for example, attempting to change to a 354 * scene that's not part of the current scene framework configuration). 355 * 356 * @param from The current scene being transitioned away from 357 * @param to The desired destination scene to transition to 358 * @param loggingReason The reason why the transition is requested, for logging purposes 359 * @return `true` if the scene change is valid; `false` if it shouldn't happen 360 */ 361 private fun validateSceneChange( 362 from: SceneKey, 363 to: SceneKey, 364 loggingReason: String, 365 ): Boolean { 366 if (!repository.allSceneKeys().contains(to)) { 367 return false 368 } 369 370 val inMidTransitionFromGone = 371 (transitionState.value as? ObservableTransitionState.Transition)?.fromScene == 372 Scenes.Gone 373 val isChangeAllowed = 374 to != Scenes.Gone || 375 inMidTransitionFromGone || 376 deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked 377 check(isChangeAllowed) { 378 "Cannot change to the Gone scene while the device is locked and not currently" + 379 " transitioning from Gone. Current transition state is ${transitionState.value}." + 380 " Logging reason for scene change was: $loggingReason" 381 } 382 383 return from != to 384 } 385 386 /** Returns a flow indicating if the currently visible scene can be resolved from [family]. */ 387 fun isCurrentSceneInFamily(family: SceneKey): Flow<Boolean> = 388 currentScene.map { currentScene -> isSceneInFamily(currentScene, family) } 389 390 /** Returns `true` if [scene] can be resolved from [family]. */ 391 fun isSceneInFamily(scene: SceneKey, family: SceneKey): Boolean = 392 sceneFamilyResolvers.get()[family]?.includesScene(scene) == true 393 } 394