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