Merge "Initial CredentialManager UX."

This commit is contained in:
Helen Qin
2022-09-22 04:38:37 +00:00
committed by Android (Google) Code Review
34 changed files with 1597 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
package {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
}
android_app {
name: "CredentialManager",
defaults: ["platform_app_defaults"],
certificate: "platform",
srcs: ["src/**/*.kt"],
resource_dirs: ["res"],
static_libs: [
"androidx.activity_activity-compose",
"androidx.appcompat_appcompat",
"androidx.compose.material_material",
"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",
"androidx.lifecycle_lifecycle-runtime-ktx",
"androidx.lifecycle_lifecycle-viewmodel-compose",
"androidx.navigation_navigation-compose",
"androidx.recyclerview_recyclerview",
],
platform_apis: true,
kotlincflags: ["-Xjvm-default=enable"],
}

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* Copyright (c) 2017 Google Inc.
*
* 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.
*/
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.credentialmanager">
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CredentialSelector">
<activity
android:name=".CredentialSelectorActivity"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:noHistory="true"
android:excludeFromRecents="true"
android:theme="@style/Theme.CredentialSelector">
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

View File

@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="24dp"
android:viewportWidth="28"
android:viewportHeight="24">
<path
android:pathData="M27.453,13.253C27.453,14.952 26.424,16.411 24.955,17.041L26.21,18.295L24.839,19.666L26.21,21.037L23.305,23.942L22.012,22.65L22.012,17.156C20.385,16.605 19.213,15.066 19.213,13.253C19.213,10.977 21.058,9.133 23.333,9.133C25.609,9.133 27.453,10.977 27.453,13.253ZM25.47,13.254C25.47,14.434 24.514,15.39 23.334,15.39C22.154,15.39 21.197,14.434 21.197,13.254C21.197,12.074 22.154,11.118 23.334,11.118C24.514,11.118 25.47,12.074 25.47,13.254Z"
android:fillColor="#00639B"
android:fillType="evenOdd"/>
<path
android:pathData="M17.85,5.768C17.85,8.953 15.268,11.536 12.083,11.536C8.897,11.536 6.315,8.953 6.315,5.768C6.315,2.582 8.897,0 12.083,0C15.268,0 17.85,2.582 17.85,5.768Z"
android:fillColor="#00639B"/>
<path
android:pathData="M0.547,20.15C0.547,16.32 8.23,14.382 12.083,14.382C13.59,14.382 15.684,14.679 17.674,15.269C18.116,16.454 18.952,17.447 20.022,18.089V23.071H0.547V20.15Z"
android:fillColor="#00639B"/>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,13 @@
<resources>
<string name="app_name">CredentialManager</string>
<string name="string_cancel">Cancel</string>
<string name="string_continue">Continue</string>
<string name="string_more_options">More options</string>
<string name="string_no_thanks">No thanks</string>
<string name="passkey_creation_intro_title">A simple way to sign in safely</string>
<string name="passkey_creation_intro_body">Use your fingerprint, face or screen lock to sign in with a unique passkey that can’t be forgotten or stolen. Learn more</string>
<string name="choose_provider_title">Choose your default provider</string>
<string name="choose_provider_body">This provider will store passkeys and passwords for you and help you easily autofill and sign in. Learn more</string>
<string name="choose_create_option_title">Create a passkey at</string>
<string name="choose_sign_in_title">Use saved sign in</string>
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.CredentialSelector" parent="@android:style/ThemeOverlay.Material">
<item name="android:statusBarColor">@color/purple_700</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>
</style>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,131 @@
package com.android.credentialmanager
import android.content.Context
import com.android.credentialmanager.createflow.CreateOptionInfo
import com.android.credentialmanager.createflow.ProviderInfo
import com.android.credentialmanager.createflow.ProviderList
import com.android.credentialmanager.getflow.CredentialOptionInfo
class CredentialManagerRepo(
private val context: Context
) {
fun getCredentialProviderList(): List<com.android.credentialmanager.getflow.ProviderInfo> {
return listOf(
com.android.credentialmanager.getflow.ProviderInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
name = "Google Password Manager",
appDomainName = "tribank.us",
credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
credentialOptions = listOf(
CredentialOptionInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
title = "Elisa Backett",
subtitle = "elisa.beckett@gmail.com",
id = "id-1",
),
CredentialOptionInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
title = "Elisa Backett Work",
subtitle = "elisa.beckett.work@google.com",
id = "id-2",
),
)
),
com.android.credentialmanager.getflow.ProviderInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
name = "Lastpass",
appDomainName = "tribank.us",
credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
credentialOptions = listOf(
CredentialOptionInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
title = "Elisa Backett",
subtitle = "elisa.beckett@lastpass.com",
id = "id-1",
),
)
),
com.android.credentialmanager.getflow.ProviderInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
name = "Dashlane",
appDomainName = "tribank.us",
credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
credentialOptions = listOf(
CredentialOptionInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
title = "Elisa Backett",
subtitle = "elisa.beckett@dashlane.com",
id = "id-1",
),
)
),
)
}
fun createCredentialProviderList(): ProviderList {
return ProviderList(
listOf(
ProviderInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
name = "Google Password Manager",
appDomainName = "tribank.us",
credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
createOptions = listOf(
CreateOptionInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
title = "Elisa Backett",
subtitle = "elisa.beckett@gmail.com",
id = "id-1",
),
CreateOptionInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
title = "Elisa Backett Work",
subtitle = "elisa.beckett.work@google.com",
id = "id-2",
),
)
),
ProviderInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
name = "Lastpass",
appDomainName = "tribank.us",
credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
createOptions = listOf(
CreateOptionInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
title = "Elisa Backett",
subtitle = "elisa.beckett@lastpass.com",
id = "id-1",
),
)
),
ProviderInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
name = "Dashlane",
appDomainName = "tribank.us",
credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
createOptions = listOf(
CreateOptionInfo(
icon = context.getDrawable(R.drawable.ic_passkey)!!,
title = "Elisa Backett",
subtitle = "elisa.beckett@dashlane.com",
id = "id-1",
),
)
),
)
)
}
companion object {
lateinit var repo: CredentialManagerRepo
fun setup(context: Context) {
repo = CredentialManagerRepo(context)
}
fun getInstance(): CredentialManagerRepo {
return repo
}
}
}

View File

@@ -0,0 +1,63 @@
package com.android.credentialmanager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.android.credentialmanager.createflow.CreatePasskeyViewModel
import com.android.credentialmanager.createflow.createPasskeyGraph
import com.android.credentialmanager.getflow.GetCredentialViewModel
import com.android.credentialmanager.getflow.getCredentialsGraph
import com.android.credentialmanager.ui.theme.CredentialSelectorTheme
@ExperimentalMaterialApi
class CredentialSelectorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CredentialManagerRepo.setup(this)
val startDestination = intent.extras?.getString(
"start_destination",
"getCredentials"
) ?: "getCredentials"
setContent {
CredentialSelectorTheme {
AppNavHost(
startDestination = startDestination,
onCancel = {this.finish()}
)
}
}
}
@ExperimentalMaterialApi
@Composable
fun AppNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String,
onCancel: () -> Unit,
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
createPasskeyGraph(
navController = navController,
viewModel = CreatePasskeyViewModel(CredentialManagerRepo.repo),
onCancel = onCancel
)
getCredentialsGraph(
navController = navController,
viewModel = GetCredentialViewModel(CredentialManagerRepo.repo),
onCancel = onCancel
)
}
}
}

View File

@@ -0,0 +1,22 @@
package com.android.credentialmanager.createflow
import android.graphics.drawable.Drawable
data class ProviderInfo(
val icon: Drawable,
val name: String,
val appDomainName: String,
val credentialTypeIcon: Drawable,
val createOptions: List<CreateOptionInfo>,
)
data class ProviderList(
val providers: List<ProviderInfo>,
)
data class CreateOptionInfo(
val icon: Drawable,
val title: String,
val subtitle: String,
val id: String,
)

View File

@@ -0,0 +1,541 @@
package com.android.credentialmanager.createflow
import androidx.compose.foundation.BorderStroke
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Card
import androidx.compose.material.Chip
import androidx.compose.material.ChipDefaults
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import androidx.navigation.navigation
import com.android.credentialmanager.R
import com.android.credentialmanager.ui.theme.Grey100
import com.android.credentialmanager.ui.theme.Shapes
import com.android.credentialmanager.ui.theme.Typography
import com.android.credentialmanager.ui.theme.lightBackgroundColor
import com.android.credentialmanager.ui.theme.lightColorAccentSecondary
import com.android.credentialmanager.ui.theme.lightSurface1
@ExperimentalMaterialApi
fun NavGraphBuilder.createPasskeyGraph(
navController: NavController,
viewModel: CreatePasskeyViewModel,
onCancel: () -> Unit,
startDestination: String = "intro", // TODO: get this from view model
) {
navigation(startDestination = startDestination, route = "createPasskey") {
composable("intro") {
CreatePasskeyIntroDialog(
onCancel = onCancel,
onConfirm = {viewModel.onConfirm(navController)}
)
}
composable("providerSelection") {
ProviderSelectionDialog(
providerList = viewModel.uiState.collectAsState().value.providerList,
onProviderSelected = {viewModel.onProviderSelected(it, navController)},
onCancel = onCancel
)
}
composable(
"createCredentialSelection/{providerName}",
arguments = listOf(navArgument("providerName") {type = NavType.StringType})
) {
val arguments = it.arguments
if (arguments == null) {
throw java.lang.IllegalStateException("createCredentialSelection without a provider name")
}
CreationSelectionDialog(
providerInfo = viewModel.getProviderInfoByName(arguments.getString("providerName")!!),
onOptionSelected = {viewModel.onCreateOptionSelected(it)},
onCancel = onCancel,
multiProvider = viewModel.uiState.collectAsState().value.providerList.providers.size > 1,
onMoreOptionSelected = {viewModel.onMoreOptionSelected(navController)},
)
}
}
}
/**
* BEGIN INTRO CONTENT
*/
@ExperimentalMaterialApi
@Composable
fun CreatePasskeyIntroDialog(
onCancel: () -> Unit = {},
onConfirm: () -> Unit = {},
) {
val state = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded,
skipHalfExpanded = true
)
ModalBottomSheetLayout(
sheetState = state,
sheetContent = {
ConfirmationCard(
onCancel = onCancel, onConfirm = onConfirm
)
},
scrimColor = Color.Transparent,
sheetShape = Shapes.medium,
) {}
LaunchedEffect(state.currentValue) {
when (state.currentValue) {
ModalBottomSheetValue.Hidden -> {
onCancel()
}
}
}
}
@Composable
fun ConfirmationCard(
onConfirm: () -> Unit,
onCancel: () -> Unit,
) {
Card(
backgroundColor = lightBackgroundColor,
) {
Column() {
Icon(
painter = painterResource(R.drawable.ic_passkey),
contentDescription = null,
tint = Color.Unspecified,
modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp)
)
Text(
text = stringResource(R.string.passkey_creation_intro_title),
style = Typography.subtitle1,
modifier = Modifier
.padding(horizontal = 24.dp)
.align(alignment = Alignment.CenterHorizontally)
)
Divider(
thickness = 24.dp,
color = Color.Transparent
)
Text(
text = stringResource(R.string.passkey_creation_intro_body),
style = Typography.body1,
modifier = Modifier.padding(horizontal = 28.dp)
)
Divider(
thickness = 48.dp,
color = Color.Transparent
)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
CancelButton(
stringResource(R.string.string_cancel),
onclick = onCancel
)
ConfirmButton(
stringResource(R.string.string_continue),
onclick = onConfirm
)
}
Divider(
thickness = 18.dp,
color = Color.Transparent,
modifier = Modifier.padding(bottom = 16.dp)
)
}
}
}
/**
* END INTRO CONTENT
*/
/**
* BEGIN PROVIDER SELECTION CONTENT
*/
@ExperimentalMaterialApi
@Composable
fun ProviderSelectionDialog(
providerList: ProviderList,
onProviderSelected: (String) -> Unit,
onCancel: () -> Unit,
) {
val state = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded,
skipHalfExpanded = true
)
ModalBottomSheetLayout(
sheetState = state,
sheetContent = {
ProviderSelectionCard(
providerList = providerList,
onCancel = onCancel,
onProviderSelected = onProviderSelected
)
},
scrimColor = Color.Transparent,
sheetShape = Shapes.medium,
) {}
LaunchedEffect(state.currentValue) {
when (state.currentValue) {
ModalBottomSheetValue.Hidden -> {
onCancel()
}
}
}
}
@ExperimentalMaterialApi
@Composable
fun ProviderSelectionCard(
providerList: ProviderList,
onProviderSelected: (String) -> Unit,
onCancel: () -> Unit
) {
Card(
backgroundColor = lightBackgroundColor,
) {
Column() {
Text(
text = stringResource(R.string.choose_provider_title),
style = Typography.subtitle1,
modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
)
Text(
text = stringResource(R.string.choose_provider_body),
style = Typography.body1,
modifier = Modifier.padding(horizontal = 28.dp)
)
Divider(
thickness = 24.dp,
color = Color.Transparent
)
Card(
shape = Shapes.medium,
modifier = Modifier
.padding(horizontal = 24.dp)
.align(alignment = Alignment.CenterHorizontally)
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
providerList.providers.forEach {
item {
ProviderRow(providerInfo = it, onProviderSelected = onProviderSelected)
}
}
}
}
Divider(
thickness = 24.dp,
color = Color.Transparent
)
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
CancelButton(stringResource(R.string.string_cancel), onCancel)
}
Divider(
thickness = 18.dp,
color = Color.Transparent,
modifier = Modifier.padding(bottom = 16.dp)
)
}
}
}
@ExperimentalMaterialApi
@Composable
fun ProviderRow(providerInfo: ProviderInfo, onProviderSelected: (String) -> Unit) {
Chip(
modifier = Modifier.fillMaxWidth(),
onClick = {onProviderSelected(providerInfo.name)},
leadingIcon = {
Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
bitmap = providerInfo.icon.toBitmap().asImageBitmap(),
// painter = painterResource(R.drawable.ic_passkey),
// TODO: add description.
contentDescription = "")
},
colors = ChipDefaults.chipColors(
backgroundColor = Grey100,
leadingIconContentColor = Grey100
),
shape = Shapes.large
) {
Text(
text = providerInfo.name,
style = Typography.button,
modifier = Modifier.padding(vertical = 18.dp)
)
}
}
/**
* END PROVIDER SELECTION CONTENT
*/
/**
* BEGIN COMMON COMPONENTS
*/
@Composable
fun CancelButton(text: String, onclick: () -> Unit) {
val colors = ButtonDefaults.buttonColors(
backgroundColor = lightBackgroundColor
)
NavigationButton(
border = BorderStroke(1.dp, lightSurface1),
colors = colors,
text = text,
onclick = onclick)
}
@Composable
fun ConfirmButton(text: String, onclick: () -> Unit) {
val colors = ButtonDefaults.buttonColors(
backgroundColor = lightColorAccentSecondary
)
NavigationButton(
colors = colors,
text = text,
onclick = onclick)
}
@Composable
fun NavigationButton(
border: BorderStroke? = null,
colors: ButtonColors,
text: String,
onclick: () -> Unit
) {
Button(
onClick = onclick,
shape = Shapes.small,
colors = colors,
border = border
) {
Text(text = text, style = Typography.button)
}
}
/**
* BEGIN CREATE OPTION SELECTION CONTENT
*/
@ExperimentalMaterialApi
@Composable
fun CreationSelectionDialog(
providerInfo: ProviderInfo,
onOptionSelected: (String) -> Unit,
onCancel: () -> Unit,
multiProvider: Boolean,
onMoreOptionSelected: () -> Unit,
) {
val state = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded,
skipHalfExpanded = true
)
ModalBottomSheetLayout(
sheetState = state,
sheetContent = {
CreationSelectionCard(
providerInfo = providerInfo,
onCancel = onCancel,
onOptionSelected = onOptionSelected,
multiProvider = multiProvider,
onMoreOptionSelected = onMoreOptionSelected,
)
},
scrimColor = Color.Transparent,
sheetShape = Shapes.medium,
) {}
LaunchedEffect(state.currentValue) {
when (state.currentValue) {
ModalBottomSheetValue.Hidden -> {
onCancel()
}
}
}
}
@ExperimentalMaterialApi
@Composable
fun CreationSelectionCard(
providerInfo: ProviderInfo,
onOptionSelected: (String) -> Unit,
onCancel: () -> Unit,
multiProvider: Boolean,
onMoreOptionSelected: () -> Unit,
) {
Card(
backgroundColor = lightBackgroundColor,
) {
Column() {
Icon(
bitmap = providerInfo.credentialTypeIcon.toBitmap().asImageBitmap(),
contentDescription = null,
tint = Color.Unspecified,
modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp)
)
Text(
text = "${stringResource(R.string.choose_create_option_title)} ${providerInfo.name}",
style = Typography.subtitle1,
modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
)
Text(
text = providerInfo.appDomainName,
style = Typography.body2,
modifier = Modifier.padding(horizontal = 28.dp)
)
Divider(
thickness = 24.dp,
color = Color.Transparent
)
Card(
shape = Shapes.medium,
modifier = Modifier
.padding(horizontal = 24.dp)
.align(alignment = Alignment.CenterHorizontally)
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
providerInfo.createOptions.forEach {
item {
CreateOptionRow(createOptionInfo = it, onOptionSelected = onOptionSelected)
}
}
if (multiProvider) {
item {
MoreOptionRow(onSelect = onMoreOptionSelected)
}
}
}
}
Divider(
thickness = 24.dp,
color = Color.Transparent
)
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
CancelButton(stringResource(R.string.string_cancel), onCancel)
}
Divider(
thickness = 18.dp,
color = Color.Transparent,
modifier = Modifier.padding(bottom = 16.dp)
)
}
}
}
@ExperimentalMaterialApi
@Composable
fun CreateOptionRow(createOptionInfo: CreateOptionInfo, onOptionSelected: (String) -> Unit) {
Chip(
modifier = Modifier.fillMaxWidth(),
onClick = {onOptionSelected(createOptionInfo.id)},
leadingIcon = {
Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
bitmap = createOptionInfo.icon.toBitmap().asImageBitmap(),
// painter = painterResource(R.drawable.ic_passkey),
// TODO: add description.
contentDescription = "")
},
colors = ChipDefaults.chipColors(
backgroundColor = Grey100,
leadingIconContentColor = Grey100
),
shape = Shapes.large
) {
Column() {
Text(
text = createOptionInfo.title,
style = Typography.h6,
modifier = Modifier.padding(top = 16.dp)
)
Text(
text = createOptionInfo.subtitle,
style = Typography.body2,
modifier = Modifier.padding(bottom = 16.dp)
)
}
}
}
@ExperimentalMaterialApi
@Composable
fun MoreOptionRow(onSelect: () -> Unit) {
Chip(
modifier = Modifier.fillMaxWidth().height(52.dp),
onClick = onSelect,
colors = ChipDefaults.chipColors(
backgroundColor = Grey100,
leadingIconContentColor = Grey100
),
shape = Shapes.large
) {
Text(
text = stringResource(R.string.string_more_options),
style = Typography.h6,
)
}
}
/**
* END CREATE OPTION SELECTION CONTENT
*/
/**
* END COMMON COMPONENTS
*/
@ExperimentalMaterialApi
@Preview(showBackground = true)
@Composable
fun CreatePasskeyEntryScreenPreview() {
// val providers = ProviderList(
// listOf(
// ProviderInfo(null),
// ProviderInfo(null, "Dashlane"),
// ProviderInfo(null, "LastPass")
// )
// )
// TatiAccountSelectorTheme {
// ConfirmationCard({}, {})
// }
}

View File

@@ -0,0 +1,51 @@
package com.android.credentialmanager.createflow
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.navigation.NavController
import com.android.credentialmanager.CredentialManagerRepo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
data class CreatePasskeyUiState(
val providerList: ProviderList,
)
class CreatePasskeyViewModel(
credManRepo: CredentialManagerRepo
) : ViewModel() {
private val _uiState = MutableStateFlow(
CreatePasskeyUiState(credManRepo.createCredentialProviderList())
)
val uiState: StateFlow<CreatePasskeyUiState> = _uiState.asStateFlow()
fun onConfirm(navController: NavController) {
if (uiState.value.providerList.providers.size > 1) {
navController.navigate("providerSelection")
} else if (uiState.value.providerList.providers.size == 1) {
onProviderSelected(uiState.value.providerList.providers[0].name, navController)
} else {
throw java.lang.IllegalStateException("Empty provider list.")
}
}
fun onProviderSelected(providerName: String, navController: NavController) {
return navController.navigate("createCredentialSelection/$providerName")
}
fun onCreateOptionSelected(createOptionId: String) {
Log.d("Account Selector", "Option selected for creation: $createOptionId")
}
fun getProviderInfoByName(providerName: String): ProviderInfo {
return uiState.value.providerList.providers.single {
it.name.equals(providerName)
}
}
fun onMoreOptionSelected(navController: NavController) {
navController.navigate("moreOption")
}
}

View File

@@ -0,0 +1,232 @@
package com.android.credentialmanager.getflow
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Card
import androidx.compose.material.Chip
import androidx.compose.material.ChipDefaults
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import com.android.credentialmanager.R
import com.android.credentialmanager.createflow.CancelButton
import com.android.credentialmanager.ui.theme.Grey100
import com.android.credentialmanager.ui.theme.Shapes
import com.android.credentialmanager.ui.theme.Typography
import com.android.credentialmanager.ui.theme.lightBackgroundColor
@ExperimentalMaterialApi
fun NavGraphBuilder.getCredentialsGraph(
navController: NavController,
viewModel: GetCredentialViewModel,
onCancel: () -> Unit,
startDestination: String = "credentialSelection", // TODO: get this from view model
) {
navigation(startDestination = startDestination, route = "getCredentials") {
composable("credentialSelection") {
CredentialSelectionDialog(
providerInfo = viewModel.getDefaultProviderInfo(),
onOptionSelected = {viewModel.onCredentailSelected(it, navController)},
onCancel = onCancel,
multiProvider = viewModel.uiState.collectAsState().value.providers.size > 1,
onMoreOptionSelected = {viewModel.onMoreOptionSelected(navController)}
)
}
}
}
/**
* BEGIN CREATE OPTION SELECTION CONTENT
*/
@ExperimentalMaterialApi
@Composable
fun CredentialSelectionDialog(
providerInfo: ProviderInfo,
onOptionSelected: (String) -> Unit,
onCancel: () -> Unit,
multiProvider: Boolean,
onMoreOptionSelected: () -> Unit,
) {
val state = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded,
skipHalfExpanded = true
)
ModalBottomSheetLayout(
sheetState = state,
sheetContent = {
CredentialSelectionCard(
providerInfo = providerInfo,
onCancel = onCancel,
onOptionSelected = onOptionSelected,
multiProvider = multiProvider,
onMoreOptionSelected = onMoreOptionSelected,
)
},
scrimColor = Color.Transparent,
sheetShape = Shapes.medium,
) {}
LaunchedEffect(state.currentValue) {
when (state.currentValue) {
ModalBottomSheetValue.Hidden -> {
onCancel()
}
}
}
}
@ExperimentalMaterialApi
@Composable
fun CredentialSelectionCard(
providerInfo: ProviderInfo,
onOptionSelected: (String) -> Unit,
onCancel: () -> Unit,
multiProvider: Boolean,
onMoreOptionSelected: () -> Unit,
) {
Card(
backgroundColor = lightBackgroundColor,
) {
Column() {
Icon(
bitmap = providerInfo.credentialTypeIcon.toBitmap().asImageBitmap(),
contentDescription = null,
tint = Color.Unspecified,
modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp)
)
Text(
text = stringResource(R.string.choose_sign_in_title),
style = Typography.subtitle1,
modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
)
Text(
text = providerInfo.appDomainName,
style = Typography.body2,
modifier = Modifier.padding(horizontal = 28.dp)
)
Divider(
thickness = 24.dp,
color = Color.Transparent
)
Card(
shape = Shapes.medium,
modifier = Modifier
.padding(horizontal = 24.dp)
.align(alignment = Alignment.CenterHorizontally)
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
providerInfo.credentialOptions.forEach {
item {
CredentialOptionRow(credentialOptionInfo = it, onOptionSelected = onOptionSelected)
}
}
if (multiProvider) {
item {
MoreOptionRow(onSelect = onMoreOptionSelected)
}
}
}
}
Divider(
thickness = 24.dp,
color = Color.Transparent
)
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
CancelButton(stringResource(R.string.string_no_thanks), onCancel)
}
Divider(
thickness = 18.dp,
color = Color.Transparent,
modifier = Modifier.padding(bottom = 16.dp)
)
}
}
}
@ExperimentalMaterialApi
@Composable
fun CredentialOptionRow(
credentialOptionInfo: CredentialOptionInfo,
onOptionSelected: (String) -> Unit
) {
Chip(
modifier = Modifier.fillMaxWidth(),
onClick = {onOptionSelected(credentialOptionInfo.id)},
leadingIcon = {
Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
bitmap = credentialOptionInfo.icon.toBitmap().asImageBitmap(),
// painter = painterResource(R.drawable.ic_passkey),
// TODO: add description.
contentDescription = "")
},
colors = ChipDefaults.chipColors(
backgroundColor = Grey100,
leadingIconContentColor = Grey100
),
shape = Shapes.large
) {
Column() {
Text(
text = credentialOptionInfo.title,
style = Typography.h6,
modifier = Modifier.padding(top = 16.dp)
)
Text(
text = credentialOptionInfo.subtitle,
style = Typography.body2,
modifier = Modifier.padding(bottom = 16.dp)
)
}
}
}
@ExperimentalMaterialApi
@Composable
fun MoreOptionRow(onSelect: () -> Unit) {
Chip(
modifier = Modifier.fillMaxWidth().height(52.dp),
onClick = onSelect,
colors = ChipDefaults.chipColors(
backgroundColor = Grey100,
leadingIconContentColor = Grey100
),
shape = Shapes.large
) {
Text(
text = stringResource(R.string.string_more_options),
style = Typography.h6,
)
}
}
/**
* END CREATE OPTION SELECTION CONTENT
*/

View File

@@ -0,0 +1,36 @@
package com.android.credentialmanager.getflow
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.navigation.NavController
import com.android.credentialmanager.CredentialManagerRepo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
data class GetCredentialUiState(
val providers: List<ProviderInfo>
)
class GetCredentialViewModel(
credManRepo: CredentialManagerRepo
) : ViewModel() {
private val _uiState = MutableStateFlow(
GetCredentialUiState(credManRepo.getCredentialProviderList())
)
val uiState: StateFlow<GetCredentialUiState> = _uiState.asStateFlow()
fun getDefaultProviderInfo(): ProviderInfo {
// TODO: correctly get the default provider.
return uiState.value.providers.first()
}
fun onCredentailSelected(credentialId: String, navController: NavController) {
Log.d("Account Selector", "credential selected: $credentialId")
}
fun onMoreOptionSelected(navController: NavController) {
Log.d("Account Selector", "More Option selected")
}
}

View File

@@ -0,0 +1,18 @@
package com.android.credentialmanager.getflow
import android.graphics.drawable.Drawable
data class ProviderInfo(
val icon: Drawable,
val name: String,
val appDomainName: String,
val credentialTypeIcon: Drawable,
val credentialOptions: List<CredentialOptionInfo>,
)
data class CredentialOptionInfo(
val icon: Drawable,
val title: String,
val subtitle: String,
val id: String,
)

View File

@@ -0,0 +1,14 @@
package com.android.credentialmanager.ui.theme
import androidx.compose.ui.graphics.Color
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)

View File

@@ -0,0 +1,11 @@
package com.android.credentialmanager.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(100.dp),
medium = RoundedCornerShape(20.dp),
large = RoundedCornerShape(0.dp)
)

View File

@@ -0,0 +1,47 @@
package com.android.credentialmanager.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
@Composable
fun CredentialSelectorTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}

View File

@@ -0,0 +1,56 @@
package com.android.credentialmanager.ui.theme
import androidx.compose.material.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(
subtitle1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 24.sp,
lineHeight = 32.sp,
),
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
),
body2 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
color = textColorSecondary
),
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
),
h6 = 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
)
*/
)