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