Updated the CredentialManager UI style.

Test: manual (see screenshot spreadsheet in comment)

Bug: 266578524
Bug: 269364773

Change-Id: I0d4c71568f69f37a1cabc8fa8bd5c6ba4084d273
This commit is contained in:
Helen Qin
2023-02-20 14:54:17 +00:00
parent 6c4cd643fd
commit 440ff48cac
30 changed files with 1826 additions and 1389 deletions

View File

@@ -94,6 +94,8 @@
<string name="accessibility_back_arrow_button">"Go back to the previous page"</string>
<!-- Spoken content description of the close "X" icon button. -->
<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. -->
<!-- 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"?>
<resources>
<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:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="fontFamily">google-sans</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
</resources>

View File

@@ -16,8 +16,6 @@
package com.android.credentialmanager
import android.app.slice.Slice
import android.app.slice.SliceSpec
import android.content.Context
import android.content.Intent
import android.credentials.CreateCredentialRequest
@@ -35,7 +33,6 @@ import android.credentials.ui.RequestInfo
import android.credentials.ui.BaseDialogResult
import android.credentials.ui.ProviderPendingIntentResponse
import android.credentials.ui.UserSelectionDialogResult
import android.net.Uri
import android.os.IBinder
import android.os.Binder
import android.os.Bundle
@@ -73,7 +70,7 @@ class CredentialManagerRepo(
requestInfo = intent.extras?.getParcelable(
RequestInfo.EXTRA_REQUEST_INFO,
RequestInfo::class.java
) ?: testCreatePasskeyRequestInfo()
) ?: testCreatePasswordRequestInfo()
providerEnabledList = when (requestInfo.type) {
RequestInfo.TYPE_CREATE ->
@@ -230,7 +227,8 @@ class CredentialManagerRepo(
context,
"key1", "subkey-1", "elisa.beckett@gmail.com",
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(
context,
@@ -239,11 +237,9 @@ class CredentialManagerRepo(
null
),
)
)
.setRemoteEntry(
newRemoteEntry("key2", "subkey-1")
)
.build(),
).setRemoteEntry(
CreateTestUtils.newRemoteCreateEntry(context, "key2", "subkey-1")
).build(),
CreateCredentialProviderData
.Builder("com.dashlane")
.setSaveEntries(
@@ -258,11 +254,11 @@ class CredentialManagerRepo(
context,
"key1", "subkey-4", "elisa.work@dashlane.com",
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(
newRemoteEntry("key4", "subkey-1")
GetTestUtils.newRemoteCredentialEntry(context, "key4", "subkey-1")
).build(),
GetCredentialProviderData.Builder("com.dashlane")
.setCredentialEntries(
@@ -333,10 +329,12 @@ class CredentialManagerRepo(
),
)
).setAuthenticationEntries(
listOf(GetTestUtils.newAuthenticationEntry(
context, "key2", "subkey-1", "foo@email.com",
AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT
))
listOf(
GetTestUtils.newAuthenticationEntry(
context, "key2", "subkey-1", "foo@email.com",
AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT,
)
)
).setActionChips(
listOf(
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 {
val request = CreatePublicKeyCredentialRequest(
"{\"extensions\": {\n" +

View File

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

View File

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

View File

@@ -29,6 +29,8 @@ import android.provider.Settings
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.PasswordCredentialEntry
import androidx.credentials.provider.PublicKeyCredentialEntry
import androidx.credentials.provider.RemoteCreateEntry
import androidx.credentials.provider.RemoteCredentialEntry
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(
context: Context,
key: String,
@@ -203,5 +220,20 @@ class CreateTestUtils {
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
private fun Scrim(
internal fun Scrim(
color: Color,
onDismiss: () -> Unit,
visible: Boolean

View File

@@ -16,6 +16,7 @@
package com.android.credentialmanager.common.ui
import androidx.compose.foundation.layout.padding
import com.android.credentialmanager.R
import androidx.compose.material.Icon
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.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
@@ -32,17 +32,22 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
@Composable
fun ActionButton(text: String, onClick: () -> Unit) {
TextButton(
modifier = Modifier.padding(vertical = 4.dp),
onClick = onClick,
colors = ButtonDefaults.textButtonColors(
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
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
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.CardDefaults
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.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
* background color with surface corlor.
* Container card for the whole sheet.
*
* Surface 1 color. No vertical padding. 24dp horizontal padding. 24dp bottom padding. 24dp top
* padding if [topAppBar] is not present, and none otherwise.
*/
@Composable
fun ContainerCard(
fun SheetContainerCard(
topAppBar: (@Composable () -> Unit)? = null,
modifier: Modifier = Modifier,
shape: Shape = CardDefaults.shape,
border: BorderStroke? = null,
content: @Composable ColumnScope.() -> Unit,
) {
Card(
modifier = modifier.fillMaxWidth(),
shape = shape,
border = border,
modifier = modifier.fillMaxWidth().wrapContentHeight(),
border = null,
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,
)

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
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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
fun ConfirmButton(text: String, onClick: () -> Unit) {
FilledTonalButton(
modifier = Modifier.padding(vertical = 4.dp),
onClick = onClick,
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
containerColor = MaterialTheme.colorScheme.primary,
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
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.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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.SuggestionChipDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
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.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.Shapes
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Entry(
onClick: () -> Unit,
label: @Composable () -> Unit,
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(
modifier = modifier.fillMaxWidth(),
modifier = modifier.fillMaxWidth().wrapContentHeight(),
onClick = onClick,
shape = EntryShape.FullSmallRoundedCorner,
label = label,
icon = icon,
label = {
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,
colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
ElevationTokens.Level3
),
// TODO: remove?
labelColor = 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
fun TransparentBackgroundEntry(
fun ActionEntry(
onClick: () -> Unit,
label: @Composable () -> Unit,
modifier: Modifier = Modifier,
icon: @Composable (() -> Unit)? = null,
entryHeadlineText: String,
entrySecondLineText: String? = null,
iconImageBitmap: ImageBitmap,
) {
SuggestionChip(
modifier = modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
onClick = onClick,
label = label,
icon = icon,
shape = Shapes.large,
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,
colors = SuggestionChipDefaults.suggestionChipColors(
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
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
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
fun TextOnSurface(
text: String,
modifier: Modifier = Modifier,
textAlign: TextAlign? = null,
style: TextStyle,
) {
TextInternal(
fun HeadlineText(text: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.wrapContentHeight(),
text = text,
color = MaterialTheme.colorScheme.onSurface,
modifier = modifier,
textAlign = textAlign,
style = style,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall,
)
}
/**
* Body-medium typography; on-surface-variant color.
*/
@Composable
fun TextSecondary(
text: String,
modifier: Modifier = Modifier,
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(
fun BodyMediumText(text: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.wrapContentHeight(),
text = text,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = modifier,
textAlign = textAlign,
style = style,
style = MaterialTheme.typography.bodyMedium,
)
}
/**
* Body-small typography; on-surface-variant color.
*/
@Composable
private fun TextInternal(
text: String,
color: Color,
modifier: Modifier,
textAlign: TextAlign?,
style: TextStyle,
) {
Text(text = text, color = color, modifier = modifier, textAlign = textAlign, style = style)
fun BodySmallText(text: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.wrapContentHeight(),
text = text,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
)
}
/**
* 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.result.ActivityResult
import androidx.activity.result.IntentSenderRequest
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.lazy.LazyColumn
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.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.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
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.ProviderActivityState
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.CredentialContainerCard
import com.android.credentialmanager.common.ui.CtaButtonRow
import com.android.credentialmanager.common.ui.Entry
import com.android.credentialmanager.common.ui.ModalBottomSheet
import com.android.credentialmanager.common.ui.TextOnSurface
import com.android.credentialmanager.common.ui.TextSecondary
import com.android.credentialmanager.common.ui.TextOnSurfaceVariant
import com.android.credentialmanager.common.ui.ContainerCard
import com.android.credentialmanager.common.ui.TransparentBackgroundEntry
import com.android.credentialmanager.ui.theme.EntryShape
import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
import com.android.credentialmanager.common.ui.MoreOptionTopAppBar
import com.android.credentialmanager.common.ui.SheetContainerCard
import com.android.credentialmanager.common.ui.SnackbarActionText
import com.android.credentialmanager.common.ui.HeadlineText
import com.android.credentialmanager.common.ui.CredentialListSectionHeader
import com.android.credentialmanager.common.ui.Snackbar
@Composable
fun GetCredentialScreen(
@@ -154,123 +138,100 @@ fun PrimarySelectionCard(
val sortedUserNameToCredentialEntryList =
providerDisplayInfo.sortedUserNameToCredentialEntryList
val authenticationEntryList = providerDisplayInfo.authenticationEntryList
ContainerCard() {
Column() {
TextOnSurface(
modifier = Modifier.padding(all = 24.dp),
textAlign = TextAlign.Center,
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)
SheetContainerCard {
HeadlineText(
text = stringResource(
if (sortedUserNameToCredentialEntryList
.size == 1 && authenticationEntryList.isEmpty()
) {
// Show max 4 entries in this primary page
if (usernameForCredentialSize + authenticationEntrySize <= 4) {
items(sortedUserNameToCredentialEntryList) {
CredentialEntryRow(
credentialEntryInfo = it.sortedCredentialEntryList.first(),
onEntrySelected = onEntrySelected,
)
}
items(authenticationEntryList) {
AuthenticationEntryRow(
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,
)
}
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
),
)
Divider(thickness = 24.dp, color = Color.Transparent)
CredentialContainerCard {
val usernameForCredentialSize = sortedUserNameToCredentialEntryList.size
val authenticationEntrySize = authenticationEntryList.size
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
// Show max 4 entries in this primary page
if (usernameForCredentialSize + authenticationEntrySize <= 4) {
items(sortedUserNameToCredentialEntryList) {
CredentialEntryRow(
credentialEntryInfo = it.sortedCredentialEntryList.first(),
onEntrySelected = onEntrySelected,
)
}
items(authenticationEntryList) {
AuthenticationEntryRow(
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,
color = Color.Transparent
)
var totalEntriesCount = sortedUserNameToCredentialEntryList
.flatMap { it.sortedCredentialEntryList }.size + authenticationEntryList
.size + providerInfoList.flatMap { it.actionEntryList }.size
if (providerDisplayInfo.remoteEntry != null) totalEntriesCount += 1
// Row horizontalArrangement differs on only one actionButton(should place on most
// left)/only one confirmButton(should place on most right)/two buttons exist the same
// time(should be one on the left, one on the right)
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) {
}
Divider(thickness = 24.dp, color = Color.Transparent)
var totalEntriesCount = sortedUserNameToCredentialEntryList
.flatMap { it.sortedCredentialEntryList }.size + authenticationEntryList
.size + providerInfoList.flatMap { it.actionEntryList }.size
if (providerDisplayInfo.remoteEntry != null) totalEntriesCount += 1
// Row horizontalArrangement differs on only one actionButton(should place on most
// left)/only one confirmButton(should place on most right)/two buttons exist the same
// time(should be one on the left, one on the right)
CtaButtonRow(
leftButton = if (totalEntriesCount > 1) {
{
ActionButton(
stringResource(R.string.get_dialog_use_saved_passkey_for),
onMoreOptionSelected
)
}
// Only one sign-in options exist
if (activeEntry != null) {
} else null,
rightButton = if (activeEntry != null) { // Only one sign-in options exist
{
ConfirmButton(
stringResource(R.string.string_continue),
onClick = onConfirm
)
}
}
Divider(
thickness = 18.dp,
color = Color.Transparent,
modifier = Modifier.padding(bottom = 16.dp)
)
}
} else null,
)
}
}
/** Draws the secondary credential selection page, where all sign-in options are listed. */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AllSignInOptionCard(
providerInfoList: List<ProviderInfo>,
@@ -283,87 +244,53 @@ fun AllSignInOptionCard(
val sortedUserNameToCredentialEntryList =
providerDisplayInfo.sortedUserNameToCredentialEntryList
val authenticationEntryList = providerDisplayInfo.authenticationEntryList
ContainerCard() {
Column() {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
),
title = {
TextOnSurface(
text = stringResource(R.string.get_dialog_title_sign_in_options),
style = MaterialTheme.typography.titleMedium
SheetContainerCard(topAppBar = {
MoreOptionTopAppBar(
text = stringResource(R.string.get_dialog_title_sign_in_options),
onNavigationIconClicked = if (isNoAccount) onCancel else onBackButtonClicked,
)
}) {
LazyColumn {
// For username
items(sortedUserNameToCredentialEntryList) { item ->
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
}
TextSecondary(
text = stringResource(R.string.get_dialog_heading_manage_sign_ins),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 8.dp)
CredentialListSectionHeader(
text = stringResource(R.string.get_dialog_heading_manage_sign_ins)
)
// TODO: tweak padding.
ContainerCard(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.medium,
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
CredentialContainerCard {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
actionChips.forEach {
ActionEntryRow(it, onEntrySelected)
}
@@ -404,38 +325,20 @@ fun RemoteEntryCard(
remoteEntry: RemoteEntryInfo,
onEntrySelected: (BaseEntry) -> Unit,
) {
TextSecondary(
text = stringResource(R.string.get_dialog_heading_from_another_device),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 8.dp)
CredentialListSectionHeader(
text = stringResource(R.string.get_dialog_heading_from_another_device)
)
ContainerCard(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.medium,
) {
CredentialContainerCard {
Column(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Entry(
onClick = { onEntrySelected(remoteEntry) },
icon = {
Icon(
painter = painterResource(R.drawable.ic_other_devices),
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)
)
}
iconPainter = painterResource(R.drawable.ic_other_devices),
entryHeadlineText = stringResource(
R.string.get_dialog_option_headline_use_a_different_device
),
)
}
}
@@ -446,15 +349,10 @@ fun LockedCredentials(
authenticationEntryList: List<AuthenticationEntryInfo>,
onEntrySelected: (BaseEntry) -> Unit,
) {
TextSecondary(
text = stringResource(R.string.get_dialog_heading_locked_password_managers),
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(vertical = 8.dp)
CredentialListSectionHeader(
text = stringResource(R.string.get_dialog_heading_locked_password_managers)
)
ContainerCard(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.medium,
) {
CredentialContainerCard {
Column(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
verticalArrangement = Arrangement.spacedBy(2.dp),
@@ -471,17 +369,12 @@ fun PerUserNameCredentials(
perUserNameCredentialEntryList: PerUserNameCredentialEntryList,
onEntrySelected: (BaseEntry) -> Unit,
) {
TextSecondary(
CredentialListSectionHeader(
text = stringResource(
R.string.get_dialog_heading_for_username, perUserNameCredentialEntryList.userName
),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 8.dp)
)
)
ContainerCard(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.medium,
) {
CredentialContainerCard {
Column(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
verticalArrangement = Arrangement.spacedBy(2.dp),
@@ -500,53 +393,28 @@ fun CredentialEntryRow(
) {
Entry(
onClick = { onEntrySelected(credentialEntryInfo) },
icon = {
if (credentialEntryInfo.icon != null) {
Image(
modifier = Modifier.padding(start = 10.dp).size(32.dp),
bitmap = credentialEntryInfo.icon.toBitmap().asImageBitmap(),
contentDescription = null,
)
} else {
Icon(
modifier = Modifier.padding(start = 10.dp).size(32.dp),
painter = painterResource(R.drawable.ic_other_sign_in),
contentDescription = null,
tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant
)
}
iconImageBitmap = credentialEntryInfo.icon?.toBitmap()?.asImageBitmap(),
// Fall back to iconPainter if iconImageBitmap isn't available
iconPainter =
if (credentialEntryInfo.icon == null) painterResource(R.drawable.ic_other_sign_in)
else null,
entryHeadlineText = credentialEntryInfo.userName,
entrySecondLineText = 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
},
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
fun AuthenticationEntryRow(
authenticationEntryInfo: AuthenticationEntryInfo,
@@ -554,144 +422,66 @@ fun AuthenticationEntryRow(
) {
Entry(
onClick = { onEntrySelected(authenticationEntryInfo) },
icon = {
Image(
modifier = Modifier.padding(start = 10.dp).size(32.dp),
bitmap = authenticationEntryInfo.icon.toBitmap().asImageBitmap(),
contentDescription = null
)
},
label = {
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),
)
}
}
}
iconImageBitmap = authenticationEntryInfo.icon.toBitmap().asImageBitmap(),
entryHeadlineText = authenticationEntryInfo.title,
entrySecondLineText = 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
),
isLockedAuthEntry = !authenticationEntryInfo.isUnlockedAndEmpty,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ActionEntryRow(
actionEntryInfo: ActionEntryInfo,
onEntrySelected: (BaseEntry) -> Unit,
) {
TransparentBackgroundEntry(
icon = {
Image(
modifier = Modifier.padding(start = 10.dp).size(24.dp),
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),
)
}
}
},
ActionEntry(
iconImageBitmap = actionEntryInfo.icon.toBitmap().asImageBitmap(),
entryHeadlineText = actionEntryInfo.title,
entrySecondLineText = actionEntryInfo.subTitle,
onClick = { onEntrySelected(actionEntryInfo) },
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RemoteCredentialSnackBarScreen(
onClick: (Boolean) -> Unit,
onCancel: () -> Unit,
) {
// TODO: Change the height, width and position according to the design
Snackbar(
modifier = Modifier.padding(horizontal = 40.dp).padding(top = 700.dp),
shape = EntryShape.FullMediumRoundedCorner,
containerColor = LocalAndroidColorScheme.current.colorBackground,
contentColor = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
action = {
TextButton(
modifier = Modifier.padding(top = 12.dp, bottom = 12.dp, start = 16.dp),
onClick = { onClick(true) },
) {
Text(text = stringResource(R.string.snackbar_action))
}
},
dismissAction = {
IconButton(onClick = onCancel) {
Icon(
Icons.Filled.Close,
contentDescription = stringResource(
R.string.accessibility_close_button
),
tint = LocalAndroidColorScheme.current.colorAccentTertiary
SnackbarActionText(
text = stringResource(R.string.snackbar_action),
Modifier.padding(vertical = 6.dp)
)
}
},
) {
Text(text = stringResource(R.string.get_dialog_use_saved_passkey_for))
}
onDismiss = onCancel,
contentText = stringResource(R.string.get_dialog_use_saved_passkey_for),
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EmptyAuthEntrySnackBarScreen(
authenticationEntryList: List<AuthenticationEntryInfo>,
onCancel: () -> Unit,
onLastLockedAuthEntryNotFound: () -> Unit,
) {
val lastLocked = authenticationEntryList.firstOrNull({it.isLastUnlocked})
val lastLocked = authenticationEntryList.firstOrNull({ it.isLastUnlocked })
if (lastLocked == null) {
onLastLockedAuthEntryNotFound()
return
}
// TODO: Change the height, width and position according to the design
Snackbar(
modifier = Modifier.padding(horizontal = 40.dp).padding(top = 700.dp),
shape = EntryShape.FullMediumRoundedCorner,
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))
}
onDismiss = onCancel,
contentText = stringResource(R.string.no_sign_in_info_in, lastLocked.providerDisplayName),
)
}

View File

@@ -96,6 +96,7 @@ class AuthenticationEntryInfo(
pendingIntent: PendingIntent?,
fillInIntent: Intent?,
val title: String,
val providerDisplayName: String,
val icon: Drawable,
// 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.

View File

@@ -22,12 +22,14 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import com.android.internal.R
/** File copied from PlatformComposeCore. */
/** CompositionLocal used to pass [AndroidColorScheme] down the tree. */
val LocalAndroidColorScheme =
staticCompositionLocalOf<AndroidColorScheme> {
throw IllegalStateException(
"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.
*/
class AndroidColorScheme internal constructor(context: Context) {
val colorPrimary = getColor(context, R.attr.colorPrimary)
val colorPrimaryDark = getColor(context, R.attr.colorPrimaryDark)
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 colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
private fun getColor(context: Context, attr: Int): Color {
val ta = context.obtainStyledAttributes(intArrayOf(attr))
@ColorInt val color = ta.getColor(0, 0)
ta.recycle()
return Color(color)
companion object {
fun getColor(context: Context, attr: Int): Color {
val ta = context.obtainStyledAttributes(intArrayOf(attr))
@ColorInt val color = ta.getColor(0, 0)
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
import android.annotation.AttrRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
val Grey100 = Color(0xFFF1F3F4)
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
val lightColorAccentSecondary = Color(0xFFC2E7FF)
val lightBackgroundColor = Color(0xFFF0F0F0)
val lightSurface1 = Color(0xFF6991D6)
val textColorSecondary = Color(0xFF40484B)
val textColorPrimary = Color(0xFF191C1D)
/** Read the [Color] from the given [attribute]. */
@Composable
@ReadOnlyComposable
fun colorAttr(@AttrRes attribute: Int): Color {
return AndroidColorScheme.getColor(LocalContext.current, attribute)
}

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,
)
}