1 /*
2  * 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 @file:OptIn(ExperimentalLayoutApi::class)
18 
19 package com.android.systemui.shade.ui.composable
20 
21 import androidx.compose.foundation.background
22 import androidx.compose.foundation.clickable
23 import androidx.compose.foundation.layout.Box
24 import androidx.compose.foundation.layout.ExperimentalLayoutApi
25 import androidx.compose.foundation.layout.PaddingValues
26 import androidx.compose.foundation.layout.Spacer
27 import androidx.compose.foundation.layout.WindowInsets
28 import androidx.compose.foundation.layout.asPaddingValues
29 import androidx.compose.foundation.layout.calculateEndPadding
30 import androidx.compose.foundation.layout.calculateStartPadding
31 import androidx.compose.foundation.layout.displayCutout
32 import androidx.compose.foundation.layout.fillMaxSize
33 import androidx.compose.foundation.layout.fillMaxWidth
34 import androidx.compose.foundation.layout.padding
35 import androidx.compose.foundation.layout.systemBarsIgnoringVisibility
36 import androidx.compose.foundation.layout.waterfall
37 import androidx.compose.foundation.layout.width
38 import androidx.compose.foundation.shape.RoundedCornerShape
39 import androidx.compose.material3.MaterialTheme
40 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
41 import androidx.compose.runtime.Composable
42 import androidx.compose.runtime.ReadOnlyComposable
43 import androidx.compose.runtime.getValue
44 import androidx.compose.ui.Alignment
45 import androidx.compose.ui.Modifier
46 import androidx.compose.ui.draw.clip
47 import androidx.compose.ui.graphics.Color
48 import androidx.compose.ui.platform.LocalLayoutDirection
49 import androidx.compose.ui.unit.dp
50 import androidx.lifecycle.compose.collectAsStateWithLifecycle
51 import com.android.compose.animation.scene.ElementKey
52 import com.android.compose.animation.scene.LowestZIndexScenePicker
53 import com.android.compose.animation.scene.SceneScope
54 import com.android.compose.windowsizeclass.LocalWindowSizeClass
55 import com.android.systemui.keyguard.ui.composable.LockscreenContent
56 import com.android.systemui.scene.shared.model.Scenes
57 import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
58 import com.android.systemui.util.kotlin.getOrNull
59 import dagger.Lazy
60 import java.util.Optional
61 
62 /** The overlay shade renders a lightweight shade UI container on top of a background scene. */
63 @Composable
SceneScopenull64 fun SceneScope.OverlayShade(
65     viewModel: OverlayShadeViewModel,
66     panelAlignment: Alignment,
67     lockscreenContent: Lazy<Optional<LockscreenContent>>,
68     modifier: Modifier = Modifier,
69     content: @Composable () -> Unit,
70 ) {
71     val backgroundScene by viewModel.backgroundScene.collectAsStateWithLifecycle()
72 
73     Box(modifier) {
74         if (backgroundScene == Scenes.Lockscreen) {
75             // Lockscreen content is optionally injected, because variants of System UI without a
76             // lockscreen cannot provide it.
77             val lockscreenContentOrNull = lockscreenContent.get().getOrNull()
78             lockscreenContentOrNull?.apply { Content(Modifier.fillMaxSize()) }
79         }
80 
81         Scrim(onClicked = viewModel::onScrimClicked)
82 
83         Box(
84             modifier = Modifier.fillMaxSize().panelPadding(),
85             contentAlignment = panelAlignment,
86         ) {
87             Panel(
88                 modifier = Modifier.element(OverlayShade.Elements.Panel).panelSize(),
89                 content = content
90             )
91         }
92     }
93 }
94 
95 @Composable
Scrimnull96 private fun SceneScope.Scrim(
97     onClicked: () -> Unit,
98     modifier: Modifier = Modifier,
99 ) {
100     Spacer(
101         modifier =
102             modifier
103                 .element(OverlayShade.Elements.Scrim)
104                 .fillMaxSize()
105                 .background(OverlayShade.Colors.ScrimBackground)
106                 .clickable(onClick = onClicked, interactionSource = null, indication = null)
107     )
108 }
109 
110 @Composable
SceneScopenull111 private fun SceneScope.Panel(
112     modifier: Modifier = Modifier,
113     content: @Composable () -> Unit,
114 ) {
115     Box(modifier = modifier.clip(OverlayShade.Shapes.RoundedCornerPanel)) {
116         Spacer(
117             modifier =
118                 Modifier.element(OverlayShade.Elements.PanelBackground)
119                     .matchParentSize()
120                     .background(
121                         color = OverlayShade.Colors.PanelBackground,
122                         shape = OverlayShade.Shapes.RoundedCornerPanel,
123                     ),
124         )
125 
126         // This content is intentionally rendered as a separate element from the background in order
127         // to allow for more flexibility when defining transitions.
128         content()
129     }
130 }
131 
132 @Composable
panelSizenull133 private fun Modifier.panelSize(): Modifier {
134     val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass
135 
136     return this.then(
137         when (widthSizeClass) {
138             WindowWidthSizeClass.Compact -> Modifier.fillMaxWidth()
139             WindowWidthSizeClass.Medium -> Modifier.width(OverlayShade.Dimensions.PanelWidthMedium)
140             WindowWidthSizeClass.Expanded -> Modifier.width(OverlayShade.Dimensions.PanelWidthLarge)
141             else -> error("Unsupported WindowWidthSizeClass \"$widthSizeClass\"")
142         }
143     )
144 }
145 
146 @Composable
panelPaddingnull147 private fun Modifier.panelPadding(): Modifier {
148     val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass
149     val systemBars = WindowInsets.systemBarsIgnoringVisibility
150     val displayCutout = WindowInsets.displayCutout
151     val waterfall = WindowInsets.waterfall
152     val contentPadding = PaddingValues(all = OverlayShade.Dimensions.ScrimContentPadding)
153 
154     val combinedPadding =
155         combinePaddings(
156             systemBars.asPaddingValues(),
157             displayCutout.asPaddingValues(),
158             waterfall.asPaddingValues(),
159             contentPadding
160         )
161 
162     return if (widthSizeClass == WindowWidthSizeClass.Compact) {
163         padding(bottom = combinedPadding.calculateBottomPadding())
164     } else {
165         padding(combinedPadding)
166     }
167 }
168 
169 /** Creates a union of [paddingValues] by using the max padding of each edge. */
170 @Composable
combinePaddingsnull171 private fun combinePaddings(vararg paddingValues: PaddingValues): PaddingValues {
172     val layoutDirection = LocalLayoutDirection.current
173 
174     return PaddingValues(
175         start = paddingValues.maxOfOrNull { it.calculateStartPadding(layoutDirection) } ?: 0.dp,
176         top = paddingValues.maxOfOrNull { it.calculateTopPadding() } ?: 0.dp,
177         end = paddingValues.maxOfOrNull { it.calculateEndPadding(layoutDirection) } ?: 0.dp,
178         bottom = paddingValues.maxOfOrNull { it.calculateBottomPadding() } ?: 0.dp
179     )
180 }
181 
182 object OverlayShade {
183     object Elements {
184         val Scrim = ElementKey("OverlayShadeScrim", scenePicker = LowestZIndexScenePicker)
185         val Panel = ElementKey("OverlayShadePanel", scenePicker = LowestZIndexScenePicker)
186         val PanelBackground =
187             ElementKey("OverlayShadePanelBackground", scenePicker = LowestZIndexScenePicker)
188     }
189 
190     object Colors {
191         val ScrimBackground = Color(0, 0, 0, alpha = 255 / 3)
192         val PanelBackground: Color
193             @Composable @ReadOnlyComposable get() = MaterialTheme.colorScheme.surfaceContainer
194     }
195 
196     object Dimensions {
197         val ScrimContentPadding = 16.dp
198         val PanelCornerRadius = 46.dp
199         val PanelWidthMedium = 390.dp
200         val PanelWidthLarge = 474.dp
201         val OverscrollLimit = 32.dp
202     }
203 
204     object Shapes {
205         val RoundedCornerPanel = RoundedCornerShape(Dimensions.PanelCornerRadius)
206     }
207 }
208