1 /*
2  * Copyright 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  *      https://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.permissioncontroller.permission.ui.wear.elements
18 
19 import androidx.compose.foundation.layout.Arrangement
20 import androidx.compose.foundation.layout.Arrangement.spacedBy
21 import androidx.compose.foundation.layout.Box
22 import androidx.compose.foundation.layout.PaddingValues
23 import androidx.compose.foundation.layout.Row
24 import androidx.compose.foundation.layout.Spacer
25 import androidx.compose.foundation.layout.fillMaxSize
26 import androidx.compose.foundation.layout.fillMaxWidth
27 import androidx.compose.foundation.layout.height
28 import androidx.compose.foundation.layout.padding
29 import androidx.compose.foundation.layout.size
30 import androidx.compose.foundation.layout.width
31 import androidx.compose.foundation.shape.CircleShape
32 import androidx.compose.material.icons.Icons
33 import androidx.compose.material.icons.filled.Check
34 import androidx.compose.material.icons.filled.Close
35 import androidx.compose.runtime.Composable
36 import androidx.compose.runtime.CompositionLocalProvider
37 import androidx.compose.ui.Alignment
38 import androidx.compose.ui.Modifier
39 import androidx.compose.ui.graphics.vector.ImageVector
40 import androidx.compose.ui.platform.LocalConfiguration
41 import androidx.compose.ui.res.stringResource
42 import androidx.compose.ui.unit.Dp
43 import androidx.compose.ui.unit.dp
44 import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
45 import androidx.wear.compose.material.ButtonDefaults
46 import androidx.wear.compose.material.ChipColors
47 import androidx.wear.compose.material.ChipDefaults
48 import androidx.wear.compose.material.LocalTextStyle
49 import androidx.wear.compose.material.MaterialTheme
50 import androidx.wear.compose.material.PositionIndicator
51 import androidx.wear.compose.material.Scaffold
52 import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumn
53 import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnDefaults.responsive
54 import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState
55 import com.android.permissioncontroller.permission.ui.wear.elements.layout.rememberColumnState
56 
57 // This file is a copy of ResponsiveDialogContent.kt from Horologist (go/horologist),
58 // remove it once after wear compose supports large screen dialogs.
59 
60 @Composable
ResponsiveDialogContentnull61 fun ResponsiveDialogContent(
62     modifier: Modifier = Modifier,
63     icon: @Composable (() -> Unit)? = null,
64     title: @Composable (() -> Unit)? = null,
65     message: @Composable (() -> Unit)? = null,
66     onOk: (() -> Unit)? = null,
67     onCancel: (() -> Unit)? = null,
68     okButtonContentDescription: String = stringResource(android.R.string.ok),
69     cancelButtonContentDescription: String = stringResource(android.R.string.cancel),
70     state: ScalingLazyColumnState =
71         rememberColumnState(
72             responsive(
73                 firstItemIsFullWidth = icon == null,
74                 additionalPaddingAtBottom = 0.dp,
75             ),
76         ),
77     showPositionIndicator: Boolean = true,
78     content: (ScalingLazyListScope.() -> Unit)? = null,
79 ) {
80     Scaffold(
81         modifier = modifier.fillMaxSize(),
82         positionIndicator = {
83             if (showPositionIndicator) {
84                 PositionIndicator(scalingLazyListState = state.state)
85             }
86         },
87         timeText = {},
88     ) {
89         // This will be applied only to the content.
90         CompositionLocalProvider(
91             LocalTextStyle provides MaterialTheme.typography.body2,
92         ) {
93             ScalingLazyColumn(columnState = state) {
94                 icon?.let {
95                     item {
96                         Row(
97                             Modifier.fillMaxWidth().padding(bottom = 4.dp), // 8.dp below icon
98                             horizontalArrangement = Arrangement.Center,
99                         ) {
100                             it()
101                         }
102                     }
103                 }
104                 title?.let {
105                     item {
106                         CompositionLocalProvider(
107                             LocalTextStyle provides MaterialTheme.typography.title3,
108                         ) {
109                             Box(
110                                 Modifier.fillMaxWidth(titleMaxWidthFraction)
111                                     .padding(bottom = 8.dp), // 12.dp below icon
112                             ) {
113                                 it()
114                             }
115                         }
116                     }
117                 }
118                 if (icon == null && title == null) {
119                     // Ensure the content is visible when there is nothing above it.
120                     item { Spacer(Modifier.height(20.dp)) }
121                 }
122                 message?.let {
123                     item {
124                         Box(
125                             Modifier.fillMaxWidth(messageMaxWidthFraction),
126                         ) {
127                             it()
128                         }
129                     }
130                 }
131                 content?.let { it() }
132                 if (onOk != null || onCancel != null) {
133                     item {
134                         val width = LocalConfiguration.current.screenWidthDp
135                         // Single buttons, or buttons on smaller screens are not meant to be
136                         // responsive.
137                         val buttonWidth =
138                             if (width < 225 || onOk == null || onCancel == null) {
139                                 ButtonDefaults.DefaultButtonSize
140                             } else {
141                                 // 14.56% on top of 5.2% margin on the sides, 12.dp between.
142                                 ((width * (1f - (2 * 0.1456f) - (2 * 0.052f)) - 12) / 2).dp
143                             }
144                         Row(
145                             Modifier.fillMaxWidth()
146                                 .padding(
147                                     top = if (content != null || message != null) 12.dp else 0.dp,
148                                 ),
149                             horizontalArrangement = spacedBy(12.dp, Alignment.CenterHorizontally),
150                             verticalAlignment = Alignment.CenterVertically,
151                         ) {
152                             onCancel?.let {
153                                 ResponsiveButton(
154                                     icon = Icons.Default.Close,
155                                     cancelButtonContentDescription,
156                                     onClick = it,
157                                     buttonWidth,
158                                     ChipDefaults.secondaryChipColors(),
159                                 )
160                             }
161                             onOk?.let {
162                                 ResponsiveButton(
163                                     icon = Icons.Default.Check,
164                                     okButtonContentDescription,
165                                     onClick = it,
166                                     buttonWidth,
167                                 )
168                             }
169                         }
170                     }
171                 }
172             }
173         }
174     }
175 }
176 
177 @Composable
ResponsiveButtonnull178 private fun ResponsiveButton(
179     icon: ImageVector,
180     contentDescription: String,
181     onClick: () -> Unit,
182     buttonWidth: Dp,
183     colors: ChipColors = ChipDefaults.primaryChipColors(),
184 ) {
185     androidx.wear.compose.material.Chip(
186         label = {
187             Box(Modifier.fillMaxWidth()) {
188                 Icon(
189                     imageVector = icon,
190                     contentDescription = contentDescription,
191                     modifier =
192                         Modifier.size(ButtonDefaults.DefaultIconSize).align(Alignment.Center),
193                 )
194             }
195         },
196         contentPadding = PaddingValues(0.dp),
197         shape = CircleShape,
198         onClick = onClick,
199         modifier = Modifier.width(buttonWidth),
200         colors = colors,
201     )
202 }
203 
204 internal const val globalHorizontalPadding = 5.2f
205 internal const val messageExtraHorizontalPadding = 4.56f
206 internal const val titleExtraHorizontalPadding = 8.84f
207 
208 // Fraction of the max available width that message should take (after global and message padding)
209 internal val messageMaxWidthFraction =
210     1f -
211         2f *
212             calculatePaddingFraction(
213                 messageExtraHorizontalPadding,
214             )
215 
216 // Fraction of the max available width that title should take (after global and message padding)
217 internal val titleMaxWidthFraction =
218     1f -
219         2f *
220             calculatePaddingFraction(
221                 titleExtraHorizontalPadding,
222             )
223 
224 // Calculate total padding given global padding and additional padding required inside that.
calculatePaddingFractionnull225 internal fun calculatePaddingFraction(extraPadding: Float) =
226     extraPadding / (100f - 2f * globalHorizontalPadding)
227