1 /*
<lambda>null2 * Copyright (C) 2024 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.qs.ui.composable
18
19 import androidx.compose.animation.AnimatedContent
20 import androidx.compose.animation.core.animateFloatAsState
21 import androidx.compose.animation.core.tween
22 import androidx.compose.animation.fadeIn
23 import androidx.compose.animation.fadeOut
24 import androidx.compose.animation.togetherWith
25 import androidx.compose.foundation.layout.Arrangement
26 import androidx.compose.foundation.layout.Box
27 import androidx.compose.foundation.layout.Column
28 import androidx.compose.foundation.layout.fillMaxWidth
29 import androidx.compose.foundation.layout.padding
30 import androidx.compose.foundation.layout.requiredHeight
31 import androidx.compose.foundation.rememberScrollState
32 import androidx.compose.foundation.systemGestureExclusion
33 import androidx.compose.foundation.verticalScroll
34 import androidx.compose.runtime.Composable
35 import androidx.compose.runtime.DisposableEffect
36 import androidx.compose.runtime.getValue
37 import androidx.compose.ui.Alignment
38 import androidx.compose.ui.Modifier
39 import androidx.compose.ui.geometry.Offset
40 import androidx.compose.ui.geometry.Rect
41 import androidx.compose.ui.geometry.Size
42 import androidx.compose.ui.graphics.Color
43 import androidx.compose.ui.graphics.graphicsLayer
44 import androidx.compose.ui.layout.boundsInWindow
45 import androidx.compose.ui.layout.onPlaced
46 import androidx.compose.ui.platform.LocalDensity
47 import androidx.compose.ui.unit.dp
48 import androidx.lifecycle.compose.collectAsStateWithLifecycle
49 import com.android.compose.animation.scene.ContentScope
50 import com.android.compose.animation.scene.ElementKey
51 import com.android.compose.animation.scene.UserAction
52 import com.android.compose.animation.scene.UserActionResult
53 import com.android.compose.animation.scene.content.state.TransitionState
54 import com.android.compose.modifiers.thenIf
55 import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
56 import com.android.systemui.brightness.ui.compose.ContainerColors
57 import com.android.systemui.compose.modifiers.sysuiResTag
58 import com.android.systemui.dagger.SysUISingleton
59 import com.android.systemui.lifecycle.rememberViewModel
60 import com.android.systemui.media.controls.ui.composable.MediaCarousel
61 import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.COLLAPSED
62 import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace
63 import com.android.systemui.qs.composefragment.ui.GridAnchor
64 import com.android.systemui.qs.flags.QsDetailedView
65 import com.android.systemui.qs.panels.ui.compose.EditMode
66 import com.android.systemui.qs.panels.ui.compose.TileDetails
67 import com.android.systemui.qs.panels.ui.compose.TileGrid
68 import com.android.systemui.qs.panels.ui.compose.toolbar.Toolbar
69 import com.android.systemui.qs.ui.composable.QuickSettingsShade.systemGestureExclusionInShade
70 import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
71 import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeOverlayActionsViewModel
72 import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeOverlayContentViewModel
73 import com.android.systemui.scene.shared.model.Overlays
74 import com.android.systemui.scene.ui.composable.Overlay
75 import com.android.systemui.shade.ui.composable.OverlayShade
76 import com.android.systemui.shade.ui.composable.OverlayShadeHeader
77 import com.android.systemui.shade.ui.composable.QuickSettingsOverlayHeader
78 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
79 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
80 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
81 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
82 import dagger.Lazy
83 import javax.inject.Inject
84 import kotlinx.coroutines.flow.Flow
85
86 @SysUISingleton
87 class QuickSettingsShadeOverlay
88 @Inject
89 constructor(
90 private val actionsViewModelFactory: QuickSettingsShadeOverlayActionsViewModel.Factory,
91 private val contentViewModelFactory: QuickSettingsShadeOverlayContentViewModel.Factory,
92 private val quickSettingsContainerViewModelFactory: QuickSettingsContainerViewModel.Factory,
93 private val notificationStackScrollView: Lazy<NotificationScrollView>,
94 private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
95 ) : Overlay {
96
97 override val key = Overlays.QuickSettingsShade
98
99 private val actionsViewModel: QuickSettingsShadeOverlayActionsViewModel by lazy {
100 actionsViewModelFactory.create()
101 }
102
103 override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions
104
105 override suspend fun activate(): Nothing {
106 actionsViewModel.activate()
107 }
108
109 @Composable
110 override fun ContentScope.Content(modifier: Modifier) {
111 val contentViewModel =
112 rememberViewModel("QuickSettingsShadeOverlayContent") {
113 contentViewModelFactory.create()
114 }
115 val quickSettingsContainerViewModel =
116 rememberViewModel("QuickSettingsShadeOverlayContainer") {
117 quickSettingsContainerViewModelFactory.create(
118 supportsBrightnessMirroring = true,
119 expansion = COLLAPSED,
120 )
121 }
122 val hunPlaceholderViewModel =
123 rememberViewModel("QuickSettingsShadeOverlayPlaceholder") {
124 notificationsPlaceholderViewModelFactory.create()
125 }
126
127 val panelCornerRadius =
128 with(LocalDensity.current) { OverlayShade.Dimensions.PanelCornerRadius.toPx().toInt() }
129 val showBrightnessMirror =
130 quickSettingsContainerViewModel.brightnessSliderViewModel.showMirror
131 val contentAlphaFromBrightnessMirror by
132 animateFloatAsState(if (showBrightnessMirror) 0f else 1f)
133
134 // Set the bounds to null when the QuickSettings overlay disappears.
135 DisposableEffect(Unit) { onDispose { contentViewModel.onPanelShapeChanged(null) } }
136
137 Box(modifier = modifier.graphicsLayer { alpha = contentAlphaFromBrightnessMirror }) {
138 OverlayShade(
139 panelElement = QuickSettingsShade.Elements.Panel,
140 alignmentOnWideScreens = Alignment.TopEnd,
141 onScrimClicked = contentViewModel::onScrimClicked,
142 header = {
143 OverlayShadeHeader(
144 viewModel = quickSettingsContainerViewModel.shadeHeaderViewModel,
145 modifier = Modifier.element(QuickSettingsShade.Elements.StatusBar),
146 )
147 },
148 ) {
149 QuickSettingsContainer(
150 viewModel = quickSettingsContainerViewModel,
151 modifier =
152 Modifier.onPlaced { coordinates ->
153 val shape =
154 ShadeScrimShape(
155 bounds = ShadeScrimBounds(coordinates.boundsInWindow()),
156 topRadius = 0,
157 bottomRadius = panelCornerRadius,
158 )
159 contentViewModel.onPanelShapeChanged(shape)
160 },
161 )
162 }
163 SnoozeableHeadsUpNotificationSpace(
164 stackScrollView = notificationStackScrollView.get(),
165 viewModel = hunPlaceholderViewModel,
166 )
167 }
168 }
169 }
170
171 /** The possible states of the `ShadeBody`. */
172 private sealed interface ShadeBodyState {
173 data object Editing : ShadeBodyState
174
175 data object TileDetails : ShadeBodyState
176
177 data object Default : ShadeBodyState
178 }
179
180 @Composable
ContentScopenull181 fun ContentScope.QuickSettingsContainer(
182 viewModel: QuickSettingsContainerViewModel,
183 modifier: Modifier = Modifier,
184 ) {
185 val isEditing by viewModel.editModeViewModel.isEditing.collectAsStateWithLifecycle()
186 val tileDetails =
187 if (QsDetailedView.isEnabled) viewModel.detailsViewModel.activeTileDetails else null
188
189 AnimatedContent(
190 targetState =
191 when {
192 isEditing -> ShadeBodyState.Editing
193 tileDetails != null -> ShadeBodyState.TileDetails
194 else -> ShadeBodyState.Default
195 },
196 transitionSpec = { fadeIn(tween(500)) togetherWith fadeOut(tween(500)) },
197 ) { state ->
198 when (state) {
199 ShadeBodyState.Editing -> {
200 EditMode(
201 viewModel = viewModel.editModeViewModel,
202 modifier =
203 modifier.fillMaxWidth().padding(QuickSettingsShade.Dimensions.Padding),
204 )
205 }
206
207 ShadeBodyState.TileDetails -> {
208 TileDetails(modifier = modifier, viewModel.detailsViewModel)
209 }
210
211 ShadeBodyState.Default -> {
212 QuickSettingsLayout(
213 viewModel = viewModel,
214 modifier = modifier.sysuiResTag("quick_settings_panel"),
215 )
216 }
217 }
218 }
219 }
220
221 /** Column containing Brightness and QS tiles. */
222 @Composable
ContentScopenull223 fun ContentScope.QuickSettingsLayout(
224 viewModel: QuickSettingsContainerViewModel,
225 modifier: Modifier = Modifier,
226 ) {
227 Column(
228 verticalArrangement = Arrangement.spacedBy(QuickSettingsShade.Dimensions.Padding),
229 horizontalAlignment = Alignment.CenterHorizontally,
230 modifier =
231 modifier.padding(
232 start = QuickSettingsShade.Dimensions.Padding,
233 end = QuickSettingsShade.Dimensions.Padding,
234 bottom = QuickSettingsShade.Dimensions.Padding,
235 ),
236 ) {
237 if (viewModel.showHeader) {
238 QuickSettingsOverlayHeader(
239 viewModel = viewModel.shadeHeaderViewModel,
240 modifier =
241 Modifier.element(QuickSettingsShade.Elements.Header)
242 .padding(top = QuickSettingsShade.Dimensions.Padding),
243 )
244 }
245 Toolbar(
246 modifier =
247 Modifier.fillMaxWidth().requiredHeight(QuickSettingsShade.Dimensions.ToolbarHeight),
248 viewModel = viewModel.toolbarViewModel,
249 )
250 Column(
251 verticalArrangement = Arrangement.spacedBy(QuickSettingsShade.Dimensions.Padding),
252 modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
253 ) {
254 MediaCarousel(
255 isVisible = viewModel.showMedia,
256 mediaHost = viewModel.mediaHost,
257 carouselController = viewModel.mediaCarouselController,
258 usingCollapsedLandscapeMedia = true,
259 modifier = Modifier.padding(horizontal = QuickSettingsShade.Dimensions.Padding),
260 )
261
262 Box(
263 Modifier.systemGestureExclusionInShade(
264 enabled = { layoutState.transitionState is TransitionState.Idle }
265 )
266 ) {
267 BrightnessSliderContainer(
268 viewModel = viewModel.brightnessSliderViewModel,
269 containerColors =
270 ContainerColors(
271 idleColor = Color.Transparent,
272 mirrorColor = OverlayShade.Colors.PanelBackground,
273 ),
274 modifier = Modifier.fillMaxWidth(),
275 )
276 }
277
278 Box {
279 GridAnchor()
280 TileGrid(
281 viewModel = viewModel.tileGridViewModel,
282 modifier = Modifier.fillMaxWidth(),
283 )
284 }
285 }
286 }
287 }
288
289 object QuickSettingsShade {
290 object Elements {
291 val StatusBar = ElementKey("QuickSettingsShadeOverlayStatusBar")
292 val Panel = ElementKey("QuickSettingsShadeOverlayPanel")
293 val Header = ElementKey("QuickSettingsShadeOverlayHeader")
294 }
295
296 object Dimensions {
297 val Padding = 16.dp
298 val ToolbarHeight = 48.dp
299 }
300
301 /**
302 * Applies system gesture exclusion to a component adding [Dimensions.Padding] to left and
303 * right.
304 */
305 @Composable
systemGestureExclusionInShadenull306 fun Modifier.systemGestureExclusionInShade(enabled: () -> Boolean): Modifier {
307 val density = LocalDensity.current
308 return thenIf(enabled()) {
309 Modifier.systemGestureExclusion { layoutCoordinates ->
310 val sidePadding = with(density) { Dimensions.Padding.toPx() }
311 Rect(
312 offset = Offset(x = -sidePadding, y = 0f),
313 size =
314 Size(
315 width = layoutCoordinates.size.width.toFloat() + 2 * sidePadding,
316 height = layoutCoordinates.size.height.toFloat(),
317 ),
318 )
319 }
320 }
321 }
322 }
323