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 android.os.Build 20 import androidx.compose.foundation.LocalOverscrollFactory 21 import androidx.compose.foundation.gestures.awaitEachGesture 22 import androidx.compose.foundation.gestures.awaitFirstDown 23 import androidx.compose.foundation.layout.Box 24 import androidx.compose.foundation.layout.fillMaxSize 25 import androidx.compose.material3.Text 26 import androidx.compose.runtime.Composable 27 import androidx.compose.runtime.DisposableEffect 28 import androidx.compose.runtime.LaunchedEffect 29 import androidx.compose.runtime.getValue 30 import androidx.compose.runtime.mutableStateMapOf 31 import androidx.compose.runtime.remember 32 import androidx.compose.runtime.rememberCoroutineScope 33 import androidx.compose.ui.Alignment 34 import androidx.compose.ui.Modifier 35 import androidx.compose.ui.graphics.Color 36 import androidx.compose.ui.input.pointer.pointerInput 37 import androidx.compose.ui.platform.LocalContext 38 import androidx.compose.ui.platform.LocalHapticFeedback 39 import androidx.compose.ui.platform.LocalView 40 import androidx.lifecycle.compose.collectAsStateWithLifecycle 41 import com.android.compose.animation.scene.ContentKey 42 import com.android.compose.animation.scene.OverlayKey 43 import com.android.compose.animation.scene.SceneKey 44 import com.android.compose.animation.scene.SceneTransitionLayout 45 import com.android.compose.animation.scene.UserAction 46 import com.android.compose.animation.scene.UserActionResult 47 import com.android.compose.animation.scene.observableTransitionState 48 import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState 49 import com.android.compose.gesture.effect.rememberOffsetOverscrollEffectFactory 50 import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn 51 import com.android.systemui.keyguard.ui.composable.modifier.burnInAware 52 import com.android.systemui.lifecycle.rememberActivated 53 import com.android.systemui.qs.ui.adapter.QSSceneAdapter 54 import com.android.systemui.qs.ui.composable.QuickSettingsTheme 55 import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon 56 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator 57 import com.android.systemui.scene.shared.model.Scenes 58 import com.android.systemui.scene.ui.view.SceneJankMonitor 59 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel 60 import com.android.systemui.shade.ui.composable.OverlayShade 61 import com.android.systemui.shade.ui.composable.isFullWidthShade 62 import javax.inject.Provider 63 64 /** 65 * Renders a container of a collection of "scenes" that the user can switch between using certain 66 * user actions (for instance, swiping up and down) or that can be switched automatically based on 67 * application business logic in response to certain events (for example, the device unlocking). 68 * 69 * It's possible for the application to host several such scene containers, the configuration system 70 * allows configuring each container with its own set of scenes. Scenes can be present in multiple 71 * containers. 72 * 73 * @param viewModel The UI state holder for this container. 74 * @param sceneByKey Mapping of [Scene] by [SceneKey], ordered by z-order such that the last scene 75 * is rendered on top of all other scenes. It's critical that this map contains exactly and only 76 * the scenes on this container. In other words: (a) there should be no scene in this map that is 77 * not in the configuration for this container and (b) all scenes in the configuration must have 78 * entries in this map. 79 * @param overlayByKey Mapping of [Overlay] by [OverlayKey], ordered by z-order such that the last 80 * overlay is rendered on top of all other overlays. It's critical that this map contains exactly 81 * and only the overlays on this container. In other words: (a) there should be no overlay in this 82 * map that is not in the configuration for this container and (b) all overlays in the 83 * configuration must have entries in this map. 84 * @param modifier A modifier. 85 */ 86 @Composable 87 fun SceneContainer( 88 viewModel: SceneContainerViewModel, 89 sceneByKey: Map<SceneKey, Scene>, 90 overlayByKey: Map<OverlayKey, Overlay>, 91 initialSceneKey: SceneKey, 92 transitionsBuilder: SceneContainerTransitionsBuilder, 93 dataSourceDelegator: SceneDataSourceDelegator, 94 qsSceneAdapter: Provider<QSSceneAdapter>, 95 sceneJankMonitorFactory: SceneJankMonitor.Factory, 96 modifier: Modifier = Modifier, 97 ) { 98 val coroutineScope = rememberCoroutineScope() 99 100 val view = LocalView.current 101 val sceneJankMonitor = 102 rememberActivated(traceName = "sceneJankMonitor") { sceneJankMonitorFactory.create() } 103 104 val hapticFeedback = LocalHapticFeedback.current 105 val shadeExpansionMotion = OverlayShade.rememberShadeExpansionMotion(isFullWidthShade()) 106 val sceneTransitions = 107 remember(hapticFeedback, shadeExpansionMotion) { 108 transitionsBuilder.build( 109 shadeExpansionMotion, 110 viewModel.hapticsViewModel.getRevealHaptics(hapticFeedback), 111 ) 112 } 113 114 val state = 115 rememberMutableSceneTransitionLayoutState( 116 initialScene = initialSceneKey, 117 canChangeScene = { toScene -> viewModel.canChangeScene(toScene) }, 118 canShowOverlay = { overlay -> viewModel.canShowOrReplaceOverlay(overlay) }, 119 canReplaceOverlay = { beingReplaced, newlyShown -> 120 viewModel.canShowOrReplaceOverlay( 121 newlyShown = newlyShown, 122 beingReplaced = beingReplaced, 123 ) 124 }, 125 transitions = sceneTransitions, 126 onTransitionStart = { transition -> 127 sceneJankMonitor.onTransitionStart( 128 view = view, 129 from = transition.fromContent, 130 to = transition.toContent, 131 cuj = transition.cuj, 132 ) 133 }, 134 onTransitionEnd = { transition -> 135 sceneJankMonitor.onTransitionEnd( 136 from = transition.fromContent, 137 to = transition.toContent, 138 cuj = transition.cuj, 139 ) 140 }, 141 ) 142 143 DisposableEffect(state) { 144 val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope) 145 dataSourceDelegator.setDelegate(dataSource) 146 onDispose { dataSourceDelegator.setDelegate(null) } 147 } 148 149 DisposableEffect(viewModel, state) { 150 viewModel.setTransitionState(state.observableTransitionState()) 151 onDispose { viewModel.setTransitionState(null) } 152 } 153 154 val actionableContentKey = 155 viewModel.getActionableContentKey(state.currentScene, state.currentOverlays, overlayByKey) 156 val userActionsByContentKey: MutableMap<ContentKey, Map<UserAction, UserActionResult>> = 157 remember { 158 mutableStateMapOf() 159 } 160 LaunchedEffect(actionableContentKey) { 161 try { 162 val actionableContent: ActionableContent = 163 checkNotNull( 164 overlayByKey[actionableContentKey] ?: sceneByKey[actionableContentKey] 165 ) { 166 "invalid ContentKey: $actionableContentKey" 167 } 168 viewModel.filteredUserActions(actionableContent.userActions).collect { userActions -> 169 userActionsByContentKey[actionableContentKey] = 170 viewModel.resolveSceneFamilies(userActions) 171 } 172 } finally { 173 userActionsByContentKey[actionableContentKey] = emptyMap() 174 } 175 } 176 177 // Overlays use the offset overscroll effect when shown on large screens, otherwise they 178 // stretch. All scenes use the OffsetOverscrollEffect. 179 val offsetOverscrollEffectFactory = rememberOffsetOverscrollEffectFactory() 180 val stretchOverscrollEffectFactory = checkNotNull(LocalOverscrollFactory.current) 181 val overlayEffectFactory = 182 if (isFullWidthShade()) stretchOverscrollEffectFactory else offsetOverscrollEffectFactory 183 184 // Inflate qsView here so that shade has the correct qqs height in the first measure pass after 185 // rebooting. 186 if ( 187 viewModel.allContentKeys.contains(Scenes.QuickSettings) || 188 viewModel.allContentKeys.contains(Scenes.Shade) 189 ) { 190 val qsAdapter = qsSceneAdapter.get() 191 QuickSettingsTheme { 192 val context = LocalContext.current 193 val qsView by qsAdapter.qsView.collectAsStateWithLifecycle() 194 LaunchedEffect(context) { 195 if (qsView == null) { 196 qsAdapter.inflate(context) 197 } 198 } 199 } 200 } 201 202 Box( 203 modifier = 204 Modifier.fillMaxSize().pointerInput(Unit) { 205 awaitEachGesture { 206 awaitFirstDown(false) 207 viewModel.onSceneContainerUserInputStarted() 208 } 209 } 210 ) { 211 SceneRevealScrim( 212 viewModel = viewModel.lightRevealScrim, 213 wallpaperViewModel = viewModel.wallpaperViewModel, 214 modifier = Modifier.fillMaxSize(), 215 ) 216 217 SceneTransitionLayout( 218 state = state, 219 modifier = modifier.fillMaxSize(), 220 swipeSourceDetector = viewModel.swipeSourceDetector, 221 ) { 222 sceneByKey.forEach { (sceneKey, scene) -> 223 scene( 224 key = sceneKey, 225 userActions = userActionsByContentKey.getOrDefault(sceneKey, emptyMap()), 226 effectFactory = offsetOverscrollEffectFactory, 227 ) { 228 // Activate the scene. 229 LaunchedEffect(scene) { scene.activate() } 230 231 // Render the scene. 232 with(scene) { 233 this@scene.Content( 234 modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize() 235 ) 236 } 237 } 238 } 239 overlayByKey.forEach { (overlayKey, overlay) -> 240 overlay( 241 key = overlayKey, 242 userActions = userActionsByContentKey.getOrDefault(overlayKey, emptyMap()), 243 effectFactory = overlayEffectFactory, 244 ) { 245 // Activate the overlay. 246 LaunchedEffect(overlay) { overlay.activate() } 247 248 // Render the overlay. 249 with(overlay) { this@overlay.Content(Modifier) } 250 } 251 } 252 } 253 254 if (Build.IS_ENG) { 255 BottomRightCornerRibbon( 256 content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) }, 257 colorSaturation = { viewModel.ribbonColorSaturation }, 258 modifier = 259 Modifier.align(Alignment.BottomEnd) 260 .burnInAware( 261 viewModel = viewModel.burnIn, 262 params = rememberBurnIn(viewModel.clock).parameters, 263 ), 264 ) 265 } 266 } 267 } 268