1 /* <lambda>null2 * Copyright (C) 2024 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.communal.domain.interactor 18 19 import android.content.res.Configuration 20 import com.android.app.tracing.coroutines.launchTraced as launch 21 import com.android.compose.animation.scene.ObservableTransitionState 22 import com.android.compose.animation.scene.SceneKey 23 import com.android.compose.animation.scene.TransitionKey 24 import com.android.systemui.communal.data.repository.CommunalSceneRepository 25 import com.android.systemui.communal.domain.model.CommunalTransitionProgressModel 26 import com.android.systemui.communal.shared.log.CommunalSceneLogger 27 import com.android.systemui.communal.shared.model.CommunalScenes 28 import com.android.systemui.communal.shared.model.CommunalScenes.toSceneContainerSceneKey 29 import com.android.systemui.communal.shared.model.EditModeState 30 import com.android.systemui.dagger.SysUISingleton 31 import com.android.systemui.dagger.qualifiers.Application 32 import com.android.systemui.dagger.qualifiers.Main 33 import com.android.systemui.keyguard.shared.model.KeyguardState 34 import com.android.systemui.scene.domain.interactor.SceneInteractor 35 import com.android.systemui.scene.shared.flag.SceneContainerFlag 36 import com.android.systemui.scene.shared.model.Scenes 37 import com.android.systemui.statusbar.policy.KeyguardStateController 38 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf 39 import com.android.systemui.util.kotlin.pairwiseBy 40 import javax.inject.Inject 41 import kotlinx.coroutines.CoroutineDispatcher 42 import kotlinx.coroutines.CoroutineScope 43 import kotlinx.coroutines.delay 44 import kotlinx.coroutines.flow.Flow 45 import kotlinx.coroutines.flow.MutableStateFlow 46 import kotlinx.coroutines.flow.SharingStarted 47 import kotlinx.coroutines.flow.StateFlow 48 import kotlinx.coroutines.flow.asStateFlow 49 import kotlinx.coroutines.flow.distinctUntilChanged 50 import kotlinx.coroutines.flow.flatMapLatest 51 import kotlinx.coroutines.flow.flowOf 52 import kotlinx.coroutines.flow.map 53 import kotlinx.coroutines.flow.onEach 54 import kotlinx.coroutines.flow.onStart 55 import kotlinx.coroutines.flow.stateIn 56 57 @SysUISingleton 58 class CommunalSceneInteractor 59 @Inject 60 constructor( 61 @Application private val applicationScope: CoroutineScope, 62 @Main private val mainImmediateDispatcher: CoroutineDispatcher, 63 private val repository: CommunalSceneRepository, 64 private val logger: CommunalSceneLogger, 65 private val sceneInteractor: SceneInteractor, 66 private val keyguardStateController: KeyguardStateController, 67 ) { 68 private val _isLaunchingWidget = MutableStateFlow(false) 69 70 /** Whether a widget launch is currently in progress. */ 71 val isLaunchingWidget: StateFlow<Boolean> = _isLaunchingWidget.asStateFlow() 72 73 fun setIsLaunchingWidget(launching: Boolean) { 74 _isLaunchingWidget.value = launching 75 } 76 77 /** 78 * Whether screen will be rotated to portrait if transitioned out of hub to keyguard screens. 79 */ 80 var willRotateToPortrait: Flow<Boolean> = 81 repository.communalContainerOrientation 82 .map { 83 it == Configuration.ORIENTATION_LANDSCAPE && 84 !keyguardStateController.isKeyguardScreenRotationAllowed() 85 } 86 .distinctUntilChanged() 87 88 /** Whether communal container is rotated to portrait. Emits an initial value of false. */ 89 val rotatedToPortrait: StateFlow<Boolean> = 90 repository.communalContainerOrientation 91 .pairwiseBy(initialValue = false) { old, new -> 92 old == Configuration.ORIENTATION_LANDSCAPE && 93 new == Configuration.ORIENTATION_PORTRAIT 94 } 95 .stateIn(applicationScope, SharingStarted.Eagerly, false) 96 97 fun setCommunalContainerOrientation(orientation: Int) { 98 repository.setCommunalContainerOrientation(orientation) 99 } 100 101 fun interface OnSceneAboutToChangeListener { 102 /** Notifies that the scene is about to change to [toScene]. */ 103 fun onSceneAboutToChange(toScene: SceneKey, keyguardState: KeyguardState?) 104 } 105 106 private val onSceneAboutToChangeListener = mutableSetOf<OnSceneAboutToChangeListener>() 107 108 /** 109 * Registers a listener which is called when the scene is about to change. 110 * 111 * This API is for legacy communal container scenes, and should not be used when 112 * [SceneContainerFlag] is enabled. 113 */ 114 fun registerSceneStateProcessor(processor: OnSceneAboutToChangeListener) { 115 SceneContainerFlag.assertInLegacyMode() 116 onSceneAboutToChangeListener.add(processor) 117 } 118 119 /** Unregisters a previously registered listener. */ 120 fun unregisterSceneStateProcessor(processor: OnSceneAboutToChangeListener) { 121 SceneContainerFlag.assertInLegacyMode() 122 onSceneAboutToChangeListener.remove(processor) 123 } 124 125 /** 126 * Asks for an asynchronous scene witch to [newScene], which will use the corresponding 127 * installed transition or the one specified by [transitionKey], if provided. 128 */ 129 fun changeScene( 130 newScene: SceneKey, 131 loggingReason: String, 132 transitionKey: TransitionKey? = null, 133 keyguardState: KeyguardState? = null, 134 ) { 135 applicationScope.launch("$TAG#changeScene", mainImmediateDispatcher) { 136 if (SceneContainerFlag.isEnabled) { 137 sceneInteractor.changeScene( 138 toScene = newScene.toSceneContainerSceneKey(), 139 loggingReason = loggingReason, 140 transitionKey = transitionKey, 141 sceneState = keyguardState, 142 ) 143 return@launch 144 } 145 146 if (currentScene.value == newScene) return@launch 147 logger.logSceneChangeRequested( 148 from = currentScene.value, 149 to = newScene, 150 reason = loggingReason, 151 isInstant = false, 152 ) 153 notifyListeners(newScene, keyguardState) 154 repository.changeScene(newScene, transitionKey) 155 } 156 } 157 158 /** Immediately snaps to the new scene. */ 159 fun snapToScene( 160 newScene: SceneKey, 161 loggingReason: String, 162 delayMillis: Long = 0, 163 keyguardState: KeyguardState? = null, 164 ) { 165 applicationScope.launch("$TAG#snapToScene") { 166 if (SceneContainerFlag.isEnabled) { 167 sceneInteractor.snapToScene( 168 toScene = newScene.toSceneContainerSceneKey(), 169 loggingReason = loggingReason, 170 ) 171 return@launch 172 } 173 174 delay(delayMillis) 175 if (currentScene.value == newScene) return@launch 176 logger.logSceneChangeRequested( 177 from = currentScene.value, 178 to = newScene, 179 reason = loggingReason, 180 isInstant = true, 181 ) 182 notifyListeners(newScene, keyguardState) 183 repository.snapToScene(newScene) 184 } 185 } 186 187 private fun notifyListeners(newScene: SceneKey, keyguardState: KeyguardState?) { 188 onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(newScene, keyguardState) } 189 } 190 191 /** 192 * Target scene as requested by the underlying [SceneTransitionLayout] or through [changeScene]. 193 */ 194 val currentScene: StateFlow<SceneKey> = 195 if (SceneContainerFlag.isEnabled) { 196 sceneInteractor.currentScene 197 } else { 198 repository.currentScene 199 .pairwiseBy(initialValue = repository.currentScene.value) { from, to -> 200 logger.logSceneChangeCommitted(from = from, to = to) 201 to 202 } 203 .stateIn( 204 scope = applicationScope, 205 started = SharingStarted.Eagerly, 206 initialValue = repository.currentScene.value, 207 ) 208 } 209 210 private val _editModeState = MutableStateFlow<EditModeState?>(null) 211 /** 212 * Current state for glanceable hub edit mode, used to chain the animations when transitioning 213 * between communal scene and the edit mode activity. 214 */ 215 val editModeState = _editModeState.asStateFlow() 216 217 fun setEditModeState(value: EditModeState?) { 218 _editModeState.value = value 219 } 220 221 /** Transition state of the hub mode. */ 222 val transitionState: StateFlow<ObservableTransitionState> = 223 if (SceneContainerFlag.isEnabled) { 224 sceneInteractor.transitionState 225 } else { 226 repository.transitionState 227 .onEach { logger.logSceneTransition(it) } 228 .stateIn( 229 scope = applicationScope, 230 started = SharingStarted.Eagerly, 231 initialValue = repository.transitionState.value, 232 ) 233 } 234 235 /** 236 * Updates the transition state of the hub [SceneTransitionLayout]. 237 * 238 * Note that you must call is with `null` when the UI is done or risk a memory leak. 239 */ 240 fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) { 241 if (SceneContainerFlag.isEnabled) { 242 sceneInteractor.setTransitionState(transitionState) 243 } else { 244 repository.setTransitionState(transitionState) 245 } 246 } 247 248 /** 249 * Returns a flow that tracks the progress of transitions to the given scene from 0-1. 250 * 251 * This API is for legacy communal container scenes, and should not be used when 252 * [SceneContainerFlag] is enabled. 253 */ 254 fun transitionProgressToScene(targetScene: SceneKey) = 255 transitionState 256 .flatMapLatest { state -> 257 when (state) { 258 is ObservableTransitionState.Idle -> 259 flowOf(CommunalTransitionProgressModel.Idle(state.currentScene)) 260 is ObservableTransitionState.Transition -> 261 if (state.toContent == targetScene) { 262 state.progress.map { 263 CommunalTransitionProgressModel.Transition( 264 // Clamp the progress values between 0 and 1 as actual progress 265 // values can be higher than 0 or lower than 1 due to a fling. 266 progress = it.coerceIn(0.0f, 1.0f) 267 ) 268 } 269 } else { 270 flowOf(CommunalTransitionProgressModel.OtherTransition) 271 } 272 } 273 } 274 .distinctUntilChanged() 275 .onStart { SceneContainerFlag.assertInLegacyMode() } 276 277 /** 278 * Flow that emits a boolean if the communal UI is fully visible and not in transition. 279 * 280 * This will not be true while transitioning to the hub and will turn false immediately when a 281 * swipe to exit the hub starts. 282 */ 283 val isIdleOnCommunal: StateFlow<Boolean> = 284 transitionState 285 .map { 286 it is ObservableTransitionState.Idle && 287 (it.currentScene == 288 if (SceneContainerFlag.isEnabled) Scenes.Communal 289 else CommunalScenes.Communal) 290 } 291 .stateIn( 292 scope = applicationScope, 293 started = SharingStarted.Eagerly, 294 initialValue = false, 295 ) 296 297 /** This flow will be true when idle on the hub and not transitioning to edit mode. */ 298 val isIdleOnCommunalNotEditMode: Flow<Boolean> = 299 allOf(isIdleOnCommunal, editModeState.map { it == null }) 300 301 /** 302 * Flow that emits a boolean if any portion of the communal UI is visible at all. 303 * 304 * This flow will be true during any transition and when idle on the communal scene. 305 */ 306 val isCommunalVisible: StateFlow<Boolean> = 307 transitionState 308 .map { 309 if (SceneContainerFlag.isEnabled) 310 it is ObservableTransitionState.Idle && it.currentScene == Scenes.Communal || 311 (it is ObservableTransitionState.Transition && 312 (it.fromContent == Scenes.Communal || it.toContent == Scenes.Communal)) 313 else 314 !(it is ObservableTransitionState.Idle && 315 it.currentScene == CommunalScenes.Blank) 316 } 317 .stateIn( 318 scope = applicationScope, 319 started = SharingStarted.WhileSubscribed(), 320 initialValue = false, 321 ) 322 323 /** Flow that emits a boolean if transitioning to or idle on communal scene. */ 324 val isTransitioningToOrIdleOnCommunal: Flow<Boolean> = 325 transitionState 326 .map { 327 (it is ObservableTransitionState.Idle && 328 it.currentScene == CommunalScenes.Communal) || 329 (it is ObservableTransitionState.Transition && 330 it.toContent == CommunalScenes.Communal) 331 } 332 .stateIn( 333 scope = applicationScope, 334 started = SharingStarted.WhileSubscribed(), 335 initialValue = false, 336 ) 337 338 private companion object { 339 const val TAG = "CommunalSceneInteractor" 340 } 341 } 342