• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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