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 }