Merge "Initial CredentialManager UX."
36
packages/CredentialManager/Android.bp
Normal 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"],
|
||||
}
|
||||
46
packages/CredentialManager/AndroidManifest.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
16
packages/CredentialManager/res/drawable/ic_passkey.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
BIN
packages/CredentialManager/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
BIN
packages/CredentialManager/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
BIN
packages/CredentialManager/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
BIN
packages/CredentialManager/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
BIN
packages/CredentialManager/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
10
packages/CredentialManager/res/values/colors.xml
Normal 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>
|
||||
13
packages/CredentialManager/res/values/strings.xml
Normal 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>
|
||||
12
packages/CredentialManager/res/values/themes.xml
Normal 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>
|
||||
13
packages/CredentialManager/res/xml/backup_rules.xml
Normal 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>
|
||||
19
packages/CredentialManager/res/xml/data_extraction_rules.xml
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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({}, {})
|
||||
// }
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
*/
|
||||
)
|
||||