1 /*
2 * Copyright (C) 2022 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.credentialmanager.common.ui
18
19 import androidx.compose.foundation.Image
20 import androidx.compose.foundation.layout.Arrangement
21 import androidx.compose.foundation.layout.Box
22 import androidx.compose.foundation.layout.Column
23 import androidx.compose.foundation.layout.Row
24 import androidx.compose.foundation.layout.heightIn
25 import androidx.compose.foundation.layout.fillMaxWidth
26 import androidx.compose.foundation.layout.padding
27 import androidx.compose.foundation.layout.size
28 import androidx.compose.foundation.layout.wrapContentHeight
29 import androidx.compose.foundation.layout.wrapContentSize
30 import androidx.compose.material.icons.Icons
31 import androidx.compose.material.icons.outlined.Lock
32 import androidx.compose.material3.Icon
33 import androidx.compose.material3.IconButton
34 import androidx.compose.material3.SuggestionChip
35 import androidx.compose.material3.SuggestionChipDefaults
36 import androidx.compose.runtime.Composable
37 import androidx.compose.runtime.getValue
38 import androidx.compose.runtime.mutableStateOf
39 import androidx.compose.runtime.remember
40 import androidx.compose.ui.Alignment
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.composed
43 import androidx.compose.ui.graphics.Color
44 import androidx.compose.ui.graphics.ImageBitmap
45 import androidx.compose.ui.graphics.graphicsLayer
46 import androidx.compose.ui.graphics.painter.Painter
47 import androidx.compose.ui.graphics.vector.ImageVector
48 import androidx.compose.ui.platform.LocalLayoutDirection
49 import androidx.compose.ui.text.AnnotatedString
50 import androidx.compose.ui.text.TextLayoutResult
51 import androidx.compose.ui.text.input.PasswordVisualTransformation
52 import androidx.compose.ui.unit.Dp
53 import androidx.compose.ui.unit.LayoutDirection
54 import androidx.compose.ui.unit.dp
55 import com.android.compose.theme.LocalAndroidColorScheme
56 import com.android.credentialmanager.ui.theme.EntryShape
57 import com.android.credentialmanager.ui.theme.Shapes
58
59 @Composable
Entrynull60 fun Entry(
61 modifier: Modifier = Modifier,
62 onClick: () -> Unit,
63 entryHeadlineText: String,
64 entrySecondLineText: String? = null,
65 entryThirdLineText: String? = null,
66 /** Supply one and only one of the [iconImageBitmap], [iconImageVector], or [iconPainter] for
67 * drawing the leading icon. */
68 iconImageBitmap: ImageBitmap? = null,
69 shouldApplyIconImageBitmapTint: Boolean = false,
70 iconImageVector: ImageVector? = null,
71 iconPainter: Painter? = null,
72 /** This will replace the [entrySecondLineText] value and render the text along with a
73 * mask on / off toggle for hiding / displaying the password value. */
74 passwordValue: String? = null,
75 /** If true, draws a trailing lock icon. */
76 isLockedAuthEntry: Boolean = false,
77 enforceOneLine: Boolean = false,
78 onTextLayout: (TextLayoutResult) -> Unit = {},
79 /** Get flow only, if present, where be drawn as a line above the headline. */
80 affiliatedDomainText: String? = null,
81 ) {
82 val iconPadding = Modifier.wrapContentSize().padding(
83 // Horizontal padding should be 16dp, but the suggestion chip itself
84 // has 8dp horizontal elements padding
85 start = 8.dp, top = 16.dp, bottom = 16.dp
86 )
87 val iconSize = Modifier.size(24.dp)
88 SuggestionChip(
89 modifier = modifier.fillMaxWidth().wrapContentHeight(),
90 onClick = onClick,
91 shape = EntryShape.FullSmallRoundedCorner,
<lambda>null92 label = {
93 Row(
94 horizontalArrangement = Arrangement.SpaceBetween,
95 modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp).padding(
96 // Total end padding should be 16dp, but the suggestion chip itself
97 // has 8dp horizontal elements padding
98 horizontal = 8.dp, vertical = 16.dp,
99 ),
100 // Make sure the trailing icon and text column are centered vertically.
101 verticalAlignment = Alignment.CenterVertically,
102 ) {
103 // Apply weight so that the trailing icon can always show.
104 Column(modifier = Modifier.wrapContentHeight().fillMaxWidth().weight(1f)) {
105 if (!affiliatedDomainText.isNullOrBlank()) {
106 BodySmallText(
107 text = affiliatedDomainText,
108 enforceOneLine = enforceOneLine,
109 onTextLayout = onTextLayout,
110 )
111 }
112 SmallTitleText(
113 text = entryHeadlineText,
114 enforceOneLine = enforceOneLine,
115 onTextLayout = onTextLayout,
116 )
117 if (passwordValue != null) {
118 Row(
119 modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
120 verticalAlignment = Alignment.CenterVertically,
121 horizontalArrangement = Arrangement.Start,
122 ) {
123 val visualTransformation = remember { PasswordVisualTransformation() }
124 val originalPassword by remember {
125 mutableStateOf(passwordValue)
126 }
127 val displayedPassword = remember {
128 mutableStateOf(
129 visualTransformation.filter(
130 AnnotatedString(originalPassword)
131 ).text.text
132 )
133 }
134 BodySmallText(
135 text = displayedPassword.value,
136 // Apply weight to allow visibility button to render first so that
137 // it doesn't get squeezed out by a super long password.
138 modifier = Modifier.wrapContentSize().weight(1f, fill = false),
139 )
140 ToggleVisibilityButton(
141 modifier = Modifier.padding(start = 12.dp).size(24.dp),
142 onToggle = {
143 if (it) {
144 displayedPassword.value = originalPassword
145 } else {
146 displayedPassword.value = visualTransformation.filter(
147 AnnotatedString(originalPassword)
148 ).text.text
149 }
150 },
151 )
152 }
153 } else if (!entrySecondLineText.isNullOrBlank()) {
154 BodySmallText(
155 text = entrySecondLineText,
156 enforceOneLine = enforceOneLine,
157 onTextLayout = onTextLayout,
158 )
159 }
160 if (!entryThirdLineText.isNullOrBlank()) {
161 BodySmallText(
162 text = entryThirdLineText,
163 enforceOneLine = enforceOneLine,
164 onTextLayout = onTextLayout,
165 )
166 }
167 }
168 if (isLockedAuthEntry) {
169 Box(modifier = Modifier.wrapContentSize().padding(start = 16.dp)) {
170 Icon(
171 imageVector = Icons.Outlined.Lock,
172 // Decorative purpose only.
173 contentDescription = null,
174 modifier = Modifier.size(24.dp),
175 tint = LocalAndroidColorScheme.current.onSurfaceVariant,
176 )
177 }
178 }
179 }
180 },
181 icon =
182 if (iconImageBitmap != null) {
183 if (shouldApplyIconImageBitmapTint) {
<lambda>null184 {
185 Box(modifier = iconPadding) {
186 Icon(
187 modifier = iconSize,
188 bitmap = iconImageBitmap,
189 tint = LocalAndroidColorScheme.current.onSurfaceVariant,
190 // Decorative purpose only.
191 contentDescription = null,
192 )
193 }
194 }
195 } else {
<lambda>null196 {
197 Box(modifier = iconPadding) {
198 Image(
199 modifier = iconSize,
200 bitmap = iconImageBitmap,
201 // Decorative purpose only.
202 contentDescription = null,
203 )
204 }
205 }
206 }
207 } else if (iconImageVector != null) {
<lambda>null208 {
209 Box(modifier = iconPadding) {
210 Icon(
211 modifier = iconSize,
212 imageVector = iconImageVector,
213 tint = LocalAndroidColorScheme.current.onSurfaceVariant,
214 // Decorative purpose only.
215 contentDescription = null,
216 )
217 }
218 }
219 } else if (iconPainter != null) {
<lambda>null220 {
221 Box(modifier = iconPadding) {
222 Icon(
223 modifier = iconSize,
224 painter = iconPainter,
225 tint = LocalAndroidColorScheme.current.onSurfaceVariant,
226 // Decorative purpose only.
227 contentDescription = null,
228 )
229 }
230 }
231 } else {
232 null
233 },
234 border = null,
235 colors = SuggestionChipDefaults.suggestionChipColors(
236 containerColor = LocalAndroidColorScheme.current.surfaceContainerHigh,
237 labelColor = LocalAndroidColorScheme.current.onSurfaceVariant,
238 iconContentColor = LocalAndroidColorScheme.current.onSurfaceVariant,
239 ),
240 )
241 }
242
243 /**
244 * A variation of the normal entry in that its background is transparent and the paddings are
245 * different (no horizontal padding).
246 */
247 @Composable
ActionEntrynull248 fun ActionEntry(
249 onClick: () -> Unit,
250 entryHeadlineText: String,
251 entrySecondLineText: String? = null,
252 iconImageBitmap: ImageBitmap,
253 ) {
254 SuggestionChip(
255 modifier = Modifier.fillMaxWidth().wrapContentHeight(),
256 onClick = onClick,
257 shape = Shapes.large,
258 label = {
259 Column(
260 modifier = Modifier.heightIn(min = 56.dp).wrapContentSize().padding(
261 start = 16.dp, top = 16.dp, bottom = 16.dp
262 ),
263 verticalArrangement = Arrangement.Center,
264 ) {
265 SmallTitleText(entryHeadlineText)
266 if (!entrySecondLineText.isNullOrBlank()) {
267 BodySmallText(entrySecondLineText)
268 }
269 }
270 },
271 icon = {
272 Box(modifier = Modifier.wrapContentSize().padding(vertical = 16.dp)) {
273 Image(
274 modifier = Modifier.size(24.dp),
275 bitmap = iconImageBitmap,
276 // Decorative purpose only.
277 contentDescription = null,
278 )
279 }
280 },
281 border = null,
282 colors = SuggestionChipDefaults.suggestionChipColors(
283 containerColor = Color.Transparent,
284 ),
285 )
286 }
287
288 /**
289 * A single row of one or two CTA buttons for continuing or cancelling the current step.
290 */
291 @Composable
CtaButtonRownull292 fun CtaButtonRow(
293 leftButton: (@Composable () -> Unit)? = null,
294 rightButton: (@Composable () -> Unit)? = null,
295 ) {
296 Row(
297 horizontalArrangement =
298 if (leftButton == null) Arrangement.End
299 else if (rightButton == null) Arrangement.Start
300 else Arrangement.SpaceBetween,
301 verticalAlignment = Alignment.CenterVertically,
302 modifier = Modifier.fillMaxWidth()
303 ) {
304 if (leftButton != null) {
305 Box(modifier = Modifier.wrapContentSize().weight(1f, fill = false)) {
306 leftButton()
307 }
308 }
309 if (rightButton != null) {
310 Box(modifier = Modifier.wrapContentSize().weight(1f, fill = false)) {
311 rightButton()
312 }
313 }
314 }
315 }
316
317 @Composable
MoreOptionTopAppBarnull318 fun MoreOptionTopAppBar(
319 text: String,
320 onNavigationIconClicked: () -> Unit,
321 navigationIcon: ImageVector,
322 navigationIconContentDescription: String,
323 bottomPadding: Dp,
324 ) {
325 Row(
326 modifier = Modifier.padding(top = 12.dp, bottom = bottomPadding),
327 verticalAlignment = Alignment.CenterVertically,
328 ) {
329 IconButton(
330 modifier = Modifier.padding(top = 8.dp, bottom = 8.dp, start = 4.dp).size(48.dp),
331 onClick = onNavigationIconClicked
332 ) {
333 Box(
334 modifier = Modifier.size(48.dp),
335 contentAlignment = Alignment.Center,
336 ) {
337 Icon(
338 imageVector = navigationIcon,
339 contentDescription = navigationIconContentDescription,
340 modifier = Modifier.size(24.dp).autoMirrored(),
341 tint = LocalAndroidColorScheme.current.onSurfaceVariant,
342 )
343 }
344 }
345 LargeTitleText(text = text, modifier = Modifier.padding(horizontal = 4.dp))
346 }
347 }
348
<lambda>null349 private fun Modifier.autoMirrored() = composed {
350 when (LocalLayoutDirection.current) {
351 LayoutDirection.Rtl -> graphicsLayer(scaleX = -1f)
352 else -> this
353 }
354 }