Add single provider and single account screens
Bug: 301206470
Test: N/A - feature not fully implemented yet
Change-Id: I25f54e2bcb8228b46a8ca7a89f63753c639a26ef
diff --git a/packages/CredentialManager/horologist/Android.bp b/packages/CredentialManager/horologist/Android.bp
index bb324bb..bb255bd 100644
--- a/packages/CredentialManager/horologist/Android.bp
+++ b/packages/CredentialManager/horologist/Android.bp
@@ -16,6 +16,7 @@
"androidx.compose.foundation_foundation",
"androidx.compose.runtime_runtime",
"androidx.compose.ui_ui",
+ "androidx.compose.ui_ui-tooling",
"androidx.navigation_navigation-compose",
"androidx.lifecycle_lifecycle-extensions",
"androidx.lifecycle_lifecycle-runtime-ktx",
diff --git a/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/layout/BelowTimeTextPreview.kt b/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/layout/BelowTimeTextPreview.kt
new file mode 100644
index 0000000..e6025fc
--- /dev/null
+++ b/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/layout/BelowTimeTextPreview.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 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
+ *
+ * https://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.google.android.horologist.compose.layout
+
+import androidx.compose.runtime.Composable
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+
+@OptIn(ExperimentalHorologistApi::class)
+@Composable
+public fun belowTimeTextPreview(): ScalingLazyColumnState {
+ return ScalingLazyColumnDefaults.belowTimeText().create()
+}
diff --git a/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/material/Button.kt b/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/material/Button.kt
new file mode 100644
index 0000000..57e0c10
--- /dev/null
+++ b/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/material/Button.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 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
+ *
+ * https://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.google.android.horologist.compose.material
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.Dp
+import androidx.wear.compose.material.Button
+import androidx.wear.compose.material.ButtonColors
+import androidx.wear.compose.material.ButtonDefaults
+import androidx.wear.compose.material.ButtonDefaults.DefaultButtonSize
+import androidx.wear.compose.material.ButtonDefaults.DefaultIconSize
+import androidx.wear.compose.material.ButtonDefaults.LargeButtonSize
+import androidx.wear.compose.material.ButtonDefaults.LargeIconSize
+import androidx.wear.compose.material.ButtonDefaults.SmallButtonSize
+import androidx.wear.compose.material.ButtonDefaults.SmallIconSize
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+
+/**
+ * This component is an alternative to [Button], providing the following:
+ * - a convenient way of providing an icon and choosing its size from a range of sizes recommended
+ * by the Wear guidelines;
+ */
+@ExperimentalHorologistApi
+@Composable
+public fun Button(
+ imageVector: ImageVector,
+ contentDescription: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ colors: ButtonColors = ButtonDefaults.primaryButtonColors(),
+ buttonSize: ButtonSize = ButtonSize.Default,
+ iconRtlMode: IconRtlMode = IconRtlMode.Default,
+ enabled: Boolean = true,
+) {
+ Button(
+ icon = imageVector,
+ contentDescription = contentDescription,
+ onClick = onClick,
+ modifier = modifier,
+ colors = colors,
+ buttonSize = buttonSize,
+ iconRtlMode = iconRtlMode,
+ enabled = enabled,
+ )
+}
+
+/**
+ * This component is an alternative to [Button], providing the following:
+ * - a convenient way of providing an icon and choosing its size from a range of sizes recommended
+ * by the Wear guidelines;
+ */
+@ExperimentalHorologistApi
+@Composable
+public fun Button(
+ @DrawableRes id: Int,
+ contentDescription: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ colors: ButtonColors = ButtonDefaults.primaryButtonColors(),
+ buttonSize: ButtonSize = ButtonSize.Default,
+ iconRtlMode: IconRtlMode = IconRtlMode.Default,
+ enabled: Boolean = true,
+) {
+ Button(
+ icon = id,
+ contentDescription = contentDescription,
+ onClick = onClick,
+ modifier = modifier,
+ colors = colors,
+ buttonSize = buttonSize,
+ iconRtlMode = iconRtlMode,
+ enabled = enabled,
+ )
+}
+
+@OptIn(ExperimentalHorologistApi::class)
+@Composable
+internal fun Button(
+ icon: Any,
+ contentDescription: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ colors: ButtonColors = ButtonDefaults.primaryButtonColors(),
+ buttonSize: ButtonSize = ButtonSize.Default,
+ iconRtlMode: IconRtlMode = IconRtlMode.Default,
+ enabled: Boolean = true,
+) {
+ Button(
+ onClick = onClick,
+ modifier = modifier.size(buttonSize.tapTargetSize),
+ enabled = enabled,
+ colors = colors,
+ ) {
+ val iconModifier = Modifier
+ .size(buttonSize.iconSize)
+ .align(Alignment.Center)
+
+ Icon(
+ icon = icon,
+ contentDescription = contentDescription,
+ modifier = iconModifier,
+ rtlMode = iconRtlMode,
+ )
+ }
+}
+
+@ExperimentalHorologistApi
+public sealed class ButtonSize(
+ public val iconSize: Dp,
+ public val tapTargetSize: Dp,
+) {
+ public object Default :
+ ButtonSize(iconSize = DefaultIconSize, tapTargetSize = DefaultButtonSize)
+
+ public object Large : ButtonSize(iconSize = LargeIconSize, tapTargetSize = LargeButtonSize)
+ public object Small : ButtonSize(iconSize = SmallIconSize, tapTargetSize = SmallButtonSize)
+
+ /**
+ * Custom sizes should follow the [accessibility principles and guidance for touch targets](https://developer.android.com/training/wearables/accessibility#set-minimum).
+ */
+ public data class Custom(val customIconSize: Dp, val customTapTargetSize: Dp) :
+ ButtonSize(iconSize = customIconSize, tapTargetSize = customTapTargetSize)
+}
+
diff --git a/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/material/Icon.kt b/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/material/Icon.kt
new file mode 100644
index 0000000..74e54c0
--- /dev/null
+++ b/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/material/Icon.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 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
+ *
+ * https://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.google.android.horologist.compose.material
+
+import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.LocalContentAlpha
+import androidx.wear.compose.material.LocalContentColor
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+
+/**
+ * This component is an alternative to [Icon], providing the following:
+ * - a convenient way of setting the icon to be mirrored in RTL mode;
+ */
+@ExperimentalHorologistApi
+@Composable
+public fun Icon(
+ imageVector: ImageVector,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
+ rtlMode: IconRtlMode = IconRtlMode.Default,
+) {
+ val shouldMirror =
+ rtlMode == IconRtlMode.Mirrored && LocalLayoutDirection.current == LayoutDirection.Rtl
+ Icon(
+ modifier = modifier.scale(
+ scaleX = if (shouldMirror) -1f else 1f,
+ scaleY = 1f,
+ ),
+ imageVector = imageVector,
+ contentDescription = contentDescription,
+ tint = tint,
+ )
+}
+
+/**
+ * This component is an alternative to [Icon], providing the following:
+ * - a convenient way of setting the icon to be mirrored in RTL mode;
+ */
+@ExperimentalHorologistApi
+@Composable
+public fun Icon(
+ @DrawableRes id: Int,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
+ rtlMode: IconRtlMode = IconRtlMode.Default,
+) {
+ val shouldMirror =
+ rtlMode == IconRtlMode.Mirrored && LocalLayoutDirection.current == LayoutDirection.Rtl
+
+ Icon(
+ painter = painterResource(id = id),
+ contentDescription = contentDescription,
+ modifier = modifier.scale(
+ scaleX = if (shouldMirror) -1f else 1f,
+ scaleY = 1f,
+ ),
+ tint = tint,
+ )
+}
+
+@OptIn(ExperimentalHorologistApi::class)
+@Composable
+internal fun Icon(
+ icon: Any,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
+ rtlMode: IconRtlMode = IconRtlMode.Default,
+) {
+ val shouldMirror =
+ rtlMode == IconRtlMode.Mirrored && LocalLayoutDirection.current == LayoutDirection.Rtl
+
+ val iconModifier = modifier.scale(
+ scaleX = if (shouldMirror) -1f else 1f,
+ scaleY = 1f,
+ )
+ when (icon) {
+ is ImageVector -> {
+ Icon(
+ imageVector = icon,
+ modifier = iconModifier,
+ contentDescription = contentDescription,
+ tint = tint,
+ )
+ }
+
+ is Int -> {
+ Icon(
+ painter = painterResource(id = icon),
+ contentDescription = contentDescription,
+ modifier = iconModifier,
+ tint = tint,
+ )
+ }
+
+ else -> throw IllegalArgumentException("Type not supported.")
+ }
+}
+
+@ExperimentalHorologistApi
+public enum class IconRtlMode {
+ Default,
+ Mirrored,
+}
diff --git a/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/material/util/A11y.kt b/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/material/util/A11y.kt
new file mode 100644
index 0000000..39de2e1
--- /dev/null
+++ b/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/material/util/A11y.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 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
+ *
+ * https://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.google.android.horologist.compose.material.util
+
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+
+/**
+ * Make explicit that a conscious decision was made to mark an element as decorative, so it does not
+ * have associated actions or state.
+ *
+ * https://developer.android.com/jetpack/compose/accessibility#describe-visual
+ */
+@ExperimentalHorologistApi
+public val DECORATIVE_ELEMENT_CONTENT_DESCRIPTION: String? = null
diff --git a/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/tools/WearPreview.kt b/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/tools/WearPreview.kt
new file mode 100644
index 0000000..0bfceee
--- /dev/null
+++ b/packages/CredentialManager/horologist/src/com/google/android/horologist/compose/tools/WearPreview.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 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
+ *
+ * https://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.google.android.horologist.compose.tools
+
+import androidx.compose.ui.tooling.preview.Preview
+
+@Preview(
+ backgroundColor = 0xff000000,
+ showBackground = true,
+)
+public annotation class WearPreview
diff --git a/packages/CredentialManager/shared/Android.bp b/packages/CredentialManager/shared/Android.bp
index ae4281e..38d98a9 100644
--- a/packages/CredentialManager/shared/Android.bp
+++ b/packages/CredentialManager/shared/Android.bp
@@ -14,5 +14,6 @@
static_libs: [
"androidx.core_core-ktx",
"androidx.credentials_credentials",
+ "guava",
],
}
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/IntentParser.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/IntentParser.kt
index 6627af5..43d8fb3 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/IntentParser.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/IntentParser.kt
@@ -17,11 +17,15 @@
package com.android.credentialmanager.ui
import android.content.Intent
+import android.credentials.ui.GetCredentialProviderData
import android.credentials.ui.RequestInfo
import com.android.credentialmanager.ui.ktx.cancelUiRequest
+import com.android.credentialmanager.ui.ktx.getCredentialProviderDataList
import com.android.credentialmanager.ui.ktx.requestInfo
import com.android.credentialmanager.ui.mapper.toCancel
import com.android.credentialmanager.ui.model.Request
+import com.google.common.collect.ImmutableList
+import com.google.common.collect.ImmutableMap
fun Intent.parse(): Request {
cancelUiRequest?.let {
@@ -32,9 +36,23 @@
RequestInfo.TYPE_CREATE -> {
Request.Create
}
+
RequestInfo.TYPE_GET -> {
- Request.Get
+ Request.Get(
+ providers = ImmutableMap.copyOf(
+ getCredentialProviderDataList.associateBy { it.providerFlattenedComponentName }
+ ),
+ entries = ImmutableList.copyOf(
+ getCredentialProviderDataList.map { providerData ->
+ check(providerData is GetCredentialProviderData) {
+ "Invalid provider data type for GetCredentialRequest"
+ }
+ providerData
+ }.flatMap { it.credentialEntries }
+ )
+ )
}
+
else -> {
throw IllegalStateException("Unrecognized request type: ${requestInfo?.type}")
}
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/factory/CredentialEntryFactory.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/factory/CredentialEntryFactory.kt
new file mode 100644
index 0000000..f01fded
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/factory/CredentialEntryFactory.kt
@@ -0,0 +1,25 @@
+package com.android.credentialmanager.ui.factory
+
+import android.app.slice.Slice
+import android.credentials.Credential
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.provider.CredentialEntry
+import androidx.credentials.provider.CustomCredentialEntry
+import androidx.credentials.provider.PasswordCredentialEntry
+import androidx.credentials.provider.PublicKeyCredentialEntry
+
+fun fromSlice(slice: Slice): CredentialEntry? =
+ try {
+ when (slice.spec?.type) {
+ Credential.TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntry.fromSlice(slice)!!
+
+ PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
+ PublicKeyCredentialEntry.fromSlice(slice)!!
+
+ else -> CustomCredentialEntry.fromSlice(slice)!!
+ }
+ } catch (e: Exception) {
+ // Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
+ // password / passkey parsing attempt.
+ CustomCredentialEntry.fromSlice(slice)
+ }
\ No newline at end of file
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/ktx/IntentKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/ktx/IntentKtx.kt
index a646851..b9895a0 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/ktx/IntentKtx.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/ktx/IntentKtx.kt
@@ -18,6 +18,9 @@
import android.content.Intent
import android.credentials.ui.CancelUiRequest
+import android.credentials.ui.CreateCredentialProviderData
+import android.credentials.ui.GetCredentialProviderData
+import android.credentials.ui.ProviderData
import android.credentials.ui.RequestInfo
val Intent.cancelUiRequest: CancelUiRequest?
@@ -30,4 +33,16 @@
get() = this.extras?.getParcelable(
RequestInfo.EXTRA_REQUEST_INFO,
RequestInfo::class.java
- )
\ No newline at end of file
+ )
+
+val Intent.getCredentialProviderDataList: List<ProviderData>
+ get() = this.extras?.getParcelableArrayList(
+ ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
+ GetCredentialProviderData::class.java
+ ) ?: emptyList()
+
+val Intent.createCredentialProviderDataList: List<ProviderData>
+ get() = this.extras?.getParcelableArrayList(
+ ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
+ CreateCredentialProviderData::class.java
+ ) ?: emptyList()
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/model/Request.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/model/Request.kt
index 3d835be..cffa2b2 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/model/Request.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ui/model/Request.kt
@@ -16,6 +16,11 @@
package com.android.credentialmanager.ui.model
+import android.credentials.ui.Entry
+import android.credentials.ui.ProviderData
+import com.google.common.collect.ImmutableList
+import com.google.common.collect.ImmutableMap
+
/**
* Represents the request made by the CredentialManager API.
*/
@@ -25,7 +30,10 @@
val appPackageName: String?
) : Request()
- data object Get : Request()
+ data class Get(
+ val providers: ImmutableMap<String, ProviderData>,
+ val entries: ImmutableList<Entry>,
+ ) : Request()
data object Create : Request()
}
diff --git a/packages/CredentialManager/wear/Android.bp b/packages/CredentialManager/wear/Android.bp
index 36340fa..c0dff16 100644
--- a/packages/CredentialManager/wear/Android.bp
+++ b/packages/CredentialManager/wear/Android.bp
@@ -32,6 +32,7 @@
"androidx.compose.material_material-icons-extended",
"androidx.compose.runtime_runtime",
"androidx.compose.ui_ui",
+ "androidx.compose.ui_ui-tooling",
"androidx.core_core-ktx",
"androidx.lifecycle_lifecycle-extensions",
"androidx.lifecycle_lifecycle-livedata",
diff --git a/packages/CredentialManager/wear/res/drawable/passkey_icon.xml b/packages/CredentialManager/wear/res/drawable/passkey_icon.xml
new file mode 100644
index 0000000..be366bf
--- /dev/null
+++ b/packages/CredentialManager/wear/res/drawable/passkey_icon.xml
@@ -0,0 +1,21 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M23,10.5H17V13.5H23V10.5Z"
+ android:fillColor="#188038"/>
+ <path
+ android:pathData="M6.5,17.5C3.5,17.5 1,15 1,12C1,9 3.5,6.5 6.5,6.5C9.5,6.5 12,9 12,12C12,15 9.5,17.5 6.5,17.5ZM6.5,9.5C5.1,9.5 4,10.6 4,12C4,13.4 5.1,14.5 6.5,14.5C7.9,14.5 9,13.4 9,12C9,10.6 7.9,9.5 6.5,9.5Z"
+ android:fillColor="#4285F4"/>
+ <path
+ android:pathData="M21,13.5H19H17V16.5H19V15.5C19,14.9 19.4,14.5 20,14.5C20.6,14.5 21,14.9 21,15.5V16.5H23V13.5H21Z"
+ android:fillColor="#34A853"/>
+ <path
+ android:pathData="M11.8,10.5H8.5C8.8,10.9 9,11.4 9,12C9,12.6 8.8,13.1 8.5,13.5H11.8C11.9,13 12,12.5 12,12C12,11.5 11.9,11 11.8,10.5Z"
+ android:fillColor="#EA4335"/>
+ <path
+ android:pathData="M17,10.5H11.8C11.9,11 12,11.5 12,12C12,12.5 11.9,13 11.8,13.5H17V10.5Z"
+ android:fillColor="#FBBC04"/>
+</vector>
diff --git a/packages/CredentialManager/wear/res/values/strings.xml b/packages/CredentialManager/wear/res/values/strings.xml
index 10ea918..109644f 100644
--- a/packages/CredentialManager/wear/res/values/strings.xml
+++ b/packages/CredentialManager/wear/res/values/strings.xml
@@ -18,4 +18,14 @@
<!-- The name of this application. Credential Manager is a service that centralizes and provides
access to a user's credentials used to sign in to various apps. [CHAR LIMIT=80] -->
<string name="app_name">Credential Manager</string>
+ <!-- Title of a screen prompting if the user would like to use their saved passkey.
+ [CHAR LIMIT=80] -->
+ <string name="use_passkey_title">Use passkey?</string>
+ <!-- Title of a screen prompting if the user would like to use their saved password.
+ [CHAR LIMIT=80] -->
+ <string name="use_password_title">Use password?</string>
+ <!-- Content description for the cancel button of a screen. [CHAR LIMIT=NONE] -->
+ <string name="dialog_cancel_button_cd">Cancel</string>
+ <!-- Content description for the OK button of a screen. [CHAR LIMIT=NONE] -->
+ <string name="dialog_ok_button_cd">OK</string>
</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt
index 2c05755..a93fa81 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt
@@ -47,7 +47,7 @@
// to the user.
}
- CredentialSelectorUiState.Get -> {
+ is CredentialSelectorUiState.Get -> {
// TODO: b/301206470 - Implement get flow
setContent {
MaterialTheme {
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorViewModel.kt
index e46fcae..c61bb2e 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorViewModel.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorViewModel.kt
@@ -23,6 +23,9 @@
import androidx.lifecycle.viewModelScope
import com.android.credentialmanager.ui.ktx.appLabel
import com.android.credentialmanager.ui.ktx.requestInfo
+import com.android.credentialmanager.ui.mapper.toGet
+import com.android.credentialmanager.ui.model.PasskeyUiModel
+import com.android.credentialmanager.ui.model.PasswordUiModel
import com.android.credentialmanager.ui.model.Request
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -62,8 +65,8 @@
_uiState.value = CredentialSelectorUiState.Create
}
- Request.Get -> {
- _uiState.value = CredentialSelectorUiState.Get
+ is Request.Get -> {
+ _uiState.value = request.toGet()
}
}
}
@@ -103,9 +106,15 @@
}
sealed class CredentialSelectorUiState {
- object Idle : CredentialSelectorUiState()
- object Get : CredentialSelectorUiState()
- object Create : CredentialSelectorUiState()
+ data object Idle : CredentialSelectorUiState()
+ sealed class Get : CredentialSelectorUiState() {
+ data class SingleProviderSinglePasskey(val passkeyUiModel: PasskeyUiModel) : Get()
+ data class SingleProviderSinglePassword(val passwordUiModel: PasswordUiModel) : Get()
+
+ // TODO: b/301206470 add the remaining states
+ }
+
+ data object Create : CredentialSelectorUiState()
data class Cancel(val appName: String) : CredentialSelectorUiState()
- object Finish : CredentialSelectorUiState()
+ data object Finish : CredentialSelectorUiState()
}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/AccountRow.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/AccountRow.kt
new file mode 100644
index 0000000..c20ee0c
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/AccountRow.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 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
+ *
+ * https://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.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+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.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Text
+import com.google.android.horologist.compose.tools.WearPreview
+
+@Composable
+fun AccountRow(
+ name: String,
+ email: String,
+ modifier: Modifier = Modifier,
+) {
+ Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text = name,
+ color = Color(0xFFE6FF7B),
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ style = MaterialTheme.typography.title2
+ )
+ Text(
+ text = email,
+ modifier = Modifier.padding(top = 7.dp),
+ color = Color(0xFFCAC5BC),
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ style = MaterialTheme.typography.body1,
+ )
+ }
+}
+
+@WearPreview
+@Composable
+fun AccountRowPreview() {
+ AccountRow(
+ name = "Elisa Beckett",
+ email = "beckett_bakery@gmail.com",
+ )
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/DialogButtonsRow.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/DialogButtonsRow.kt
new file mode 100644
index 0000000..5cb3c15
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/DialogButtonsRow.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 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
+ *
+ * https://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.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.ButtonDefaults
+import com.google.android.horologist.compose.material.Button
+import com.google.android.horologist.compose.tools.WearPreview
+import com.android.credentialmanager.R
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+
+@OptIn(ExperimentalHorologistApi::class)
+@Composable
+fun DialogButtonsRow(
+ onCancelClick: () -> Unit,
+ onOKClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ cancelButtonIcon: ImageVector = Icons.Default.Close,
+ okButtonIcon: ImageVector = Icons.Default.Check,
+ cancelButtonContentDescription: String = stringResource(R.string.dialog_cancel_button_cd),
+ okButtonContentDescription: String = stringResource(R.string.dialog_ok_button_cd),
+) {
+ Row(
+ modifier = modifier,
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ Button(
+ imageVector = cancelButtonIcon,
+ contentDescription = cancelButtonContentDescription,
+ onClick = onCancelClick,
+ colors = ButtonDefaults.secondaryButtonColors(),
+ )
+ Button(
+ imageVector = okButtonIcon,
+ contentDescription = okButtonContentDescription,
+ onClick = onOKClick,
+ modifier = Modifier.padding(start = 20.dp)
+ )
+ }
+}
+
+@WearPreview
+@Composable
+fun DialogButtonsRowPreview() {
+ DialogButtonsRow(onCancelClick = {}, onOKClick = {})
+}
+
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/PasswordRow.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/PasswordRow.kt
new file mode 100644
index 0000000..97900b7
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/PasswordRow.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 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
+ *
+ * https://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.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+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.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Text
+import com.google.android.horologist.compose.tools.WearPreview
+
+@Composable
+fun PasswordRow(
+ email: String,
+ modifier: Modifier = Modifier,
+) {
+ Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text = email,
+ color = Color(0xFFE6FF7B),
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ style = MaterialTheme.typography.title2
+ )
+ Text(
+ text = "••••••••••••••",
+ modifier = Modifier.padding(top = 7.dp),
+ color = Color(0xFFCAC5BC),
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ style = MaterialTheme.typography.body1,
+ )
+ }
+}
+
+@WearPreview
+@Composable
+fun PasswordRowPreview() {
+ PasswordRow(
+ email = "beckett_bakery@gmail.com",
+ )
+}
+
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt
new file mode 100644
index 0000000..956c56b
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 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
+ *
+ * https://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.components
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Text
+import com.android.credentialmanager.R
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.material.Icon
+import com.google.android.horologist.compose.material.util.DECORATIVE_ELEMENT_CONTENT_DESCRIPTION
+import com.google.android.horologist.compose.tools.WearPreview
+
+@OptIn(ExperimentalHorologistApi::class)
+@Composable
+fun SignInHeader(
+ @DrawableRes icon: Int,
+ title: String,
+ modifier: Modifier = Modifier,
+) {
+ SignInHeader(
+ iconContent = {
+ Icon(
+ id = icon,
+ contentDescription = DECORATIVE_ELEMENT_CONTENT_DESCRIPTION
+ )
+ },
+ title = title,
+ modifier = modifier,
+ )
+}
+
+@Composable
+fun SignInHeader(
+ iconContent: @Composable ColumnScope.() -> Unit,
+ title: String,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ iconContent()
+ Text(
+ text = title,
+ modifier = Modifier
+ .padding(top = 6.dp)
+ .padding(horizontal = 10.dp),
+ style = MaterialTheme.typography.title3
+ )
+ }
+}
+
+@WearPreview
+@Composable
+fun SignInHeaderPreview() {
+ SignInHeader(
+ icon = R.drawable.passkey_icon,
+ title = stringResource(R.string.use_passkey_title)
+ )
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mapper/CredentialSelectorUiStateGetMapper.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mapper/CredentialSelectorUiStateGetMapper.kt
new file mode 100644
index 0000000..1fe1e55
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mapper/CredentialSelectorUiStateGetMapper.kt
@@ -0,0 +1,51 @@
+package com.android.credentialmanager.ui.mapper
+
+import androidx.credentials.provider.CustomCredentialEntry
+import androidx.credentials.provider.PasswordCredentialEntry
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import com.android.credentialmanager.ui.CredentialSelectorUiState
+import com.android.credentialmanager.ui.factory.fromSlice
+import com.android.credentialmanager.ui.model.PasswordUiModel
+import com.android.credentialmanager.ui.model.Request
+
+fun Request.Get.toGet(): CredentialSelectorUiState.Get {
+ if (this.providers.isEmpty()) {
+ throw IllegalStateException("Invalid GetCredential request with empty list of providers.")
+ }
+
+ if (this.entries.isEmpty()) {
+ throw IllegalStateException("Invalid GetCredential request with empty list of entries.")
+ }
+
+ if (this.providers.size == 1) {
+ if (this.entries.size == 1) {
+ val slice = this.entries.first().slice
+ when (val credentialEntry = fromSlice(slice)) {
+ is PasswordCredentialEntry -> {
+ return CredentialSelectorUiState.Get.SingleProviderSinglePassword(
+ PasswordUiModel(credentialEntry.displayName.toString())
+ )
+ }
+
+ is PublicKeyCredentialEntry -> {
+ TODO("b/301206470 - to be implemented")
+ }
+
+ is CustomCredentialEntry -> {
+ TODO("b/301206470 - to be implemented")
+ }
+
+ else -> {
+ throw IllegalStateException(
+ "Encountered unrecognized credential entry (${slice.spec?.type}) for " +
+ "GetCredential request with single account"
+ )
+ }
+ }
+ } else {
+ TODO("b/301206470 - to be implemented")
+ }
+ } else {
+ TODO("b/301206470 - to be implemented")
+ }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/model/PasskeyUiModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/model/PasskeyUiModel.kt
new file mode 100644
index 0000000..a368de2
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/model/PasskeyUiModel.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.0N
+ *
+ * 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.model
+
+data class PasskeyUiModel(
+ val name: String,
+ val email: String,
+)
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/model/PasswordUiModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/model/PasswordUiModel.kt
new file mode 100644
index 0000000..514dca8
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/model/PasswordUiModel.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.0N
+ *
+ * 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.model
+
+data class PasswordUiModel(val name: String)
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SingleAccountScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SingleAccountScreen.kt
new file mode 100644
index 0000000..f344ad0
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SingleAccountScreen.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 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
+ *
+ * https://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(ExperimentalHorologistApi::class)
+
+package com.android.credentialmanager.ui.screens
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
+import com.android.credentialmanager.R
+import com.android.credentialmanager.ui.components.AccountRow
+import com.android.credentialmanager.ui.components.DialogButtonsRow
+import com.android.credentialmanager.ui.components.SignInHeader
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.layout.ScalingLazyColumn
+import com.google.android.horologist.compose.layout.ScalingLazyColumnState
+import com.google.android.horologist.compose.layout.belowTimeTextPreview
+import com.google.android.horologist.compose.tools.WearPreview
+
+@Composable
+fun SingleAccountScreen(
+ headerContent: @Composable () -> Unit,
+ accountContent: @Composable () -> Unit,
+ columnState: ScalingLazyColumnState,
+ modifier: Modifier = Modifier,
+ content: ScalingLazyListScope.() -> Unit,
+) {
+ ScalingLazyColumn(
+ columnState = columnState,
+ modifier = modifier.fillMaxSize(),
+ ) {
+ item { headerContent() }
+ item { accountContent() }
+ content()
+ }
+}
+
+@WearPreview
+@Composable
+fun SingleAccountScreenPreview() {
+ SingleAccountScreen(
+ headerContent = {
+ SignInHeader(
+ icon = R.drawable.passkey_icon,
+ title = stringResource(R.string.use_passkey_title),
+ )
+ },
+ accountContent = {
+ AccountRow(
+ name = "Elisa Beckett",
+ email = "beckett_bakery@gmail.com",
+ modifier = Modifier.padding(top = 10.dp)
+ )
+ },
+ columnState = belowTimeTextPreview(),
+ ) {
+ item {
+ DialogButtonsRow(
+ onCancelClick = {},
+ onOKClick = {},
+ modifier = Modifier.padding(top = 10.dp)
+ )
+ }
+ }
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasskeyScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasskeyScreen.kt
new file mode 100644
index 0000000..c8f871e
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasskeyScreen.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 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
+ *
+ * https://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(ExperimentalHorologistApi::class)
+
+package com.android.credentialmanager.ui.screens
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.android.credentialmanager.R
+import com.android.credentialmanager.ui.components.AccountRow
+import com.android.credentialmanager.ui.components.DialogButtonsRow
+import com.android.credentialmanager.ui.components.SignInHeader
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.layout.ScalingLazyColumnState
+import com.google.android.horologist.compose.layout.belowTimeTextPreview
+import com.google.android.horologist.compose.tools.WearPreview
+
+@Composable
+fun SinglePasskeyScreen(
+ name: String,
+ email: String,
+ onCancelClick: () -> Unit,
+ onOKClick: () -> Unit,
+ columnState: ScalingLazyColumnState,
+ modifier: Modifier = Modifier,
+) {
+ SingleAccountScreen(
+ headerContent = {
+ SignInHeader(
+ icon = R.drawable.passkey_icon,
+ title = stringResource(R.string.use_passkey_title),
+ )
+ },
+ accountContent = {
+ AccountRow(
+ name = name,
+ email = email,
+ modifier = Modifier.padding(top = 10.dp),
+ )
+ },
+ columnState = columnState,
+ modifier = modifier.padding(horizontal = 10.dp)
+ ) {
+ item {
+ DialogButtonsRow(
+ onCancelClick = onCancelClick,
+ onOKClick = onOKClick,
+ modifier = Modifier.padding(top = 10.dp)
+ )
+ }
+ }
+}
+
+@WearPreview
+@Composable
+fun SinglePasskeyScreenPreview() {
+ SinglePasskeyScreen(
+ name = "Elisa Beckett",
+ email = "beckett_bakery@gmail.com",
+ onCancelClick = {},
+ onOKClick = {},
+ columnState = belowTimeTextPreview(),
+ )
+}
+
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasswordScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasswordScreen.kt
new file mode 100644
index 0000000..d863d3c
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasswordScreen.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 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
+ *
+ * https://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(ExperimentalHorologistApi::class)
+
+package com.android.credentialmanager.ui.screens
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.android.credentialmanager.R
+import com.android.credentialmanager.ui.components.DialogButtonsRow
+import com.android.credentialmanager.ui.components.PasswordRow
+import com.android.credentialmanager.ui.components.SignInHeader
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.layout.ScalingLazyColumnState
+import com.google.android.horologist.compose.layout.belowTimeTextPreview
+import com.google.android.horologist.compose.tools.WearPreview
+
+@Composable
+fun SinglePasswordScreen(
+ email: String,
+ onCancelClick: () -> Unit,
+ onOKClick: () -> Unit,
+ columnState: ScalingLazyColumnState,
+ modifier: Modifier = Modifier,
+) {
+ SingleAccountScreen(
+ headerContent = {
+ SignInHeader(
+ icon = R.drawable.passkey_icon,
+ title = stringResource(R.string.use_password_title),
+ )
+ },
+ accountContent = {
+ PasswordRow(
+ email = email,
+ modifier = Modifier.padding(top = 10.dp),
+ )
+ },
+ columnState = columnState,
+ modifier = modifier.padding(horizontal = 10.dp)
+ ) {
+ item {
+ DialogButtonsRow(
+ onCancelClick = onCancelClick,
+ onOKClick = onOKClick,
+ modifier = Modifier.padding(top = 10.dp)
+ )
+ }
+ }
+}
+
+@WearPreview
+@Composable
+fun SinglePasswordScreenPreview() {
+ SinglePasswordScreen(
+ email = "beckett_bakery@gmail.com",
+ onCancelClick = {},
+ onOKClick = {},
+ columnState = belowTimeTextPreview(),
+ )
+}
+