Use the launch intent data in the ux.

Note: The intent data is not a full list of everything we need yet so
the ux is looking a bit raw operating on the only information it can
get. Following up will be improving this.

Test: deployed locally
Bug: 247855226
Change-Id: Icd66866d37210486f3c5223c9e93b9b589476f96
This commit is contained in:
Helen Qin
2022-10-14 02:54:09 +00:00
parent 9db605bb91
commit 0bc03e3032
13 changed files with 415 additions and 163 deletions

View File

@@ -36,6 +36,12 @@ public class RequestInfo implements Parcelable {
*/
public static final @NonNull String EXTRA_REQUEST_INFO =
"android.credentials.ui.extra.REQUEST_INFO";
/**
* The intent extra key for the {@code ResultReceiver} object when launching the UX
* activities.
*/
public static final @NonNull String EXTRA_RESULT_RECEIVER =
"android.credentials.ui.extra.RESULT_RECEIVER";
/** Type value for an executeGetCredential request. */
public static final @NonNull String TYPE_GET = "android.credentials.ui.TYPE_GET";

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager
import android.app.slice.Slice
import android.credentials.ui.Entry
import android.graphics.drawable.Icon
/**
* UI representation for a credential entry used during the get credential flow.
*
* TODO: move to jetpack.
*/
class CredentialEntryUi(
val userName: CharSequence,
val displayName: CharSequence?,
val icon: Icon?,
// TODO: add last used.
) {
companion object {
fun fromSlice(slice: Slice): CredentialEntryUi {
val items = slice.items
var title: String? = null
var subTitle: String? = null
var icon: Icon? = null
items.forEach {
if (it.hasHint(Entry.HINT_ICON)) {
icon = it.icon
} else if (it.hasHint(Entry.HINT_SUBTITLE)) {
subTitle = it.text.toString()
} else if (it.hasHint(Entry.HINT_TITLE)) {
title = it.text.toString()
}
}
// TODO: fail NPE more elegantly.
return CredentialEntryUi(title!!, subTitle, icon)
}
}
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager
import android.app.slice.Slice
@@ -9,11 +25,8 @@ import android.credentials.ui.ProviderData
import android.credentials.ui.RequestInfo
import android.graphics.drawable.Icon
import android.os.Binder
import com.android.credentialmanager.createflow.CreateOptionInfo
import com.android.credentialmanager.createflow.CreatePasskeyUiState
import com.android.credentialmanager.createflow.CreateScreenState
import com.android.credentialmanager.createflow.ProviderInfo
import com.android.credentialmanager.getflow.CredentialOptionInfo
import com.android.credentialmanager.getflow.GetCredentialUiState
import com.android.credentialmanager.getflow.GetScreenState
@@ -41,6 +54,39 @@ class CredentialManagerRepo(
) ?: testProviderList()
}
fun getCredentialInitialUiState(): GetCredentialUiState {
val providerList = GetFlowUtils.toProviderList(providerList, context)
return GetCredentialUiState(
providerList,
GetScreenState.CREDENTIAL_SELECTION,
providerList.first()
)
}
fun createPasskeyInitialUiState(): CreatePasskeyUiState {
val providerList = CreateFlowUtils.toProviderList(providerList, context)
return CreatePasskeyUiState(
providers = providerList,
currentScreenState = CreateScreenState.PASSKEY_INTRO,
)
}
companion object {
lateinit var repo: CredentialManagerRepo
fun setup(
context: Context,
intent: Intent,
) {
repo = CredentialManagerRepo(context, intent)
}
fun getInstance(): CredentialManagerRepo {
return repo
}
}
// TODO: below are prototype functionalities. To be removed for productionization.
private fun testProviderList(): List<ProviderData> {
return listOf(
ProviderData(
@@ -94,143 +140,4 @@ class CredentialManagerRepo(
slice
)
}
private 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",
),
)
),
)
}
private fun createCredentialProviderList(): List<ProviderInfo> {
return 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",
),
)
),
)
}
fun getCredentialInitialUiState(): GetCredentialUiState {
val providerList = getCredentialProviderList()
return GetCredentialUiState(
providerList,
GetScreenState.CREDENTIAL_SELECTION,
providerList.first()
)
}
fun createPasskeyInitialUiState(): CreatePasskeyUiState {
val providerList = createCredentialProviderList()
return CreatePasskeyUiState(
providers = providerList,
currentScreenState = CreateScreenState.PASSKEY_INTRO,
)
}
companion object {
lateinit var repo: CredentialManagerRepo
fun setup(
context: Context,
intent: Intent,
) {
repo = CredentialManagerRepo(context, intent)
}
fun getInstance(): CredentialManagerRepo {
return repo
}
}
}

View File

@@ -1,5 +1,22 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager
import android.credentials.ui.RequestInfo
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
@@ -16,14 +33,20 @@ class CredentialSelectorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CredentialManagerRepo.setup(this, intent)
val startDestination = intent.extras?.getString(
"start_destination",
"CREATE_PASSKEY"
) ?: "CREATE_PASSKEY"
setContent {
CredentialSelectorTheme {
CredentialManagerBottomSheet(startDestination)
val requestInfo = intent.extras?.getParcelable<RequestInfo>(RequestInfo.EXTRA_REQUEST_INFO)
if (requestInfo != null) {
val requestType = requestInfo.type
setContent {
CredentialSelectorTheme {
CredentialManagerBottomSheet(requestType)
}
}
} else {
// TODO: prototype only code to be removed. In production should exit.
setContent {
CredentialSelectorTheme {
CredentialManagerBottomSheet(RequestInfo.TYPE_CREATE)
}
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager
import android.content.Context
import android.credentials.ui.Entry
import android.credentials.ui.ProviderData
import com.android.credentialmanager.createflow.CreateOptionInfo
import com.android.credentialmanager.getflow.CredentialOptionInfo
import com.android.credentialmanager.getflow.ProviderInfo
/** Utility functions for converting CredentialManager data structures to or from UI formats. */
class GetFlowUtils {
companion object {
fun toProviderList(
providerDataList: List<ProviderData>,
context: Context,
): List<ProviderInfo> {
return providerDataList.map {
ProviderInfo(
// TODO: replace to extract from the service data structure when available
icon = context.getDrawable(R.drawable.ic_passkey)!!,
name = it.packageName,
appDomainName = "tribank.us",
credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context)
)
}
}
/* From service data structure to UI credential entry list representation. */
private fun toCredentialOptionInfoList(
credentialEntries: List<Entry>,
context: Context,
): List<CredentialOptionInfo> {
return credentialEntries.map {
val credentialEntryUi = CredentialEntryUi.fromSlice(it.slice)
// Consider directly move the UI object into the class.
return@map CredentialOptionInfo(
// TODO: remove fallbacks
icon = credentialEntryUi.icon?.loadDrawable(context)
?: context.getDrawable(R.drawable.ic_passkey)!!,
title = credentialEntryUi.userName.toString(),
subtitle = credentialEntryUi.displayName?.toString() ?: "Unknown display name",
id = it.entryId,
)
}
}
}
}
class CreateFlowUtils {
companion object {
fun toProviderList(
providerDataList: List<ProviderData>,
context: Context,
): List<com.android.credentialmanager.createflow.ProviderInfo> {
return providerDataList.map {
com.android.credentialmanager.createflow.ProviderInfo(
// TODO: replace to extract from the service data structure when available
icon = context.getDrawable(R.drawable.ic_passkey)!!,
name = it.packageName,
appDomainName = "tribank.us",
credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
createOptions = toCreationOptionInfoList(it.credentialEntries, context),
)
}
}
private fun toCreationOptionInfoList(
creationEntries: List<Entry>,
context: Context,
): List<CreateOptionInfo> {
return creationEntries.map {
val saveEntryUi = SaveEntryUi.fromSlice(it.slice)
return@map CreateOptionInfo(
// TODO: remove fallbacks
icon = saveEntryUi.icon?.loadDrawable(context)
?: context.getDrawable(R.drawable.ic_passkey)!!,
title = saveEntryUi.title.toString(),
subtitle = saveEntryUi.subTitle?.toString() ?: "Unknown subtitle",
id = it.entryId,
)
}
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager
import android.app.slice.Slice
import android.credentials.ui.Entry
import android.graphics.drawable.Icon
/**
* UI representation for a save entry used during the create credential flow.
*
* TODO: move to jetpack.
*/
class SaveEntryUi(
val title: CharSequence,
val subTitle: CharSequence?,
val icon: Icon?,
// TODO: add
) {
companion object {
fun fromSlice(slice: Slice): SaveEntryUi {
val items = slice.items
var title: String? = null
var subTitle: String? = null
var icon: Icon? = null
items.forEach {
if (it.hasHint(Entry.HINT_ICON)) {
icon = it.icon
} else if (it.hasHint(Entry.HINT_SUBTITLE)) {
subTitle = it.text.toString()
} else if (it.hasHint(Entry.HINT_TITLE)) {
title = it.text.toString()
}
}
// TODO: fail NPE more elegantly.
return SaveEntryUi(title!!, subTitle, icon)
}
}
}

View File

@@ -1,5 +1,23 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.common
import android.credentials.ui.RequestInfo
enum class DialogType {
CREATE_PASSKEY,
GET_CREDENTIALS,
@@ -8,10 +26,10 @@ enum class DialogType {
companion object {
fun toDialogType(value: String): DialogType {
return try {
valueOf(value)
} catch (e: IllegalArgumentException) {
UNKNOWN
return when (value) {
RequestInfo.TYPE_GET -> GET_CREDENTIALS
RequestInfo.TYPE_CREATE -> CREATE_PASSKEY
else -> UNKNOWN
}
}
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.createflow
import android.graphics.drawable.Drawable
@@ -14,7 +30,7 @@ data class CreateOptionInfo(
val icon: Drawable,
val title: String,
val subtitle: String,
val id: String,
val id: Int,
)
/** The name of the current screen. */

View File

@@ -222,7 +222,9 @@ fun MoreOptionSelectionCard(
) {
Column() {
TopAppBar(
title = { Text(text = stringResource(R.string.string_more_options), style = Typography.subtitle1) },
title = {
Text(text = stringResource(R.string.string_more_options), style = Typography.subtitle1)
},
backgroundColor = lightBackgroundColor,
elevation = 0.dp,
navigationIcon =
@@ -343,7 +345,7 @@ fun NavigationButton(
@Composable
fun CreationSelectionCard(
providerInfo: ProviderInfo,
onOptionSelected: (String) -> Unit,
onOptionSelected: (Int) -> Unit,
onCancel: () -> Unit,
multiProvider: Boolean,
onMoreOptionSelected: (String) -> Unit,
@@ -414,7 +416,7 @@ fun CreationSelectionCard(
@ExperimentalMaterialApi
@Composable
fun CreateOptionRow(createOptionInfo: CreateOptionInfo, onOptionSelected: (String) -> Unit) {
fun CreateOptionRow(createOptionInfo: CreateOptionInfo, onOptionSelected: (Int) -> Unit) {
Chip(
modifier = Modifier.fillMaxWidth(),
onClick = {onOptionSelected(createOptionInfo.id)},

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.createflow
import android.util.Log
@@ -42,7 +58,7 @@ class CreatePasskeyViewModel(
)
}
fun onCreateOptionSelected(createOptionId: String) {
fun onCreateOptionSelected(createOptionId: Int) {
Log.d("Account Selector", "Option selected for creation: $createOptionId")
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.getflow
import androidx.compose.foundation.Image
@@ -74,7 +90,7 @@ fun GetCredentialScreen(
@Composable
fun CredentialSelectionCard(
providerInfo: ProviderInfo,
onOptionSelected: (String) -> Unit,
onOptionSelected: (Int) -> Unit,
onCancel: () -> Unit,
multiProvider: Boolean,
onMoreOptionSelected: () -> Unit,
@@ -149,7 +165,7 @@ fun CredentialSelectionCard(
@Composable
fun CredentialOptionRow(
credentialOptionInfo: CredentialOptionInfo,
onOptionSelected: (String) -> Unit
onOptionSelected: (Int) -> Unit
) {
Chip(
modifier = Modifier.fillMaxWidth(),

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.getflow
import android.util.Log
@@ -20,7 +36,7 @@ class GetCredentialViewModel(
var uiState by mutableStateOf(credManRepo.getCredentialInitialUiState())
private set
fun onCredentailSelected(credentialId: String) {
fun onCredentailSelected(credentialId: Int) {
Log.d("Account Selector", "credential selected: $credentialId")
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.getflow
import android.graphics.drawable.Drawable
@@ -14,7 +30,7 @@ data class CredentialOptionInfo(
val icon: Drawable,
val title: String,
val subtitle: String,
val id: String,
val id: Int,
)
/** The name of the current screen. */