1 /*
2  * 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.systemui.dagger.SysUISingleton
20 import com.android.systemui.dagger.qualifiers.Application
21 import com.android.systemui.scene.data.repository.SceneContainerRepository
22 import com.android.systemui.scene.shared.logger.SceneLogger
23 import com.android.systemui.scene.shared.model.ObservableTransitionState
24 import com.android.systemui.scene.shared.model.SceneKey
25 import com.android.systemui.scene.shared.model.SceneModel
26 import javax.inject.Inject
27 import kotlinx.coroutines.CoroutineScope
28 import kotlinx.coroutines.flow.Flow
29 import kotlinx.coroutines.flow.SharingStarted
30 import kotlinx.coroutines.flow.StateFlow
31 import kotlinx.coroutines.flow.map
32 import kotlinx.coroutines.flow.stateIn
33 
34 /**
35  * Generic business logic and app state accessors for the scene framework.
36  *
37  * Note that this class should not depend on state or logic of other modules or features. Instead,
38  * other feature modules should depend on and call into this class when their parts of the
39  * application state change.
40  */
41 @SysUISingleton
42 class SceneInteractor
43 @Inject
44 constructor(
45     @Application applicationScope: CoroutineScope,
46     private val repository: SceneContainerRepository,
47     private val logger: SceneLogger,
48 ) {
49 
50     /**
51      * The currently *desired* scene.
52      *
53      * **Important:** this value will _commonly be different_ from what is being rendered in the UI,
54      * by design.
55      *
56      * There are two intended sources for this value:
57      * 1. Programmatic requests to transition to another scene (calls to [changeScene]).
58      * 2. Reports from the UI about completing a transition to another scene (calls to
59      *    [onSceneChanged]).
60      *
61      * Both the sources above cause the value of this flow to change; however, they cause mismatches
62      * in different ways.
63      *
64      * **Updates from programmatic transitions**
65      *
66      * When an external bit of code asks the framework to switch to another scene, the value here
67      * will update immediately. Downstream, the UI will detect this change and initiate the
68      * transition animation. As the transition animation progresses, a threshold will be reached, at
69      * which point the UI and the state here will match each other.
70      *
71      * **Updates from the UI**
72      *
73      * When the user interacts with the UI, the UI runs a transition animation that tracks the user
74      * pointer (for example, the user's finger). During this time, the state value here and what the
75      * UI shows will likely not match. Once/if a threshold is met, the UI reports it and commits the
76      * change, making the value here match the UI again.
77      */
78     val desiredScene: StateFlow<SceneModel> = repository.desiredScene
79 
80     /**
81      * The current state of the transition.
82      *
83      * Consumers should use this state to know:
84      * 1. Whether there is an ongoing transition or if the system is at rest.
85      * 2. When transitioning, which scenes are being transitioned between.
86      * 3. When transitioning, what the progress of the transition is.
87      */
88     val transitionState: StateFlow<ObservableTransitionState> = repository.transitionState
89 
90     /**
91      * The key of the scene that the UI is currently transitioning to or `null` if there is no
92      * active transition at the moment.
93      *
94      * This is a convenience wrapper around [transitionState], meant for flow-challenged consumers
95      * like Java code.
96      */
97     val transitioningTo: StateFlow<SceneKey?> =
98         transitionState
99             .map { state -> (state as? ObservableTransitionState.Transition)?.toScene }
100             .stateIn(
101                 scope = applicationScope,
102                 started = SharingStarted.WhileSubscribed(),
103                 initialValue = null,
104             )
105 
106     /** Whether the scene container is visible. */
107     val isVisible: StateFlow<Boolean> = repository.isVisible
108 
109     /**
110      * Returns the keys of all scenes in the container.
111      *
112      * The scenes will be sorted in z-order such that the last one is the one that should be
113      * rendered on top of all previous ones.
114      */
115     fun allSceneKeys(): List<SceneKey> {
116         return repository.allSceneKeys()
117     }
118 
119     /**
120      * Requests a scene change to the given scene.
121      *
122      * The change is animated. Therefore, while the value in [desiredScene] will update immediately,
123      * it will be some time before the UI will switch to the desired scene. The scene change
124      * requested is remembered here but served by the UI layer, which will start a transition
125      * animation. Once enough of the transition has occurred, the system will come into agreement
126      * between the [desiredScene] and the UI.
127      */
128     fun changeScene(scene: SceneModel, loggingReason: String) {
129         updateDesiredScene(scene, loggingReason, logger::logSceneChangeRequested)
130     }
131 
132     /** Sets the visibility of the container. */
133     fun setVisible(isVisible: Boolean, loggingReason: String) {
134         val wasVisible = repository.isVisible.value
135         if (wasVisible == isVisible) {
136             return
137         }
138 
139         logger.logVisibilityChange(
140             from = wasVisible,
141             to = isVisible,
142             reason = loggingReason,
143         )
144         return repository.setVisible(isVisible)
145     }
146 
147     /**
148      * Binds the given flow so the system remembers it.
149      *
150      * Note that you must call is with `null` when the UI is done or risk a memory leak.
151      */
152     fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
153         repository.setTransitionState(transitionState)
154     }
155 
156     /**
157      * Notifies that the UI has transitioned sufficiently to the given scene.
158      *
159      * *Not intended for external use!*
160      *
161      * Once a transition between one scene and another passes a threshold, the UI invokes this
162      * method to report it, updating the value in [desiredScene] to match what the UI shows.
163      */
164     internal fun onSceneChanged(scene: SceneModel, loggingReason: String) {
165         updateDesiredScene(scene, loggingReason, logger::logSceneChangeCommitted)
166     }
167 
168     private fun updateDesiredScene(
169         scene: SceneModel,
170         loggingReason: String,
171         log: (from: SceneKey, to: SceneKey, loggingReason: String) -> Unit,
172     ) {
173         val currentSceneKey = desiredScene.value.key
174         if (currentSceneKey == scene.key) {
175             return
176         }
177 
178         log(
179             /* from= */ currentSceneKey,
180             /* to= */ scene.key,
181             /* loggingReason= */ loggingReason,
182         )
183         repository.setDesiredScene(scene)
184     }
185 }
186