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