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.ui.composable
18 
19 import androidx.compose.foundation.gestures.awaitEachGesture
20 import androidx.compose.foundation.gestures.awaitFirstDown
21 import androidx.compose.foundation.layout.Box
22 import androidx.compose.foundation.layout.fillMaxSize
23 import androidx.compose.material3.Text
24 import androidx.compose.runtime.Composable
25 import androidx.compose.runtime.DisposableEffect
26 import androidx.compose.runtime.LaunchedEffect
27 import androidx.compose.runtime.getValue
28 import androidx.compose.runtime.mutableStateMapOf
29 import androidx.compose.runtime.remember
30 import androidx.compose.runtime.rememberCoroutineScope
31 import androidx.compose.ui.Alignment
32 import androidx.compose.ui.Modifier
33 import androidx.compose.ui.graphics.Color
34 import androidx.compose.ui.input.pointer.pointerInput
35 import androidx.compose.ui.platform.LocalContext
36 import androidx.lifecycle.compose.collectAsStateWithLifecycle
37 import com.android.compose.animation.scene.ContentKey
38 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
39 import com.android.compose.animation.scene.OverlayKey
40 import com.android.compose.animation.scene.SceneKey
41 import com.android.compose.animation.scene.SceneTransitionLayout
42 import com.android.compose.animation.scene.SceneTransitions
43 import com.android.compose.animation.scene.UserAction
44 import com.android.compose.animation.scene.UserActionResult
45 import com.android.compose.animation.scene.observableTransitionState
46 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
47 import com.android.systemui.qs.ui.composable.QuickSettingsTheme
48 import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon
49 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
50 import com.android.systemui.scene.shared.model.Scenes
51 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
52 import javax.inject.Provider
53 
54 /**
55  * Renders a container of a collection of "scenes" that the user can switch between using certain
56  * user actions (for instance, swiping up and down) or that can be switched automatically based on
57  * application business logic in response to certain events (for example, the device unlocking).
58  *
59  * It's possible for the application to host several such scene containers, the configuration system
60  * allows configuring each container with its own set of scenes. Scenes can be present in multiple
61  * containers.
62  *
63  * @param viewModel The UI state holder for this container.
64  * @param sceneByKey Mapping of [Scene] by [SceneKey], ordered by z-order such that the last scene
65  *   is rendered on top of all other scenes. It's critical that this map contains exactly and only
66  *   the scenes on this container. In other words: (a) there should be no scene in this map that is
67  *   not in the configuration for this container and (b) all scenes in the configuration must have
68  *   entries in this map.
69  * @param overlayByKey Mapping of [Overlay] by [OverlayKey], ordered by z-order such that the last
70  *   overlay is rendered on top of all other overlays. It's critical that this map contains exactly
71  *   and only the overlays on this container. In other words: (a) there should be no overlay in this
72  *   map that is not in the configuration for this container and (b) all overlays in the
73  *   configuration must have entries in this map.
74  * @param modifier A modifier.
75  */
76 @Composable
77 fun SceneContainer(
78     viewModel: SceneContainerViewModel,
79     sceneByKey: Map<SceneKey, Scene>,
80     overlayByKey: Map<OverlayKey, Overlay>,
81     initialSceneKey: SceneKey,
82     sceneTransitions: SceneTransitions,
83     dataSourceDelegator: SceneDataSourceDelegator,
84     qsSceneAdapter: Provider<QSSceneAdapter>,
85     modifier: Modifier = Modifier,
86 ) {
87     val coroutineScope = rememberCoroutineScope()
88     val state: MutableSceneTransitionLayoutState = remember {
89         MutableSceneTransitionLayoutState(
90             initialScene = initialSceneKey,
91             canChangeScene = { toScene -> viewModel.canChangeScene(toScene) },
92             transitions = sceneTransitions,
93         )
94     }
95 
96     DisposableEffect(state) {
97         val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope)
98         dataSourceDelegator.setDelegate(dataSource)
99         onDispose { dataSourceDelegator.setDelegate(null) }
100     }
101 
102     DisposableEffect(viewModel, state) {
103         viewModel.setTransitionState(state.observableTransitionState())
104         onDispose { viewModel.setTransitionState(null) }
105     }
106 
107     val actionableContentKey =
108         viewModel.getActionableContentKey(state.currentScene, state.currentOverlays, overlayByKey)
109     val userActionsByContentKey: MutableMap<ContentKey, Map<UserAction, UserActionResult>> =
110         remember {
111             mutableStateMapOf()
112         }
113     LaunchedEffect(actionableContentKey) {
114         try {
115             val actionableContent: ActionableContent =
116                 checkNotNull(
117                     overlayByKey[actionableContentKey] ?: sceneByKey[actionableContentKey]
118                 ) {
119                     "invalid ContentKey: $actionableContentKey"
120                 }
121             viewModel.filteredUserActions(actionableContent.userActions).collect { userActions ->
122                 userActionsByContentKey[actionableContentKey] =
123                     viewModel.resolveSceneFamilies(userActions)
124             }
125         } finally {
126             userActionsByContentKey[actionableContentKey] = emptyMap()
127         }
128     }
129 
130     // Inflate qsView here so that shade has the correct qqs height in the first measure pass after
131     // rebooting
132     if (
133         viewModel.allContentKeys.contains(Scenes.QuickSettings) ||
134             viewModel.allContentKeys.contains(Scenes.Shade)
135     ) {
136         val qsAdapter = qsSceneAdapter.get()
137         QuickSettingsTheme {
138             val context = LocalContext.current
139             val qsView by qsAdapter.qsView.collectAsStateWithLifecycle()
140             LaunchedEffect(context) {
141                 if (qsView == null) {
142                     qsAdapter.inflate(context)
143                 }
144             }
145         }
146     }
147 
148     Box(
149         modifier =
150             Modifier.fillMaxSize().pointerInput(Unit) {
151                 awaitEachGesture {
152                     awaitFirstDown(false)
153                     viewModel.onSceneContainerUserInputStarted()
154                 }
155             }
156     ) {
157         SceneTransitionLayout(
158             state = state,
159             modifier = modifier.fillMaxSize(),
160             swipeSourceDetector = viewModel.edgeDetector,
161         ) {
162             sceneByKey.forEach { (sceneKey, scene) ->
163                 scene(
164                     key = sceneKey,
165                     userActions = userActionsByContentKey.getOrDefault(sceneKey, emptyMap()),
166                 ) {
167                     // Activate the scene.
168                     LaunchedEffect(scene) { scene.activate() }
169 
170                     // Render the scene.
171                     with(scene) {
172                         this@scene.Content(
173                             modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize()
174                         )
175                     }
176                 }
177             }
178             overlayByKey.forEach { (overlayKey, overlay) ->
179                 overlay(
180                     key = overlayKey,
181                     userActions = userActionsByContentKey.getOrDefault(overlayKey, emptyMap()),
182                 ) {
183                     // Activate the overlay.
184                     LaunchedEffect(overlay) { overlay.activate() }
185 
186                     // Render the overlay.
187                     with(overlay) { this@overlay.Content(Modifier) }
188                 }
189             }
190         }
191 
192         BottomRightCornerRibbon(
193             content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) },
194             modifier = Modifier.align(Alignment.BottomEnd),
195         )
196     }
197 }
198