• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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