Merge "Updated the CredentialManager UI style." into udc-dev am: 97a4d91c0f

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21532412

Change-Id: If4b1028e25170fdc22f3e456964f2a6bd754b64b
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Helen Qin
2023-02-24 23:09:08 +00:00
committed by Automerger Merge Worker
30 changed files with 1825 additions and 1388 deletions

View File

@@ -94,6 +94,8 @@
<string name="accessibility_back_arrow_button">"Go back to the previous page"</string> <string name="accessibility_back_arrow_button">"Go back to the previous page"</string>
<!-- Spoken content description of the close "X" icon button. --> <!-- Spoken content description of the close "X" icon button. -->
<string name="accessibility_close_button">Close</string> <string name="accessibility_close_button">Close</string>
<!-- Spoken content description of the close "X" icon button. [CHAR LIMIT=NONE] -->
<string name="accessibility_snackbar_dismiss">Dismiss</string>
<!-- Strings for the get flow. --> <!-- Strings for the get flow. -->
<!-- This appears as the title of the modal bottom sheet asking for user confirmation to use the single previously saved passkey to sign in to the app. [CHAR LIMIT=200] --> <!-- This appears as the title of the modal bottom sheet asking for user confirmation to use the single previously saved passkey to sign in to the app. [CHAR LIMIT=200] -->

View File

@@ -1,13 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.CredentialSelector" parent="@android:style/ThemeOverlay.Material"> <style name="Theme.CredentialSelector" parent="@android:style/ThemeOverlay.Material">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item> <item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item> <item name="android:windowIsTranslucent">true</item>
<item name="android:colorBackgroundCacheHint">@null</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="fontFamily">google-sans</item> <item name="android:navigationBarColor">@android:color/transparent</item>
</style> </style>
</resources> </resources>

View File

@@ -16,8 +16,6 @@
package com.android.credentialmanager package com.android.credentialmanager
import android.app.slice.Slice
import android.app.slice.SliceSpec
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.credentials.CreateCredentialRequest import android.credentials.CreateCredentialRequest
@@ -35,7 +33,6 @@ import android.credentials.ui.RequestInfo
import android.credentials.ui.BaseDialogResult import android.credentials.ui.BaseDialogResult
import android.credentials.ui.ProviderPendingIntentResponse import android.credentials.ui.ProviderPendingIntentResponse
import android.credentials.ui.UserSelectionDialogResult import android.credentials.ui.UserSelectionDialogResult
import android.net.Uri
import android.os.IBinder import android.os.IBinder
import android.os.Binder import android.os.Binder
import android.os.Bundle import android.os.Bundle
@@ -230,7 +227,8 @@ class CredentialManagerRepo(
context, context,
"key1", "subkey-1", "elisa.beckett@gmail.com", "key1", "subkey-1", "elisa.beckett@gmail.com",
20, 7, 27, Instant.ofEpochSecond(10L), 20, 7, 27, Instant.ofEpochSecond(10L),
"Legal note" "You can use your passkey on this or other devices. It is saved to " +
"the Password Manager for elisa.beckett@gmail.com."
), ),
CreateTestUtils.newCreateEntry( CreateTestUtils.newCreateEntry(
context, context,
@@ -239,11 +237,9 @@ class CredentialManagerRepo(
null null
), ),
) )
) ).setRemoteEntry(
.setRemoteEntry( CreateTestUtils.newRemoteCreateEntry(context, "key2", "subkey-1")
newRemoteEntry("key2", "subkey-1") ).build(),
)
.build(),
CreateCredentialProviderData CreateCredentialProviderData
.Builder("com.dashlane") .Builder("com.dashlane")
.setSaveEntries( .setSaveEntries(
@@ -258,11 +254,11 @@ class CredentialManagerRepo(
context, context,
"key1", "subkey-4", "elisa.work@dashlane.com", "key1", "subkey-4", "elisa.work@dashlane.com",
20, 7, 27, Instant.ofEpochSecond(14L), 20, 7, 27, Instant.ofEpochSecond(14L),
null "You can use your passkey on this or other devices. It is saved to " +
"the Password Manager for elisa.work@dashlane.com"
), ),
) )
) ).build(),
.build(),
) )
} }
@@ -318,7 +314,7 @@ class CredentialManagerRepo(
), ),
) )
).setRemoteEntry( ).setRemoteEntry(
newRemoteEntry("key4", "subkey-1") GetTestUtils.newRemoteCredentialEntry(context, "key4", "subkey-1")
).build(), ).build(),
GetCredentialProviderData.Builder("com.dashlane") GetCredentialProviderData.Builder("com.dashlane")
.setCredentialEntries( .setCredentialEntries(
@@ -333,10 +329,12 @@ class CredentialManagerRepo(
), ),
) )
).setAuthenticationEntries( ).setAuthenticationEntries(
listOf(GetTestUtils.newAuthenticationEntry( listOf(
context, "key2", "subkey-1", "foo@email.com", GetTestUtils.newAuthenticationEntry(
AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT context, "key2", "subkey-1", "foo@email.com",
)) AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT,
)
)
).setActionChips( ).setActionChips(
listOf( listOf(
GetTestUtils.newActionEntry( GetTestUtils.newActionEntry(
@@ -348,20 +346,6 @@ class CredentialManagerRepo(
) )
} }
private fun newRemoteEntry(
key: String,
subkey: String,
): Entry {
return Entry(
key,
subkey,
Slice.Builder(
Uri.EMPTY, SliceSpec("type", 1)
).build()
)
}
private fun testCreatePasskeyRequestInfo(): RequestInfo { private fun testCreatePasskeyRequestInfo(): RequestInfo {
val request = CreatePublicKeyCredentialRequest( val request = CreatePublicKeyCredentialRequest(
"{\"extensions\": {\n" + "{\"extensions\": {\n" +

View File

@@ -39,7 +39,7 @@ import com.android.credentialmanager.createflow.CreateCredentialScreen
import com.android.credentialmanager.createflow.hasContentToDisplay import com.android.credentialmanager.createflow.hasContentToDisplay
import com.android.credentialmanager.getflow.GetCredentialScreen import com.android.credentialmanager.getflow.GetCredentialScreen
import com.android.credentialmanager.getflow.hasContentToDisplay import com.android.credentialmanager.getflow.hasContentToDisplay
import com.android.credentialmanager.ui.theme.CredentialSelectorTheme import com.android.credentialmanager.ui.theme.PlatformTheme
@ExperimentalMaterialApi @ExperimentalMaterialApi
class CredentialSelectorActivity : ComponentActivity() { class CredentialSelectorActivity : ComponentActivity() {
@@ -50,7 +50,7 @@ class CredentialSelectorActivity : ComponentActivity() {
val userConfigRepo = UserConfigRepo(this) val userConfigRepo = UserConfigRepo(this)
val credManRepo = CredentialManagerRepo(this, intent, userConfigRepo) val credManRepo = CredentialManagerRepo(this, intent, userConfigRepo)
setContent { setContent {
CredentialSelectorTheme { PlatformTheme {
CredentialManagerBottomSheet( CredentialManagerBottomSheet(
credManRepo, credManRepo,
userConfigRepo userConfigRepo

View File

@@ -287,6 +287,7 @@ class GetFlowUtils {
pendingIntent = structuredAuthEntry.pendingIntent, pendingIntent = structuredAuthEntry.pendingIntent,
fillInIntent = entry.frameworkExtrasIntent, fillInIntent = entry.frameworkExtrasIntent,
title = title, title = title,
providerDisplayName = providerDisplayName,
icon = providerIcon, icon = providerIcon,
isUnlockedAndEmpty = entry.status != AuthenticationEntry.STATUS_LOCKED, isUnlockedAndEmpty = entry.status != AuthenticationEntry.STATUS_LOCKED,
isLastUnlocked = isLastUnlocked =

View File

@@ -29,6 +29,8 @@ import android.provider.Settings
import androidx.credentials.provider.CreateEntry import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.PasswordCredentialEntry import androidx.credentials.provider.PasswordCredentialEntry
import androidx.credentials.provider.PublicKeyCredentialEntry import androidx.credentials.provider.PublicKeyCredentialEntry
import androidx.credentials.provider.RemoteCreateEntry
import androidx.credentials.provider.RemoteCredentialEntry
import java.time.Instant import java.time.Instant
@@ -69,6 +71,21 @@ class GetTestUtils {
) )
} }
internal fun newRemoteCredentialEntry(
context: Context,
key: String,
subkey: String,
): Entry {
val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
val pendingIntent =
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
return Entry(
key,
subkey,
RemoteCredentialEntry(pendingIntent).slice
)
}
internal fun newActionEntry( internal fun newActionEntry(
context: Context, context: Context,
key: String, key: String,
@@ -203,5 +220,20 @@ class CreateTestUtils {
Intent() Intent()
) )
} }
internal fun newRemoteCreateEntry(
context: Context,
key: String,
subkey: String,
): Entry {
val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
val pendingIntent =
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
return Entry(
key,
subkey,
RemoteCreateEntry(pendingIntent).slice
)
}
} }
} }

View File

@@ -451,7 +451,7 @@ private fun Modifier.bottomSheetSwipeable(
} }
@Composable @Composable
private fun Scrim( internal fun Scrim(
color: Color, color: Color,
onDismiss: () -> Unit, onDismiss: () -> Unit,
visible: Boolean visible: Boolean

View File

@@ -16,6 +16,7 @@
package com.android.credentialmanager.common.ui package com.android.credentialmanager.common.ui
import androidx.compose.foundation.layout.padding
import com.android.credentialmanager.R import com.android.credentialmanager.R
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
@@ -24,7 +25,6 @@ import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@@ -32,17 +32,22 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
@Composable @Composable
fun ActionButton(text: String, onClick: () -> Unit) { fun ActionButton(text: String, onClick: () -> Unit) {
TextButton( TextButton(
modifier = Modifier.padding(vertical = 4.dp),
onClick = onClick, onClick = onClick,
colors = ButtonDefaults.textButtonColors( colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.primary,
) )
) { ) {
Text(text = text) LargeLabelText(
text = text,
modifier = Modifier.padding(vertical = 10.dp, horizontal = 12.dp),
)
} }
} }

View File

@@ -16,33 +16,73 @@
package com.android.credentialmanager.common.ui package com.android.credentialmanager.common.ui
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
/** /**
* By default the card is filled with surfaceVariant color. This container card instead fills the * Container card for the whole sheet.
* background color with surface corlor. *
* Surface 1 color. No vertical padding. 24dp horizontal padding. 24dp bottom padding. 24dp top
* padding if [topAppBar] is not present, and none otherwise.
*/ */
@Composable @Composable
fun ContainerCard( fun SheetContainerCard(
topAppBar: (@Composable () -> Unit)? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
shape: Shape = CardDefaults.shape,
border: BorderStroke? = null,
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
Card( Card(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth().wrapContentHeight(),
shape = shape, border = null,
border = border,
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
ElevationTokens.Level1
),
),
) {
if (topAppBar != null) {
topAppBar()
}
Column(
modifier = Modifier.padding(
start = 24.dp,
end = 24.dp,
bottom = 18.dp,
top = if (topAppBar == null) 24.dp else 0.dp
).fillMaxWidth().wrapContentHeight(),
horizontalAlignment = Alignment.CenterHorizontally,
content = content,
)
}
}
/**
* Container card for the entries.
*
* Surface 3 color. No padding. Four rounded corner shape.
*/
@Composable
fun CredentialContainerCard(
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
Card(
modifier = modifier.fillMaxWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.medium,
border = null,
colors = CardDefaults.cardColors(
containerColor = Color.Transparent,
), ),
content = content, content = content,
) )

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.common.ui
import androidx.compose.material3.ColorScheme
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.math.ln
fun ColorScheme.surfaceColorAtElevation(elevation: Dp): Color {
if (elevation == 0.dp) return surface
val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
return surfaceTint.copy(alpha = alpha).compositeOver(surface)
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.common.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
@Composable
fun EntryListColumn(content: LazyListScope.() -> Unit) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
content = content,
)
}

View File

@@ -16,21 +16,28 @@
package com.android.credentialmanager.common.ui package com.android.credentialmanager.common.ui
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/** Primary container color; label-large button text; on-primary button text color. */
@Composable @Composable
fun ConfirmButton(text: String, onClick: () -> Unit) { fun ConfirmButton(text: String, onClick: () -> Unit) {
FilledTonalButton( FilledTonalButton(
modifier = Modifier.padding(vertical = 4.dp),
onClick = onClick, onClick = onClick,
colors = ButtonDefaults.filledTonalButtonColors( colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer, containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer, contentColor = MaterialTheme.colorScheme.onPrimary,
) )
) { ) {
Text(text = text) LargeLabelText(
text = text,
modifier = Modifier.padding(vertical = 10.dp, horizontal = 24.dp),
)
} }
} }

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.common.ui
import androidx.compose.ui.unit.dp
/** Copied from androidx.compose.material3.tokens. */
internal object ElevationTokens {
val Level0 = 0.0.dp
val Level1 = 1.0.dp
val Level2 = 3.0.dp
val Level3 = 6.0.dp
val Level4 = 8.0.dp
val Level5 = 12.0.dp
}

View File

@@ -16,55 +16,314 @@
package com.android.credentialmanager.common.ui package com.android.credentialmanager.common.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.SuggestionChipDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import com.android.credentialmanager.R
import com.android.credentialmanager.ui.theme.EntryShape import com.android.credentialmanager.ui.theme.EntryShape
import com.android.credentialmanager.ui.theme.Shapes
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun Entry( fun Entry(
onClick: () -> Unit,
label: @Composable () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: @Composable (() -> Unit)? = null, onClick: () -> Unit,
entryHeadlineText: String,
entrySecondLineText: String? = null,
entryThirdLineText: String? = null,
/** Supply one and only one of the [iconImageBitmap], [iconImageVector], or [iconPainter] for
* drawing the leading icon. */
iconImageBitmap: ImageBitmap? = null,
shouldApplyIconImageBitmapTint: Boolean = false,
iconImageVector: ImageVector? = null,
iconPainter: Painter? = null,
/** This will replace the [entrySecondLineText] value and render the text along with a
* mask on / off toggle for hiding / displaying the password value. */
passwordValue: String? = null,
/** If true, draws a trailing lock icon. */
isLockedAuthEntry: Boolean = false,
) { ) {
SuggestionChip( SuggestionChip(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth().wrapContentHeight(),
onClick = onClick, onClick = onClick,
shape = EntryShape.FullSmallRoundedCorner, shape = EntryShape.FullSmallRoundedCorner,
label = label, label = {
icon = icon, Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(all = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.wrapContentSize()) {
SmallTitleText(entryHeadlineText)
if (passwordValue != null) {
Row(modifier = Modifier.fillMaxWidth()) {
val visualTransformation = remember { PasswordVisualTransformation() }
val originalPassword by remember {
mutableStateOf(passwordValue)
}
val displayedPassword = remember {
mutableStateOf(
visualTransformation.filter(
AnnotatedString(originalPassword)
).text.text
)
}
BodySmallText(displayedPassword.value)
ToggleVisibilityButton(
modifier = Modifier.padding(start = 5.dp).size(24.dp),
onToggle = {
if (it) {
displayedPassword.value = originalPassword
} else {
displayedPassword.value = visualTransformation.filter(
AnnotatedString(originalPassword)
).text.text
}
},
)
}
} else if (entrySecondLineText != null) {
BodySmallText(entrySecondLineText)
}
if (entryThirdLineText != null) {
BodySmallText(entryThirdLineText)
}
}
if (isLockedAuthEntry) {
Box(modifier = Modifier.wrapContentSize().padding(end = 16.dp)) {
Icon(
imageVector = Icons.Outlined.Lock,
// Decorative purpose only.
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
},
icon =
if (iconImageBitmap != null) {
if (shouldApplyIconImageBitmapTint) {
{
Box(modifier = Modifier.wrapContentSize()
.padding(start = 16.dp, top = 16.dp, bottom = 16.dp)) {
Icon(
modifier = Modifier.size(24.dp),
bitmap = iconImageBitmap,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
// Decorative purpose only.
contentDescription = null,
)
}
}
} else {
{
Box(modifier = Modifier.wrapContentSize()
.padding(start = 16.dp, top = 16.dp, bottom = 16.dp)) {
Image(
modifier = Modifier.size(24.dp),
bitmap = iconImageBitmap,
// Decorative purpose only.
contentDescription = null,
)
}
}
}
} else if (iconImageVector != null) {
{
Box(modifier = Modifier.wrapContentSize()
.padding(start = 16.dp, top = 16.dp, bottom = 16.dp)) {
Icon(
modifier = Modifier.size(24.dp),
imageVector = iconImageVector,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
// Decorative purpose only.
contentDescription = null,
)
}
}
} else if (iconPainter != null) {
{
Box(modifier = Modifier.wrapContentSize()
.padding(start = 16.dp, top = 16.dp, bottom = 16.dp)) {
Icon(
modifier = Modifier.size(24.dp),
painter = iconPainter,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
// Decorative purpose only.
contentDescription = null,
)
}
}
} else {
null
},
border = null, border = null,
colors = SuggestionChipDefaults.suggestionChipColors( colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant, containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
ElevationTokens.Level3
),
// TODO: remove?
labelColor = MaterialTheme.colorScheme.onSurfaceVariant, labelColor = MaterialTheme.colorScheme.onSurfaceVariant,
iconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, iconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
), ),
) )
} }
@OptIn(ExperimentalMaterial3Api::class) /**
* A variation of the normal entry in that its background is transparent and the paddings are
* different (no horizontal padding).
*/
@Composable @Composable
fun TransparentBackgroundEntry( fun ActionEntry(
onClick: () -> Unit, onClick: () -> Unit,
label: @Composable () -> Unit, entryHeadlineText: String,
modifier: Modifier = Modifier, entrySecondLineText: String? = null,
icon: @Composable (() -> Unit)? = null, iconImageBitmap: ImageBitmap,
) { ) {
SuggestionChip( SuggestionChip(
modifier = modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().wrapContentHeight(),
onClick = onClick, onClick = onClick,
label = label, shape = Shapes.large,
icon = icon, label = {
Column(modifier = Modifier.wrapContentSize()
.padding(start = 16.dp, top = 16.dp, bottom = 16.dp)) {
SmallTitleText(entryHeadlineText)
if (entrySecondLineText != null) {
BodySmallText(entrySecondLineText)
}
}
},
icon = {
Box(modifier = Modifier.wrapContentSize().padding(vertical = 16.dp)) {
Image(
modifier = Modifier.size(24.dp),
bitmap = iconImageBitmap,
// Decorative purpose only.
contentDescription = null,
)
}
},
border = null, border = null,
colors = SuggestionChipDefaults.suggestionChipColors( colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = Color.Transparent, containerColor = Color.Transparent,
), ),
) )
}
/**
* A single row of leading icon and text describing a benefit of passkeys, used by the
* [com.android.credentialmanager.createflow.PasskeyIntroCard].
*/
@Composable
fun PasskeyBenefitRow(
leadingIconPainter: Painter,
text: String,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
modifier = Modifier.size(24.dp),
painter = leadingIconPainter,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
// Decorative purpose only.
contentDescription = null,
)
BodyMediumText(text = text)
}
}
/**
* A single row of one or two CTA buttons for continuing or cancelling the current step.
*/
@Composable
fun CtaButtonRow(
leftButton: (@Composable () -> Unit)? = null,
rightButton: (@Composable () -> Unit)? = null,
) {
Row(
horizontalArrangement =
if (leftButton == null) Arrangement.End
else if (rightButton == null) Arrangement.Start
else Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
if (leftButton != null) {
leftButton()
}
if (rightButton != null) {
rightButton()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MoreOptionTopAppBar(
text: String,
onNavigationIconClicked: () -> Unit,
) {
TopAppBar(
title = {
LargeTitleText(text = text, modifier = Modifier.padding(horizontal = 4.dp))
},
navigationIcon = {
IconButton(
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp, start = 4.dp),
onClick = onNavigationIconClicked
) {
Box(
modifier = Modifier.size(48.dp),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(
R.string.accessibility_back_arrow_button
),
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
modifier = Modifier.padding(top = 12.dp, bottom = 8.dp)
)
} }

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.common.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
/** Tinted primary; centered; 32X32. */
@Composable
fun HeadlineIcon(bitmap: ImageBitmap, tint: Color? = null) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
) {
Icon(
modifier = Modifier.size(32.dp),
bitmap = bitmap,
tint = tint ?: MaterialTheme.colorScheme.primary,
// Decorative purpose only.
contentDescription = null,
)
}
}
@Composable
fun HeadlineIcon(imageVector: ImageVector) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
) {
Icon(
modifier = Modifier.size(32.dp),
imageVector = imageVector,
tint = MaterialTheme.colorScheme.primary,
// Decorative purpose only.
contentDescription = null,
)
}
}
@Composable
fun HeadlineIcon(painter: Painter) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
) {
Icon(
modifier = Modifier.size(32.dp),
painter = painter,
tint = MaterialTheme.colorScheme.primary,
// Decorative purpose only.
contentDescription = null,
)
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.common.ui
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun CredentialListSectionHeader(text: String) {
InternalSectionHeader(text, MaterialTheme.colorScheme.onSurfaceVariant)
}
@Composable
fun MoreAboutPasskeySectionHeader(text: String) {
InternalSectionHeader(text, MaterialTheme.colorScheme.onSurface)
}
@Composable
private fun InternalSectionHeader(text: String, color: Color) {
Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) {
SectionHeaderText(
text,
modifier = Modifier.padding(top = 20.dp, bottom = 8.dp),
color = color
)
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.common.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.android.credentialmanager.R
import com.android.credentialmanager.common.material.Scrim
import com.android.credentialmanager.ui.theme.Shapes
@Composable
fun Snackbar(
contentText: String,
action: (@Composable () -> Unit)? = null,
onDismiss: () -> Unit,
) {
BoxWithConstraints {
Box(Modifier.fillMaxSize()) {
Scrim(
color = Color.Transparent,
onDismiss = onDismiss,
visible = true
)
}
Box(
modifier = Modifier
.align(Alignment.BottomCenter).wrapContentSize().padding(bottom = 18.dp)
) {
Card(
shape = Shapes.medium,
modifier = Modifier.wrapContentSize(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.inverseSurface,
)
) {
Row(
modifier = Modifier.wrapContentSize(),
verticalAlignment = Alignment.CenterVertically,
) {
SnackbarContentText(contentText, modifier = Modifier.padding(
top = 18.dp, bottom = 18.dp, start = 24.dp,
))
if (action != null) {
action()
}
IconButton(onClick = onDismiss, modifier = Modifier.padding(
top = 18.dp, bottom = 18.dp, start = 16.dp, end = 24.dp,
)) {
Icon(
Icons.Filled.Close,
contentDescription = stringResource(
R.string.accessibility_snackbar_dismiss
),
tint = MaterialTheme.colorScheme.inverseOnSurface,
)
}
}
}
}
}
}

View File

@@ -16,69 +16,144 @@
package com.android.credentialmanager.common.ui package com.android.credentialmanager.common.ui
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
/**
* The headline for a screen. E.g. "Create a passkey for X", "Choose a saved sign-in for X".
*
* Centered horizontally; headline-small typography; on-surface color.
*/
@Composable @Composable
fun TextOnSurface( fun HeadlineText(text: String, modifier: Modifier = Modifier) {
text: String, Text(
modifier: Modifier = Modifier, modifier = modifier.wrapContentHeight(),
textAlign: TextAlign? = null,
style: TextStyle,
) {
TextInternal(
text = text, text = text,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
modifier = modifier, textAlign = TextAlign.Center,
textAlign = textAlign, style = MaterialTheme.typography.headlineSmall,
style = style,
) )
} }
/**
* Body-medium typography; on-surface-variant color.
*/
@Composable @Composable
fun TextSecondary( fun BodyMediumText(text: String, modifier: Modifier = Modifier) {
text: String, Text(
modifier: Modifier = Modifier, modifier = modifier.wrapContentHeight(),
textAlign: TextAlign? = null,
style: TextStyle,
) {
TextInternal(
text = text,
color = MaterialTheme.colorScheme.secondary,
modifier = modifier,
textAlign = textAlign,
style = style,
)
}
@Composable
fun TextOnSurfaceVariant(
text: String,
modifier: Modifier = Modifier,
textAlign: TextAlign? = null,
style: TextStyle,
) {
TextInternal(
text = text, text = text,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = modifier, style = MaterialTheme.typography.bodyMedium,
textAlign = textAlign,
style = style,
) )
} }
/**
* Body-small typography; on-surface-variant color.
*/
@Composable @Composable
private fun TextInternal( fun BodySmallText(text: String, modifier: Modifier = Modifier) {
text: String, Text(
color: Color, modifier = modifier.wrapContentHeight(),
modifier: Modifier, text = text,
textAlign: TextAlign?, color = MaterialTheme.colorScheme.onSurfaceVariant,
style: TextStyle, style = MaterialTheme.typography.bodySmall,
) { )
Text(text = text, color = color, modifier = modifier, textAlign = textAlign, style = style) }
/**
* Title-large typography; on-surface color.
*/
@Composable
fun LargeTitleText(text: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.wrapContentHeight(),
text = text,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge,
)
}
/**
* Title-small typography; on-surface color.
*/
@Composable
fun SmallTitleText(text: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.wrapContentHeight(),
text = text,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleSmall,
)
}
/**
* Title-small typography.
*/
@Composable
fun SectionHeaderText(text: String, modifier: Modifier = Modifier, color: Color) {
Text(
modifier = modifier.wrapContentHeight(),
text = text,
color = color,
style = MaterialTheme.typography.titleSmall,
)
}
/**
* Body-medium typography; inverse-on-surface color.
*/
@Composable
fun SnackbarContentText(text: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.wrapContentHeight(),
text = text,
color = MaterialTheme.colorScheme.inverseOnSurface,
style = MaterialTheme.typography.bodyMedium,
)
}
/**
* Label-large typography; inverse-primary color.
*/
@Composable
fun SnackbarActionText(text: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.wrapContentHeight(),
text = text,
color = MaterialTheme.colorScheme.inversePrimary,
style = MaterialTheme.typography.labelLarge,
)
}
/**
* Label-large typography; on-surface-variant color; centered.
*/
@Composable
fun LargeLabelTextOnSurfaceVariant(text: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.wrapContentHeight(),
text = text,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
)
}
/**
* Label-large typography; color following parent spec; centered.
*/
@Composable
fun LargeLabelText(text: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.wrapContentHeight(),
text = text,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelLarge,
)
} }

View File

@@ -20,40 +20,22 @@ import android.text.TextUtils
import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.IntentSenderRequest import androidx.activity.result.IntentSenderRequest
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Snackbar
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.CredentialSelectorViewModel
@@ -62,16 +44,18 @@ import com.android.credentialmanager.common.BaseEntry
import com.android.credentialmanager.common.CredentialType import com.android.credentialmanager.common.CredentialType
import com.android.credentialmanager.common.ProviderActivityState import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.common.ui.ActionButton import com.android.credentialmanager.common.ui.ActionButton
import com.android.credentialmanager.common.ui.ActionEntry
import com.android.credentialmanager.common.ui.ConfirmButton import com.android.credentialmanager.common.ui.ConfirmButton
import com.android.credentialmanager.common.ui.CredentialContainerCard
import com.android.credentialmanager.common.ui.CtaButtonRow
import com.android.credentialmanager.common.ui.Entry import com.android.credentialmanager.common.ui.Entry
import com.android.credentialmanager.common.ui.ModalBottomSheet import com.android.credentialmanager.common.ui.ModalBottomSheet
import com.android.credentialmanager.common.ui.TextOnSurface import com.android.credentialmanager.common.ui.MoreOptionTopAppBar
import com.android.credentialmanager.common.ui.TextSecondary import com.android.credentialmanager.common.ui.SheetContainerCard
import com.android.credentialmanager.common.ui.TextOnSurfaceVariant import com.android.credentialmanager.common.ui.SnackbarActionText
import com.android.credentialmanager.common.ui.ContainerCard import com.android.credentialmanager.common.ui.HeadlineText
import com.android.credentialmanager.common.ui.TransparentBackgroundEntry import com.android.credentialmanager.common.ui.CredentialListSectionHeader
import com.android.credentialmanager.ui.theme.EntryShape import com.android.credentialmanager.common.ui.Snackbar
import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
@Composable @Composable
fun GetCredentialScreen( fun GetCredentialScreen(
@@ -154,123 +138,100 @@ fun PrimarySelectionCard(
val sortedUserNameToCredentialEntryList = val sortedUserNameToCredentialEntryList =
providerDisplayInfo.sortedUserNameToCredentialEntryList providerDisplayInfo.sortedUserNameToCredentialEntryList
val authenticationEntryList = providerDisplayInfo.authenticationEntryList val authenticationEntryList = providerDisplayInfo.authenticationEntryList
ContainerCard() { SheetContainerCard {
Column() { HeadlineText(
TextOnSurface( text = stringResource(
modifier = Modifier.padding(all = 24.dp), if (sortedUserNameToCredentialEntryList
textAlign = TextAlign.Center, .size == 1 && authenticationEntryList.isEmpty()
style = MaterialTheme.typography.headlineSmall,
text = stringResource(
if (sortedUserNameToCredentialEntryList
.size == 1 && authenticationEntryList.isEmpty()
) {
if (sortedUserNameToCredentialEntryList.first()
.sortedCredentialEntryList.first().credentialType
== CredentialType.PASSKEY
) R.string.get_dialog_title_use_passkey_for
else R.string.get_dialog_title_use_sign_in_for
} else if (
sortedUserNameToCredentialEntryList
.isEmpty() && authenticationEntryList.size == 1
) {
R.string.get_dialog_title_use_sign_in_for
} else R.string.get_dialog_title_choose_sign_in_for,
requestDisplayInfo.appName
),
)
ContainerCard(
shape = MaterialTheme.shapes.medium,
modifier = Modifier
.padding(horizontal = 24.dp)
.align(alignment = Alignment.CenterHorizontally)
) {
val usernameForCredentialSize = sortedUserNameToCredentialEntryList
.size
val authenticationEntrySize = authenticationEntryList.size
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
// Show max 4 entries in this primary page if (sortedUserNameToCredentialEntryList.first()
if (usernameForCredentialSize + authenticationEntrySize <= 4) { .sortedCredentialEntryList.first().credentialType
items(sortedUserNameToCredentialEntryList) { == CredentialType.PASSKEY
CredentialEntryRow( ) R.string.get_dialog_title_use_passkey_for
credentialEntryInfo = it.sortedCredentialEntryList.first(), else R.string.get_dialog_title_use_sign_in_for
onEntrySelected = onEntrySelected, } else if (
) sortedUserNameToCredentialEntryList
} .isEmpty() && authenticationEntryList.size == 1
items(authenticationEntryList) { ) {
AuthenticationEntryRow( R.string.get_dialog_title_use_sign_in_for
authenticationEntryInfo = it, } else R.string.get_dialog_title_choose_sign_in_for,
onEntrySelected = onEntrySelected, requestDisplayInfo.appName
) ),
} )
} else if (usernameForCredentialSize < 4) { Divider(thickness = 24.dp, color = Color.Transparent)
items(sortedUserNameToCredentialEntryList) { CredentialContainerCard {
CredentialEntryRow( val usernameForCredentialSize = sortedUserNameToCredentialEntryList.size
credentialEntryInfo = it.sortedCredentialEntryList.first(), val authenticationEntrySize = authenticationEntryList.size
onEntrySelected = onEntrySelected, LazyColumn(
) verticalArrangement = Arrangement.spacedBy(2.dp)
} ) {
items(authenticationEntryList.take(4 - usernameForCredentialSize)) { // Show max 4 entries in this primary page
AuthenticationEntryRow( if (usernameForCredentialSize + authenticationEntrySize <= 4) {
authenticationEntryInfo = it, items(sortedUserNameToCredentialEntryList) {
onEntrySelected = onEntrySelected, CredentialEntryRow(
) credentialEntryInfo = it.sortedCredentialEntryList.first(),
} onEntrySelected = onEntrySelected,
} else { )
items(sortedUserNameToCredentialEntryList.take(4)) { }
CredentialEntryRow( items(authenticationEntryList) {
credentialEntryInfo = it.sortedCredentialEntryList.first(), AuthenticationEntryRow(
onEntrySelected = onEntrySelected, authenticationEntryInfo = it,
) onEntrySelected = onEntrySelected,
} )
}
} else if (usernameForCredentialSize < 4) {
items(sortedUserNameToCredentialEntryList) {
CredentialEntryRow(
credentialEntryInfo = it.sortedCredentialEntryList.first(),
onEntrySelected = onEntrySelected,
)
}
items(authenticationEntryList.take(4 - usernameForCredentialSize)) {
AuthenticationEntryRow(
authenticationEntryInfo = it,
onEntrySelected = onEntrySelected,
)
}
} else {
items(sortedUserNameToCredentialEntryList.take(4)) {
CredentialEntryRow(
credentialEntryInfo = it.sortedCredentialEntryList.first(),
onEntrySelected = onEntrySelected,
)
} }
} }
} }
Divider( }
thickness = 24.dp, Divider(thickness = 24.dp, color = Color.Transparent)
color = Color.Transparent var totalEntriesCount = sortedUserNameToCredentialEntryList
) .flatMap { it.sortedCredentialEntryList }.size + authenticationEntryList
var totalEntriesCount = sortedUserNameToCredentialEntryList .size + providerInfoList.flatMap { it.actionEntryList }.size
.flatMap { it.sortedCredentialEntryList }.size + authenticationEntryList if (providerDisplayInfo.remoteEntry != null) totalEntriesCount += 1
.size + providerInfoList.flatMap { it.actionEntryList }.size // Row horizontalArrangement differs on only one actionButton(should place on most
if (providerDisplayInfo.remoteEntry != null) totalEntriesCount += 1 // left)/only one confirmButton(should place on most right)/two buttons exist the same
// Row horizontalArrangement differs on only one actionButton(should place on most // time(should be one on the left, one on the right)
// left)/only one confirmButton(should place on most right)/two buttons exist the same CtaButtonRow(
// time(should be one on the left, one on the right) leftButton = if (totalEntriesCount > 1) {
Row( {
horizontalArrangement =
if (totalEntriesCount <= 1 && activeEntry != null) Arrangement.End
else if (totalEntriesCount > 1 && activeEntry == null) Arrangement.Start
else Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
if (totalEntriesCount > 1) {
ActionButton( ActionButton(
stringResource(R.string.get_dialog_use_saved_passkey_for), stringResource(R.string.get_dialog_use_saved_passkey_for),
onMoreOptionSelected onMoreOptionSelected
) )
} }
// Only one sign-in options exist } else null,
if (activeEntry != null) { rightButton = if (activeEntry != null) { // Only one sign-in options exist
{
ConfirmButton( ConfirmButton(
stringResource(R.string.string_continue), stringResource(R.string.string_continue),
onClick = onConfirm onClick = onConfirm
) )
} }
} } else null,
Divider( )
thickness = 18.dp,
color = Color.Transparent,
modifier = Modifier.padding(bottom = 16.dp)
)
}
} }
} }
/** Draws the secondary credential selection page, where all sign-in options are listed. */ /** Draws the secondary credential selection page, where all sign-in options are listed. */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AllSignInOptionCard( fun AllSignInOptionCard(
providerInfoList: List<ProviderInfo>, providerInfoList: List<ProviderInfo>,
@@ -283,87 +244,53 @@ fun AllSignInOptionCard(
val sortedUserNameToCredentialEntryList = val sortedUserNameToCredentialEntryList =
providerDisplayInfo.sortedUserNameToCredentialEntryList providerDisplayInfo.sortedUserNameToCredentialEntryList
val authenticationEntryList = providerDisplayInfo.authenticationEntryList val authenticationEntryList = providerDisplayInfo.authenticationEntryList
ContainerCard() { SheetContainerCard(topAppBar = {
Column() { MoreOptionTopAppBar(
TopAppBar( text = stringResource(R.string.get_dialog_title_sign_in_options),
colors = TopAppBarDefaults.topAppBarColors( onNavigationIconClicked = if (isNoAccount) onCancel else onBackButtonClicked,
containerColor = Color.Transparent, )
), }) {
title = { LazyColumn {
TextOnSurface( // For username
text = stringResource(R.string.get_dialog_title_sign_in_options), items(sortedUserNameToCredentialEntryList) { item ->
style = MaterialTheme.typography.titleMedium PerUserNameCredentials(
perUserNameCredentialEntryList = item,
onEntrySelected = onEntrySelected,
)
}
// Locked password manager
if (authenticationEntryList.isNotEmpty()) {
item {
LockedCredentials(
authenticationEntryList = authenticationEntryList,
onEntrySelected = onEntrySelected,
) )
},
navigationIcon = {
IconButton(onClick = if (isNoAccount) onCancel else onBackButtonClicked) {
Icon(
Icons.Filled.ArrowBack,
contentDescription = stringResource(
R.string.accessibility_back_arrow_button)
)
}
},
modifier = Modifier.padding(top = 12.dp)
)
ContainerCard(
shape = MaterialTheme.shapes.large,
modifier = Modifier
.padding(start = 24.dp, end = 24.dp, bottom = 24.dp)
.align(alignment = Alignment.CenterHorizontally),
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// For username
items(sortedUserNameToCredentialEntryList) { item ->
PerUserNameCredentials(
perUserNameCredentialEntryList = item,
onEntrySelected = onEntrySelected,
)
}
// Locked password manager
if (authenticationEntryList.isNotEmpty()) {
item {
LockedCredentials(
authenticationEntryList = authenticationEntryList,
onEntrySelected = onEntrySelected,
)
}
}
item {
Divider(
thickness = 8.dp,
color = Color.Transparent,
)
}
// From another device
val remoteEntry = providerDisplayInfo.remoteEntry
if (remoteEntry != null) {
item {
RemoteEntryCard(
remoteEntry = remoteEntry,
onEntrySelected = onEntrySelected,
)
}
}
item {
Divider(
thickness = 1.dp,
color = Color.LightGray,
modifier = Modifier.padding(top = 16.dp)
)
}
// Manage sign-ins (action chips)
item {
ActionChips(
providerInfoList = providerInfoList,
onEntrySelected = onEntrySelected
)
}
} }
} }
// From another device
val remoteEntry = providerDisplayInfo.remoteEntry
if (remoteEntry != null) {
item {
RemoteEntryCard(
remoteEntry = remoteEntry,
onEntrySelected = onEntrySelected,
)
}
}
item {
Divider(
thickness = 1.dp,
color = Color.LightGray,
modifier = Modifier.padding(top = 16.dp)
)
}
// Manage sign-ins (action chips)
item {
ActionChips(
providerInfoList = providerInfoList,
onEntrySelected = onEntrySelected
)
}
} }
} }
} }
@@ -381,17 +308,11 @@ fun ActionChips(
return return
} }
TextSecondary( CredentialListSectionHeader(
text = stringResource(R.string.get_dialog_heading_manage_sign_ins), text = stringResource(R.string.get_dialog_heading_manage_sign_ins)
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 8.dp)
) )
// TODO: tweak padding. CredentialContainerCard {
ContainerCard( Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.medium,
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
actionChips.forEach { actionChips.forEach {
ActionEntryRow(it, onEntrySelected) ActionEntryRow(it, onEntrySelected)
} }
@@ -404,38 +325,20 @@ fun RemoteEntryCard(
remoteEntry: RemoteEntryInfo, remoteEntry: RemoteEntryInfo,
onEntrySelected: (BaseEntry) -> Unit, onEntrySelected: (BaseEntry) -> Unit,
) { ) {
TextSecondary( CredentialListSectionHeader(
text = stringResource(R.string.get_dialog_heading_from_another_device), text = stringResource(R.string.get_dialog_heading_from_another_device)
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 8.dp)
) )
ContainerCard( CredentialContainerCard {
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.medium,
) {
Column( Column(
modifier = Modifier.fillMaxWidth().wrapContentHeight(), modifier = Modifier.fillMaxWidth().wrapContentHeight(),
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
) { ) {
Entry( Entry(
onClick = { onEntrySelected(remoteEntry) }, onClick = { onEntrySelected(remoteEntry) },
icon = { iconPainter = painterResource(R.drawable.ic_other_devices),
Icon( entryHeadlineText = stringResource(
painter = painterResource(R.drawable.ic_other_devices), R.string.get_dialog_option_headline_use_a_different_device
contentDescription = null, ),
tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
modifier = Modifier.padding(start = 10.dp)
)
},
label = {
TextOnSurfaceVariant(
text = stringResource(
R.string.get_dialog_option_headline_use_a_different_device),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(start = 10.dp, top = 18.dp, bottom = 18.dp)
.align(alignment = Alignment.CenterHorizontally)
)
}
) )
} }
} }
@@ -446,15 +349,10 @@ fun LockedCredentials(
authenticationEntryList: List<AuthenticationEntryInfo>, authenticationEntryList: List<AuthenticationEntryInfo>,
onEntrySelected: (BaseEntry) -> Unit, onEntrySelected: (BaseEntry) -> Unit,
) { ) {
TextSecondary( CredentialListSectionHeader(
text = stringResource(R.string.get_dialog_heading_locked_password_managers), text = stringResource(R.string.get_dialog_heading_locked_password_managers)
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(vertical = 8.dp)
) )
ContainerCard( CredentialContainerCard {
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.medium,
) {
Column( Column(
modifier = Modifier.fillMaxWidth().wrapContentHeight(), modifier = Modifier.fillMaxWidth().wrapContentHeight(),
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
@@ -471,17 +369,12 @@ fun PerUserNameCredentials(
perUserNameCredentialEntryList: PerUserNameCredentialEntryList, perUserNameCredentialEntryList: PerUserNameCredentialEntryList,
onEntrySelected: (BaseEntry) -> Unit, onEntrySelected: (BaseEntry) -> Unit,
) { ) {
TextSecondary( CredentialListSectionHeader(
text = stringResource( text = stringResource(
R.string.get_dialog_heading_for_username, perUserNameCredentialEntryList.userName R.string.get_dialog_heading_for_username, perUserNameCredentialEntryList.userName
), )
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 8.dp)
) )
ContainerCard( CredentialContainerCard {
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.medium,
) {
Column( Column(
modifier = Modifier.fillMaxWidth().wrapContentHeight(), modifier = Modifier.fillMaxWidth().wrapContentHeight(),
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
@@ -500,53 +393,28 @@ fun CredentialEntryRow(
) { ) {
Entry( Entry(
onClick = { onEntrySelected(credentialEntryInfo) }, onClick = { onEntrySelected(credentialEntryInfo) },
icon = { iconImageBitmap = credentialEntryInfo.icon?.toBitmap()?.asImageBitmap(),
if (credentialEntryInfo.icon != null) { // Fall back to iconPainter if iconImageBitmap isn't available
Image( iconPainter =
modifier = Modifier.padding(start = 10.dp).size(32.dp), if (credentialEntryInfo.icon == null) painterResource(R.drawable.ic_other_sign_in)
bitmap = credentialEntryInfo.icon.toBitmap().asImageBitmap(), else null,
contentDescription = null, entryHeadlineText = credentialEntryInfo.userName,
) entrySecondLineText = if (
} else { credentialEntryInfo.credentialType == CredentialType.PASSWORD) {
Icon( "••••••••••••"
modifier = Modifier.padding(start = 10.dp).size(32.dp), } else {
painter = painterResource(R.drawable.ic_other_sign_in), if (TextUtils.isEmpty(credentialEntryInfo.displayName))
contentDescription = null, credentialEntryInfo.credentialTypeDisplayName
tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant else
) credentialEntryInfo.credentialTypeDisplayName +
} stringResource(
R.string.get_dialog_sign_in_type_username_separator
) +
credentialEntryInfo.displayName
}, },
label = {
Column() {
// TODO: fix the text values.
TextOnSurfaceVariant(
text = credentialEntryInfo.userName,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 16.dp, start = 5.dp)
)
TextSecondary(
text = if (
credentialEntryInfo.credentialType == CredentialType.PASSWORD) {
"••••••••••••"
} else {
if (TextUtils.isEmpty(credentialEntryInfo.displayName))
credentialEntryInfo.credentialTypeDisplayName
else
credentialEntryInfo.credentialTypeDisplayName +
stringResource(
R.string.get_dialog_sign_in_type_username_separator
) +
credentialEntryInfo.displayName
},
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 16.dp, start = 5.dp)
)
}
}
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AuthenticationEntryRow( fun AuthenticationEntryRow(
authenticationEntryInfo: AuthenticationEntryInfo, authenticationEntryInfo: AuthenticationEntryInfo,
@@ -554,144 +422,66 @@ fun AuthenticationEntryRow(
) { ) {
Entry( Entry(
onClick = { onEntrySelected(authenticationEntryInfo) }, onClick = { onEntrySelected(authenticationEntryInfo) },
icon = { iconImageBitmap = authenticationEntryInfo.icon.toBitmap().asImageBitmap(),
Image( entryHeadlineText = authenticationEntryInfo.title,
modifier = Modifier.padding(start = 10.dp).size(32.dp), entrySecondLineText = stringResource(
bitmap = authenticationEntryInfo.icon.toBitmap().asImageBitmap(), if (authenticationEntryInfo.isUnlockedAndEmpty)
contentDescription = null R.string.locked_credential_entry_label_subtext_no_sign_in
) else R.string.locked_credential_entry_label_subtext_tap_to_unlock
}, ),
label = { isLockedAuthEntry = !authenticationEntryInfo.isUnlockedAndEmpty,
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
) {
Column() {
TextOnSurfaceVariant(
text = authenticationEntryInfo.title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 16.dp)
)
TextSecondary(
text = stringResource(
if (authenticationEntryInfo.isUnlockedAndEmpty)
R.string.locked_credential_entry_label_subtext_no_sign_in
else R.string.locked_credential_entry_label_subtext_tap_to_unlock
),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
}
if (!authenticationEntryInfo.isUnlockedAndEmpty) {
Icon(
Icons.Outlined.Lock,
null,
Modifier.align(alignment = Alignment.CenterVertically).padding(end = 10.dp),
)
}
}
}
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ActionEntryRow( fun ActionEntryRow(
actionEntryInfo: ActionEntryInfo, actionEntryInfo: ActionEntryInfo,
onEntrySelected: (BaseEntry) -> Unit, onEntrySelected: (BaseEntry) -> Unit,
) { ) {
TransparentBackgroundEntry( ActionEntry(
icon = { iconImageBitmap = actionEntryInfo.icon.toBitmap().asImageBitmap(),
Image( entryHeadlineText = actionEntryInfo.title,
modifier = Modifier.padding(start = 10.dp).size(24.dp), entrySecondLineText = actionEntryInfo.subTitle,
bitmap = actionEntryInfo.icon.toBitmap().asImageBitmap(),
contentDescription = null,
)
},
label = {
Column() {
TextOnSurfaceVariant(
text = actionEntryInfo.title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(start = 8.dp),
)
if (actionEntryInfo.subTitle != null) {
TextSecondary(
text = actionEntryInfo.subTitle,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(start = 8.dp),
)
}
}
},
onClick = { onEntrySelected(actionEntryInfo) }, onClick = { onEntrySelected(actionEntryInfo) },
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RemoteCredentialSnackBarScreen( fun RemoteCredentialSnackBarScreen(
onClick: (Boolean) -> Unit, onClick: (Boolean) -> Unit,
onCancel: () -> Unit, onCancel: () -> Unit,
) { ) {
// TODO: Change the height, width and position according to the design
Snackbar( Snackbar(
modifier = Modifier.padding(horizontal = 40.dp).padding(top = 700.dp),
shape = EntryShape.FullMediumRoundedCorner,
containerColor = LocalAndroidColorScheme.current.colorBackground,
contentColor = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
action = { action = {
TextButton( TextButton(
modifier = Modifier.padding(top = 12.dp, bottom = 12.dp, start = 16.dp),
onClick = { onClick(true) }, onClick = { onClick(true) },
) { ) {
Text(text = stringResource(R.string.snackbar_action)) SnackbarActionText(
} text = stringResource(R.string.snackbar_action),
}, Modifier.padding(vertical = 6.dp)
dismissAction = {
IconButton(onClick = onCancel) {
Icon(
Icons.Filled.Close,
contentDescription = stringResource(
R.string.accessibility_close_button
),
tint = LocalAndroidColorScheme.current.colorAccentTertiary
) )
} }
}, },
) { onDismiss = onCancel,
Text(text = stringResource(R.string.get_dialog_use_saved_passkey_for)) contentText = stringResource(R.string.get_dialog_use_saved_passkey_for),
} )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun EmptyAuthEntrySnackBarScreen( fun EmptyAuthEntrySnackBarScreen(
authenticationEntryList: List<AuthenticationEntryInfo>, authenticationEntryList: List<AuthenticationEntryInfo>,
onCancel: () -> Unit, onCancel: () -> Unit,
onLastLockedAuthEntryNotFound: () -> Unit, onLastLockedAuthEntryNotFound: () -> Unit,
) { ) {
val lastLocked = authenticationEntryList.firstOrNull({it.isLastUnlocked}) val lastLocked = authenticationEntryList.firstOrNull({ it.isLastUnlocked })
if (lastLocked == null) { if (lastLocked == null) {
onLastLockedAuthEntryNotFound() onLastLockedAuthEntryNotFound()
return return
} }
// TODO: Change the height, width and position according to the design
Snackbar( Snackbar(
modifier = Modifier.padding(horizontal = 40.dp).padding(top = 700.dp), onDismiss = onCancel,
shape = EntryShape.FullMediumRoundedCorner, contentText = stringResource(R.string.no_sign_in_info_in, lastLocked.providerDisplayName),
containerColor = LocalAndroidColorScheme.current.colorBackground, )
contentColor = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
dismissAction = {
IconButton(onClick = onCancel) {
Icon(
Icons.Filled.Close,
contentDescription = stringResource(R.string.accessibility_close_button),
tint = LocalAndroidColorScheme.current.colorAccentTertiary
)
}
},
) {
Text(text = stringResource(R.string.no_sign_in_info_in, lastLocked.title))
}
} }

View File

@@ -96,6 +96,7 @@ class AuthenticationEntryInfo(
pendingIntent: PendingIntent?, pendingIntent: PendingIntent?,
fillInIntent: Intent?, fillInIntent: Intent?,
val title: String, val title: String,
val providerDisplayName: String,
val icon: Drawable, val icon: Drawable,
// The entry had been unlocked and turned out to be empty. Used to determine whether to // The entry had been unlocked and turned out to be empty. Used to determine whether to
// show "Tap to unlock" or "No sign-in info" for this entry. // show "Tap to unlock" or "No sign-in info" for this entry.

View File

@@ -22,12 +22,14 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.android.internal.R import com.android.internal.R
/** File copied from PlatformComposeCore. */
/** CompositionLocal used to pass [AndroidColorScheme] down the tree. */ /** CompositionLocal used to pass [AndroidColorScheme] down the tree. */
val LocalAndroidColorScheme = val LocalAndroidColorScheme =
staticCompositionLocalOf<AndroidColorScheme> { staticCompositionLocalOf<AndroidColorScheme> {
throw IllegalStateException( throw IllegalStateException(
"No AndroidColorScheme configured. Make sure to use LocalAndroidColorScheme in a " + "No AndroidColorScheme configured. Make sure to use LocalAndroidColorScheme in a " +
"Composable surrounded by a CredentialSelectorTheme {}." "Composable surrounded by a PlatformTheme {}."
) )
} }
@@ -38,7 +40,6 @@ val LocalAndroidColorScheme =
* most of the colors in this class will be removed in favor of their M3 counterpart. * most of the colors in this class will be removed in favor of their M3 counterpart.
*/ */
class AndroidColorScheme internal constructor(context: Context) { class AndroidColorScheme internal constructor(context: Context) {
val colorPrimary = getColor(context, R.attr.colorPrimary) val colorPrimary = getColor(context, R.attr.colorPrimary)
val colorPrimaryDark = getColor(context, R.attr.colorPrimaryDark) val colorPrimaryDark = getColor(context, R.attr.colorPrimaryDark)
val colorAccent = getColor(context, R.attr.colorAccent) val colorAccent = getColor(context, R.attr.colorAccent)
@@ -66,10 +67,12 @@ class AndroidColorScheme internal constructor(context: Context) {
val colorForeground = getColor(context, R.attr.colorForeground) val colorForeground = getColor(context, R.attr.colorForeground)
val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse) val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
private fun getColor(context: Context, attr: Int): Color { companion object {
val ta = context.obtainStyledAttributes(intArrayOf(attr)) fun getColor(context: Context, attr: Int): Color {
@ColorInt val color = ta.getColor(0, 0) val ta = context.obtainStyledAttributes(intArrayOf(attr))
ta.recycle() @ColorInt val color = ta.getColor(0, 0)
return Color(color) ta.recycle()
return Color(color)
}
} }
} }

View File

@@ -1,14 +1,30 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.ui.theme package com.android.credentialmanager.ui.theme
import android.annotation.AttrRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
val Grey100 = Color(0xFFF1F3F4) /** Read the [Color] from the given [attribute]. */
val Purple200 = Color(0xFFBB86FC) @Composable
val Purple500 = Color(0xFF6200EE) @ReadOnlyComposable
val Purple700 = Color(0xFF3700B3) fun colorAttr(@AttrRes attribute: Int): Color {
val Teal200 = Color(0xFF03DAC5) return AndroidColorScheme.getColor(LocalContext.current, attribute)
val lightColorAccentSecondary = Color(0xFFC2E7FF) }
val lightBackgroundColor = Color(0xFFF0F0F0)
val lightSurface1 = Color(0xFF6991D6)
val textColorSecondary = Color(0xFF40484B)
val textColorPrimary = Color(0xFF191C1D)

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import com.android.credentialmanager.ui.theme.typography.TypeScaleTokens
import com.android.credentialmanager.ui.theme.typography.TypefaceNames
import com.android.credentialmanager.ui.theme.typography.TypefaceTokens
import com.android.credentialmanager.ui.theme.typography.TypographyTokens
import com.android.credentialmanager.ui.theme.typography.platformTypography
/** File copied from PlatformComposeCore. */
/** The Material 3 theme that should wrap all Platform Composables. */
@Composable
fun PlatformTheme(
isDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val context = LocalContext.current
// TODO(b/230605885): Define our color scheme.
val colorScheme =
if (isDarkTheme) {
dynamicDarkColorScheme(context)
} else {
dynamicLightColorScheme(context)
}
val androidColorScheme = AndroidColorScheme(context)
val typefaceNames = remember(context) { TypefaceNames.get(context) }
val typography =
remember(typefaceNames) {
platformTypography(TypographyTokens(TypeScaleTokens(TypefaceTokens(typefaceNames))))
}
MaterialTheme(colorScheme, typography = typography) {
CompositionLocalProvider(
LocalAndroidColorScheme provides androidColorScheme,
) {
content()
}
}
}

View File

@@ -1,38 +0,0 @@
package com.android.credentialmanager.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
@Composable
fun CredentialSelectorTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val context = LocalContext.current
val colorScheme =
if (darkTheme) {
dynamicDarkColorScheme(context)
} else {
dynamicLightColorScheme(context)
}
val androidColorScheme = AndroidColorScheme(context)
val typography = Typography
MaterialTheme(
colorScheme,
typography = typography,
shapes = Shapes
) {
CompositionLocalProvider(
LocalAndroidColorScheme provides androidColorScheme,
) {
content()
}
}
}

View File

@@ -1,56 +0,0 @@
package com.android.credentialmanager.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
titleMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 24.sp,
lineHeight = 32.sp,
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
),
bodyMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
color = textColorSecondary
),
labelLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
),
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp,
color = textColorPrimary
),
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.ui.theme.typography
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
/** File copied from PlatformComposeCore. */
/**
* The typography for Platform Compose code.
*
* Do not use directly and call [MaterialTheme.typography] instead to access the different text
* styles.
*/
internal fun platformTypography(typographyTokens: TypographyTokens): Typography {
return Typography(
displayLarge = typographyTokens.displayLarge,
displayMedium = typographyTokens.displayMedium,
displaySmall = typographyTokens.displaySmall,
headlineLarge = typographyTokens.headlineLarge,
headlineMedium = typographyTokens.headlineMedium,
headlineSmall = typographyTokens.headlineSmall,
titleLarge = typographyTokens.titleLarge,
titleMedium = typographyTokens.titleMedium,
titleSmall = typographyTokens.titleSmall,
bodyLarge = typographyTokens.bodyLarge,
bodyMedium = typographyTokens.bodyMedium,
bodySmall = typographyTokens.bodySmall,
labelLarge = typographyTokens.labelLarge,
labelMedium = typographyTokens.labelMedium,
labelSmall = typographyTokens.labelSmall,
)
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.ui.theme.typography
import androidx.compose.ui.unit.sp
/** File copied from PlatformComposeCore. */
internal class TypeScaleTokens(typefaceTokens: TypefaceTokens) {
val bodyLargeFont = typefaceTokens.plain
val bodyLargeLineHeight = 24.0.sp
val bodyLargeSize = 16.sp
val bodyLargeTracking = 0.0.sp
val bodyLargeWeight = TypefaceTokens.WeightRegular
val bodyMediumFont = typefaceTokens.plain
val bodyMediumLineHeight = 20.0.sp
val bodyMediumSize = 14.sp
val bodyMediumTracking = 0.0.sp
val bodyMediumWeight = TypefaceTokens.WeightRegular
val bodySmallFont = typefaceTokens.plain
val bodySmallLineHeight = 16.0.sp
val bodySmallSize = 12.sp
val bodySmallTracking = 0.1.sp
val bodySmallWeight = TypefaceTokens.WeightRegular
val displayLargeFont = typefaceTokens.brand
val displayLargeLineHeight = 64.0.sp
val displayLargeSize = 57.sp
val displayLargeTracking = 0.0.sp
val displayLargeWeight = TypefaceTokens.WeightRegular
val displayMediumFont = typefaceTokens.brand
val displayMediumLineHeight = 52.0.sp
val displayMediumSize = 45.sp
val displayMediumTracking = 0.0.sp
val displayMediumWeight = TypefaceTokens.WeightRegular
val displaySmallFont = typefaceTokens.brand
val displaySmallLineHeight = 44.0.sp
val displaySmallSize = 36.sp
val displaySmallTracking = 0.0.sp
val displaySmallWeight = TypefaceTokens.WeightRegular
val headlineLargeFont = typefaceTokens.brand
val headlineLargeLineHeight = 40.0.sp
val headlineLargeSize = 32.sp
val headlineLargeTracking = 0.0.sp
val headlineLargeWeight = TypefaceTokens.WeightRegular
val headlineMediumFont = typefaceTokens.brand
val headlineMediumLineHeight = 36.0.sp
val headlineMediumSize = 28.sp
val headlineMediumTracking = 0.0.sp
val headlineMediumWeight = TypefaceTokens.WeightRegular
val headlineSmallFont = typefaceTokens.brand
val headlineSmallLineHeight = 32.0.sp
val headlineSmallSize = 24.sp
val headlineSmallTracking = 0.0.sp
val headlineSmallWeight = TypefaceTokens.WeightRegular
val labelLargeFont = typefaceTokens.plain
val labelLargeLineHeight = 20.0.sp
val labelLargeSize = 14.sp
val labelLargeTracking = 0.0.sp
val labelLargeWeight = TypefaceTokens.WeightMedium
val labelMediumFont = typefaceTokens.plain
val labelMediumLineHeight = 16.0.sp
val labelMediumSize = 12.sp
val labelMediumTracking = 0.1.sp
val labelMediumWeight = TypefaceTokens.WeightMedium
val labelSmallFont = typefaceTokens.plain
val labelSmallLineHeight = 16.0.sp
val labelSmallSize = 11.sp
val labelSmallTracking = 0.1.sp
val labelSmallWeight = TypefaceTokens.WeightMedium
val titleLargeFont = typefaceTokens.brand
val titleLargeLineHeight = 28.0.sp
val titleLargeSize = 22.sp
val titleLargeTracking = 0.0.sp
val titleLargeWeight = TypefaceTokens.WeightRegular
val titleMediumFont = typefaceTokens.plain
val titleMediumLineHeight = 24.0.sp
val titleMediumSize = 16.sp
val titleMediumTracking = 0.0.sp
val titleMediumWeight = TypefaceTokens.WeightMedium
val titleSmallFont = typefaceTokens.plain
val titleSmallLineHeight = 20.0.sp
val titleSmallSize = 14.sp
val titleSmallTracking = 0.0.sp
val titleSmallWeight = TypefaceTokens.WeightMedium
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(ExperimentalTextApi::class)
package com.android.credentialmanager.ui.theme.typography
import android.content.Context
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.font.DeviceFontFamilyName
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
/** File copied from PlatformComposeCore. */
internal class TypefaceTokens(typefaceNames: TypefaceNames) {
companion object {
val WeightMedium = FontWeight.Medium
val WeightRegular = FontWeight.Normal
}
private val brandFont = DeviceFontFamilyName(typefaceNames.brand)
private val plainFont = DeviceFontFamilyName(typefaceNames.plain)
val brand =
FontFamily(
Font(brandFont, weight = WeightMedium),
Font(brandFont, weight = WeightRegular),
)
val plain =
FontFamily(
Font(plainFont, weight = WeightMedium),
Font(plainFont, weight = WeightRegular),
)
}
internal data class TypefaceNames
private constructor(
val brand: String,
val plain: String,
) {
private enum class Config(val configName: String, val default: String) {
Brand("config_headlineFontFamily", "sans-serif"),
Plain("config_bodyFontFamily", "sans-serif"),
}
companion object {
fun get(context: Context): TypefaceNames {
return TypefaceNames(
brand = getTypefaceName(context, Config.Brand),
plain = getTypefaceName(context, Config.Plain),
)
}
private fun getTypefaceName(context: Context, config: Config): String {
return context
.getString(context.resources.getIdentifier(config.configName, "string", "android"))
.takeIf { it.isNotEmpty() }
?: config.default
}
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.ui.theme.typography
import androidx.compose.ui.text.TextStyle
/** File copied from PlatformComposeCore. */
internal class TypographyTokens(typeScaleTokens: TypeScaleTokens) {
val bodyLarge =
TextStyle(
fontFamily = typeScaleTokens.bodyLargeFont,
fontWeight = typeScaleTokens.bodyLargeWeight,
fontSize = typeScaleTokens.bodyLargeSize,
lineHeight = typeScaleTokens.bodyLargeLineHeight,
letterSpacing = typeScaleTokens.bodyLargeTracking,
)
val bodyMedium =
TextStyle(
fontFamily = typeScaleTokens.bodyMediumFont,
fontWeight = typeScaleTokens.bodyMediumWeight,
fontSize = typeScaleTokens.bodyMediumSize,
lineHeight = typeScaleTokens.bodyMediumLineHeight,
letterSpacing = typeScaleTokens.bodyMediumTracking,
)
val bodySmall =
TextStyle(
fontFamily = typeScaleTokens.bodySmallFont,
fontWeight = typeScaleTokens.bodySmallWeight,
fontSize = typeScaleTokens.bodySmallSize,
lineHeight = typeScaleTokens.bodySmallLineHeight,
letterSpacing = typeScaleTokens.bodySmallTracking,
)
val displayLarge =
TextStyle(
fontFamily = typeScaleTokens.displayLargeFont,
fontWeight = typeScaleTokens.displayLargeWeight,
fontSize = typeScaleTokens.displayLargeSize,
lineHeight = typeScaleTokens.displayLargeLineHeight,
letterSpacing = typeScaleTokens.displayLargeTracking,
)
val displayMedium =
TextStyle(
fontFamily = typeScaleTokens.displayMediumFont,
fontWeight = typeScaleTokens.displayMediumWeight,
fontSize = typeScaleTokens.displayMediumSize,
lineHeight = typeScaleTokens.displayMediumLineHeight,
letterSpacing = typeScaleTokens.displayMediumTracking,
)
val displaySmall =
TextStyle(
fontFamily = typeScaleTokens.displaySmallFont,
fontWeight = typeScaleTokens.displaySmallWeight,
fontSize = typeScaleTokens.displaySmallSize,
lineHeight = typeScaleTokens.displaySmallLineHeight,
letterSpacing = typeScaleTokens.displaySmallTracking,
)
val headlineLarge =
TextStyle(
fontFamily = typeScaleTokens.headlineLargeFont,
fontWeight = typeScaleTokens.headlineLargeWeight,
fontSize = typeScaleTokens.headlineLargeSize,
lineHeight = typeScaleTokens.headlineLargeLineHeight,
letterSpacing = typeScaleTokens.headlineLargeTracking,
)
val headlineMedium =
TextStyle(
fontFamily = typeScaleTokens.headlineMediumFont,
fontWeight = typeScaleTokens.headlineMediumWeight,
fontSize = typeScaleTokens.headlineMediumSize,
lineHeight = typeScaleTokens.headlineMediumLineHeight,
letterSpacing = typeScaleTokens.headlineMediumTracking,
)
val headlineSmall =
TextStyle(
fontFamily = typeScaleTokens.headlineSmallFont,
fontWeight = typeScaleTokens.headlineSmallWeight,
fontSize = typeScaleTokens.headlineSmallSize,
lineHeight = typeScaleTokens.headlineSmallLineHeight,
letterSpacing = typeScaleTokens.headlineSmallTracking,
)
val labelLarge =
TextStyle(
fontFamily = typeScaleTokens.labelLargeFont,
fontWeight = typeScaleTokens.labelLargeWeight,
fontSize = typeScaleTokens.labelLargeSize,
lineHeight = typeScaleTokens.labelLargeLineHeight,
letterSpacing = typeScaleTokens.labelLargeTracking,
)
val labelMedium =
TextStyle(
fontFamily = typeScaleTokens.labelMediumFont,
fontWeight = typeScaleTokens.labelMediumWeight,
fontSize = typeScaleTokens.labelMediumSize,
lineHeight = typeScaleTokens.labelMediumLineHeight,
letterSpacing = typeScaleTokens.labelMediumTracking,
)
val labelSmall =
TextStyle(
fontFamily = typeScaleTokens.labelSmallFont,
fontWeight = typeScaleTokens.labelSmallWeight,
fontSize = typeScaleTokens.labelSmallSize,
lineHeight = typeScaleTokens.labelSmallLineHeight,
letterSpacing = typeScaleTokens.labelSmallTracking,
)
val titleLarge =
TextStyle(
fontFamily = typeScaleTokens.titleLargeFont,
fontWeight = typeScaleTokens.titleLargeWeight,
fontSize = typeScaleTokens.titleLargeSize,
lineHeight = typeScaleTokens.titleLargeLineHeight,
letterSpacing = typeScaleTokens.titleLargeTracking,
)
val titleMedium =
TextStyle(
fontFamily = typeScaleTokens.titleMediumFont,
fontWeight = typeScaleTokens.titleMediumWeight,
fontSize = typeScaleTokens.titleMediumSize,
lineHeight = typeScaleTokens.titleMediumLineHeight,
letterSpacing = typeScaleTokens.titleMediumTracking,
)
val titleSmall =
TextStyle(
fontFamily = typeScaleTokens.titleSmallFont,
fontWeight = typeScaleTokens.titleSmallWeight,
fontSize = typeScaleTokens.titleSmallSize,
lineHeight = typeScaleTokens.titleSmallLineHeight,
letterSpacing = typeScaleTokens.titleSmallTracking,
)
}