Merge "Updated the CredentialManager UI style." into udc-dev
This commit is contained in:
@@ -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] -->
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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" +
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -451,7 +451,7 @@ private fun Modifier.bottomSheetSwipeable(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Scrim(
|
||||
internal fun Scrim(
|
||||
color: Color,
|
||||
onDismiss: () -> Unit,
|
||||
visible: Boolean
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user