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