Use the official jetpack lib.

Also included bug fixes of incorrectly showing 0 for absent credential
counts and unknown create credential flow always displaying fake request
info.

Fix: 255688790
Fix: 267575490
Fix: 261050124
Test: local deployment
Change-Id: I6659a9a94c14cd10de5641615177b891c9a472c2
This commit is contained in:
Helen Qin
2023-02-01 19:06:04 +00:00
parent 968a80bc30
commit 8a83c3d077
29 changed files with 597 additions and 2029 deletions

View File

@@ -31,6 +31,7 @@ android_app {
"androidx.compose.ui_ui",
"androidx.compose.ui_ui-tooling",
"androidx.core_core-ktx",
"androidx.credentials_credentials",
"androidx.lifecycle_lifecycle-extensions",
"androidx.lifecycle_lifecycle-livedata",
"androidx.lifecycle_lifecycle-runtime-ktx",

View File

@@ -16,12 +16,10 @@
package com.android.credentialmanager
import android.app.PendingIntent
import android.app.slice.Slice
import android.app.slice.SliceSpec
import android.content.Context
import android.content.Intent
import android.content.pm.SigningInfo
import android.credentials.CreateCredentialRequest
import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import android.credentials.CredentialOption
@@ -40,18 +38,15 @@ import android.net.Uri
import android.os.Binder
import android.os.Bundle
import android.os.ResultReceiver
import android.service.credentials.CredentialProviderService
import com.android.credentialmanager.createflow.DisabledProviderInfo
import com.android.credentialmanager.createflow.EnabledProviderInfo
import com.android.credentialmanager.createflow.RequestDisplayInfo
import com.android.credentialmanager.getflow.GetCredentialUiState
import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toCredentialDataBundle
import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
import com.android.credentialmanager.jetpack.provider.Action
import com.android.credentialmanager.jetpack.provider.CreateEntry
import com.android.credentialmanager.jetpack.provider.CredentialCountInformation
import com.android.credentialmanager.jetpack.provider.CredentialEntry
import androidx.credentials.CreateCredentialRequest.DisplayInfo
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.CreatePasswordRequest
import java.time.Instant
// Consider repo per screen, similar to view model?
class CredentialManagerRepo(
@@ -180,14 +175,14 @@ class CredentialManagerRepo(
.Builder("io.enpass.app")
.setSaveEntries(
listOf<Entry>(
newCreateEntry(
CreateTestUtils.newCreateEntry(context,
"key1", "subkey-1", "elisa.beckett@gmail.com",
20, 7, 27, 10L,
"Optional footer description"
20, 7, 27, Instant.ofEpochSecond(10L),
"Legal note"
),
newCreateEntry(
CreateTestUtils.newCreateEntry(context,
"key1", "subkey-2", "elisa.work@google.com",
20, 7, 27, 12L,
20, 7, 27, Instant.ofEpochSecond(12L),
null
),
)
@@ -200,14 +195,14 @@ class CredentialManagerRepo(
.Builder("com.dashlane")
.setSaveEntries(
listOf<Entry>(
newCreateEntry(
CreateTestUtils.newCreateEntry(context,
"key1", "subkey-3", "elisa.beckett@dashlane.com",
20, 7, 27, 11L,
20, 7, 27, Instant.ofEpochSecond(11L),
null
),
newCreateEntry(
CreateTestUtils.newCreateEntry(context,
"key1", "subkey-4", "elisa.work@dashlane.com",
20, 7, 27, 14L,
20, 7, 27, Instant.ofEpochSecond(14L),
null
),
)
@@ -228,33 +223,33 @@ class CredentialManagerRepo(
GetCredentialProviderData.Builder("io.enpass.app")
.setCredentialEntries(
listOf<Entry>(
newGetEntry(
"key1", "subkey-1", TYPE_PASSWORD_CREDENTIAL, "Password",
"elisa.family@outlook.com", null, 3L
GetTestUtils.newPasswordEntry(
context, "key1", "subkey-1", "elisa.family@outlook.com", null,
Instant.ofEpochSecond(8000L)
),
newGetEntry(
"key1", "subkey-1", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
"elisa.bakery@gmail.com", "Elisa Beckett", 0L
GetTestUtils.newPasskeyEntry(
context, "key1", "subkey-1", "elisa.bakery@gmail.com", "Elisa Beckett",
null
),
newGetEntry(
"key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password",
"elisa.bakery@gmail.com", null, 10L
GetTestUtils.newPasswordEntry(
context, "key1", "subkey-2", "elisa.bakery@gmail.com", null,
Instant.ofEpochSecond(10000L)
),
newGetEntry(
"key1", "subkey-3", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
"elisa.family@outlook.com", "Elisa Beckett", 1L
GetTestUtils.newPasskeyEntry(
context, "key1", "subkey-3", "elisa.family@outlook.com",
"Elisa Beckett", Instant.ofEpochSecond(500L)
),
)
).setAuthenticationEntry(
newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL)
GetTestUtils.newAuthenticationEntry(context, "key2", "subkey-1")
).setActionChips(
listOf(
newActionEntry(
"key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL,
GetTestUtils.newActionEntry(
context, "key3", "subkey-1",
"Open Google Password Manager", "elisa.beckett@gmail.com"
),
newActionEntry(
"key3", "subkey-2", TYPE_PASSWORD_CREDENTIAL,
GetTestUtils.newActionEntry(
context, "key3", "subkey-2",
"Open Google Password Manager", "beckett-family@gmail.com"
),
)
@@ -264,137 +259,29 @@ class CredentialManagerRepo(
GetCredentialProviderData.Builder("com.dashlane")
.setCredentialEntries(
listOf<Entry>(
newGetEntry(
"key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password",
"elisa.family@outlook.com", null, 4L
GetTestUtils.newPasswordEntry(
context, "key1", "subkey-2", "elisa.family@outlook.com", null,
Instant.ofEpochSecond(9000L)
),
newGetEntry(
"key1", "subkey-3", TYPE_PASSWORD_CREDENTIAL, "Password",
"elisa.work@outlook.com", null, 11L
GetTestUtils.newPasswordEntry(
context, "key1", "subkey-3", "elisa.work@outlook.com", null,
Instant.ofEpochSecond(11000L)
),
)
).setAuthenticationEntry(
newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL)
GetTestUtils.newAuthenticationEntry(context, "key2", "subkey-1")
).setActionChips(
listOf(
newActionEntry(
"key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL,
"Open Enpass"
GetTestUtils.newActionEntry(
context, "key3", "subkey-1", "Open Enpass",
"Manage passwords"
),
)
).build(),
)
}
private fun newActionEntry(
key: String,
subkey: String,
credentialType: String,
text: String,
subtext: String? = null,
): Entry {
val action = Action(text, subtext, null)
return Entry(
key,
subkey,
Action.toSlice(action)
)
}
private fun newAuthenticationEntry(
key: String,
subkey: String,
credentialType: String,
): Entry {
val slice = Slice.Builder(
Uri.EMPTY, SliceSpec(credentialType, 1)
)
return Entry(
key,
subkey,
slice.build()
)
}
private fun newGetEntry(
key: String,
subkey: String,
credentialType: String,
credentialTypeDisplayName: String,
userName: String,
userDisplayName: String?,
lastUsedTimeMillis: Long?,
): Entry {
val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
.setPackage("com.androidauth.androidvault")
intent.putExtra("provider_extra_sample", "testprovider")
val pendingIntent = PendingIntent.getActivity(
context, 1,
intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
or PendingIntent.FLAG_ONE_SHOT)
)
val credentialEntry = CredentialEntry(
credentialType, credentialTypeDisplayName, userName,
userDisplayName, pendingIntent, lastUsedTimeMillis
?: 0L, null, false
)
return Entry(
key,
subkey,
CredentialEntry.toSlice(credentialEntry),
Intent()
)
}
private fun newCreateEntry(
key: String,
subkey: String,
providerDisplayName: String,
passwordCount: Int,
passkeyCount: Int,
totalCredentialCount: Int,
lastUsedTimeMillis: Long,
footerDescription: String?,
): Entry {
val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
.setPackage("com.androidauth.androidvault")
intent.putExtra("provider_extra_sample", "testprovider")
val pendingIntent = PendingIntent.getActivity(
context, 1,
intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
or PendingIntent.FLAG_ONE_SHOT)
)
val createPasswordRequest = android.service.credentials.CreateCredentialRequest(
android.service.credentials.CallingAppInfo(
context.applicationInfo.packageName, SigningInfo()
),
TYPE_PASSWORD_CREDENTIAL,
toCredentialDataBundle("beckett-bakert@gmail.com", "password123")
)
val fillInIntent = Intent().putExtra(
CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST,
createPasswordRequest
)
val createEntry = CreateEntry(
providerDisplayName, pendingIntent,
null, lastUsedTimeMillis,
listOf(
CredentialCountInformation.createPasswordCountInformation(passwordCount),
CredentialCountInformation.createPublicKeyCountInformation(passkeyCount),
), footerDescription
)
return Entry(
key,
subkey,
CreateEntry.toSlice(createEntry),
fillInIntent
)
}
private fun newRemoteEntry(
key: String,
@@ -451,7 +338,7 @@ class CredentialManagerRepo(
return RequestInfo.newCreateRequestInfo(
Binder(),
CreateCredentialRequest(
TYPE_PUBLIC_KEY_CREDENTIAL,
"androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL",
credentialData,
// TODO: populate with actual data
/*candidateQueryData=*/ Bundle(),
@@ -462,14 +349,13 @@ class CredentialManagerRepo(
}
private fun testCreatePasswordRequestInfo(): RequestInfo {
val data = toCredentialDataBundle("beckett-bakert@gmail.com", "password123")
val request = CreatePasswordRequest("beckett-bakert@gmail.com", "password123")
return RequestInfo.newCreateRequestInfo(
Binder(),
CreateCredentialRequest(
TYPE_PASSWORD_CREDENTIAL,
data,
// TODO: populate with actual data
/*candidateQueryData=*/ Bundle(),
request.credentialData,
request.candidateQueryData,
/*isSystemProviderRequired=*/ false
),
"com.google.android.youtube"
@@ -478,6 +364,10 @@ class CredentialManagerRepo(
private fun testCreateOtherCredentialRequestInfo(): RequestInfo {
val data = Bundle()
val displayInfo = DisplayInfo("my-username00", "Joe")
data.putBundle(
"androidx.credentials.BUNDLE_KEY_REQUEST_DISPLAY_INFO",
displayInfo.toBundle())
return RequestInfo.newCreateRequestInfo(
Binder(),
CreateCredentialRequest(
@@ -498,7 +388,7 @@ class CredentialManagerRepo(
)
.addCredentialOption(
CredentialOption(
TYPE_PUBLIC_KEY_CREDENTIAL,
"androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL",
Bundle(),
Bundle(), /*isSystemProviderRequired=*/
false

View File

@@ -16,18 +16,22 @@
package com.android.credentialmanager
import android.app.slice.Slice
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import android.credentials.ui.CreateCredentialProviderData
import android.credentials.ui.DisabledProviderData
import android.credentials.ui.Entry
import android.credentials.ui.GetCredentialProviderData
import android.credentials.ui.RequestInfo
import android.graphics.drawable.Drawable
import android.service.credentials.CredentialEntry
import android.text.TextUtils
import android.util.Log
import com.android.credentialmanager.common.Constants
import com.android.credentialmanager.common.CredentialType
import com.android.credentialmanager.createflow.ActiveEntry
import com.android.credentialmanager.createflow.CreateCredentialUiState
import com.android.credentialmanager.createflow.CreateOptionInfo
@@ -41,17 +45,22 @@ import com.android.credentialmanager.getflow.AuthenticationEntryInfo
import com.android.credentialmanager.getflow.CredentialEntryInfo
import com.android.credentialmanager.getflow.ProviderInfo
import com.android.credentialmanager.getflow.RemoteEntryInfo
import com.android.credentialmanager.jetpack.developer.CreateCredentialRequest
import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest
import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
import com.android.credentialmanager.jetpack.provider.Action
import com.android.credentialmanager.jetpack.provider.AuthenticationAction
import com.android.credentialmanager.jetpack.provider.CreateEntry
import com.android.credentialmanager.jetpack.provider.CredentialCountInformation
import com.android.credentialmanager.jetpack.provider.CredentialEntry
import androidx.credentials.CreateCredentialRequest
import androidx.credentials.CreateCustomCredentialRequest
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
import androidx.credentials.provider.Action
import androidx.credentials.provider.AuthenticationAction
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.CustomCredentialEntry
import androidx.credentials.provider.PasswordCredentialEntry
import androidx.credentials.provider.PublicKeyCredentialEntry
import androidx.credentials.provider.RemoteCreateEntry
import androidx.credentials.provider.RemoteCredentialEntry
import org.json.JSONObject
// TODO: remove all !! checks
private fun getAppLabel(
pm: PackageManager,
appPackageName: String
@@ -174,25 +183,76 @@ class GetFlowUtils {
credentialEntries: List<Entry>,
context: Context,
): List<CredentialEntryInfo> {
return credentialEntries.map {
// TODO: handle NPE gracefully
val credentialEntry = CredentialEntry.fromSlice(it.slice)!!
val result: MutableList<CredentialEntryInfo> = mutableListOf()
credentialEntries.forEach {
val credentialEntry = parseCredentialEntryFromSlice(it.slice)
when (credentialEntry) {
is PasswordCredentialEntry -> {
result.add(CredentialEntryInfo(
providerId = providerId,
entryKey = it.key,
entrySubkey = it.subkey,
pendingIntent = credentialEntry.pendingIntent,
fillInIntent = it.frameworkExtrasIntent,
credentialType = CredentialType.PASSWORD,
credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
userName = credentialEntry.username.toString(),
displayName = credentialEntry.displayName?.toString(),
icon = credentialEntry.icon.loadDrawable(context),
lastUsedTimeMillis = credentialEntry.lastUsedTime,
))
}
is PublicKeyCredentialEntry -> {
result.add(CredentialEntryInfo(
providerId = providerId,
entryKey = it.key,
entrySubkey = it.subkey,
pendingIntent = credentialEntry.pendingIntent,
fillInIntent = it.frameworkExtrasIntent,
credentialType = CredentialType.PASSKEY,
credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
userName = credentialEntry.username.toString(),
displayName = credentialEntry.displayName?.toString(),
icon = credentialEntry.icon.loadDrawable(context),
lastUsedTimeMillis = credentialEntry.lastUsedTime,
))
}
is CustomCredentialEntry -> {
result.add(CredentialEntryInfo(
providerId = providerId,
entryKey = it.key,
entrySubkey = it.subkey,
pendingIntent = credentialEntry.pendingIntent,
fillInIntent = it.frameworkExtrasIntent,
credentialType = CredentialType.UNKNOWN,
credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
userName = credentialEntry.title.toString(),
displayName = credentialEntry.subtitle?.toString(),
icon = credentialEntry.icon.loadDrawable(context),
lastUsedTimeMillis = credentialEntry.lastUsedTime,
))
}
else -> Log.d(
Constants.LOG_TAG,
"Encountered unrecognized credential entry ${it.slice.spec?.type}"
)
}
}
// TODO: handle empty list due to parsing error.
return result
}
// Consider directly move the UI object into the class.
return@map CredentialEntryInfo(
providerId = providerId,
entryKey = it.key,
entrySubkey = it.subkey,
pendingIntent = credentialEntry.pendingIntent,
fillInIntent = it.frameworkExtrasIntent,
credentialType = credentialEntry.type,
credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
userName = credentialEntry.username.toString(),
displayName = credentialEntry.displayName?.toString(),
// TODO: proper fallback
icon = credentialEntry.icon?.loadDrawable(context),
lastUsedTimeMillis = credentialEntry.lastUsedTimeMillis,
)
private fun parseCredentialEntryFromSlice(slice: Slice): CredentialEntry? {
try {
when (slice.spec?.type) {
TYPE_PASSWORD_CREDENTIAL -> return PasswordCredentialEntry.fromSlice(slice)!!
TYPE_PUBLIC_KEY_CREDENTIAL -> return PublicKeyCredentialEntry.fromSlice(slice)!!
else -> return CustomCredentialEntry.fromSlice(slice)!!
}
} catch (e: Exception) {
// Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
// password / passkey parsing attempt.
return CustomCredentialEntry.fromSlice(slice)
}
}
@@ -205,18 +265,13 @@ class GetFlowUtils {
if (authEntry == null) {
return null
}
val authStructuredEntry = AuthenticationAction.fromSlice(
authEntry!!.slice
)
if (authStructuredEntry == null) {
return null
}
val structuredAuthEntry =
AuthenticationAction.fromSlice(authEntry.slice) ?: return null
return AuthenticationEntryInfo(
providerId = providerId,
entryKey = authEntry.key,
entrySubkey = authEntry.subkey,
pendingIntent = authStructuredEntry.pendingIntent,
pendingIntent = structuredAuthEntry.pendingIntent,
fillInIntent = authEntry.frameworkExtrasIntent,
title = providerDisplayName,
icon = providerIcon,
@@ -228,11 +283,13 @@ class GetFlowUtils {
if (remoteEntry == null) {
return null
}
val structuredRemoteEntry = RemoteCredentialEntry.fromSlice(remoteEntry.slice)
?: return null
return RemoteEntryInfo(
providerId = providerId,
entryKey = remoteEntry.key,
entrySubkey = remoteEntry.subkey,
pendingIntent = remoteEntry.pendingIntent,
pendingIntent = structuredRemoteEntry.pendingIntent,
fillInIntent = remoteEntry.frameworkExtrasIntent,
)
}
@@ -242,22 +299,22 @@ class GetFlowUtils {
actionEntries: List<Entry>,
providerIcon: Drawable,
): List<ActionEntryInfo> {
return actionEntries.map {
// TODO: handle NPE gracefully
val actionEntryUi = Action.fromSlice(it.slice)!!
return@map ActionEntryInfo(
val result: MutableList<ActionEntryInfo> = mutableListOf()
actionEntries.forEach {
val actionEntryUi = Action.fromSlice(it.slice) ?: return@forEach
result.add(ActionEntryInfo(
providerId = providerId,
entryKey = it.key,
entrySubkey = it.subkey,
pendingIntent = actionEntryUi.pendingIntent,
fillInIntent = it.frameworkExtrasIntent,
title = actionEntryUi.title.toString(),
// TODO: gracefully fail
icon = providerIcon,
subTitle = actionEntryUi.subTitle?.toString(),
)
subTitle = actionEntryUi.subtitle?.toString(),
))
}
// TODO: handle empty list
return result
}
}
}
@@ -316,22 +373,21 @@ class CreateFlowUtils {
): RequestDisplayInfo? {
val appLabel = getAppLabel(context.packageManager, requestInfo.appPackageName)
?: return null
val createCredentialRequest = requestInfo.createCredentialRequest
val createCredentialRequestJetpack = createCredentialRequest?.let {
CreateCredentialRequest.createFrom(
it.type, it.credentialData, it.candidateQueryData, it.isSystemProviderRequired
val createCredentialRequest = requestInfo.createCredentialRequest ?: return null
val createCredentialRequestJetpack = CreateCredentialRequest.createFrom(
createCredentialRequest.type,
createCredentialRequest.credentialData,
createCredentialRequest.candidateQueryData,
createCredentialRequest.isSystemProviderRequired
)
return when (createCredentialRequestJetpack) {
is CreatePasswordRequest -> RequestDisplayInfo(
createCredentialRequestJetpack.id,
createCredentialRequestJetpack.password,
CredentialType.PASSWORD,
appLabel,
context.getDrawable(R.drawable.ic_password)!!
)
}
when (createCredentialRequestJetpack) {
is CreatePasswordRequest -> {
return RequestDisplayInfo(
createCredentialRequestJetpack.id,
createCredentialRequestJetpack.password,
createCredentialRequestJetpack.type,
appLabel,
context.getDrawable(R.drawable.ic_password)!!
)
}
is CreatePublicKeyCredentialRequest -> {
val requestJson = createCredentialRequestJetpack.requestJson
val json = JSONObject(requestJson)
@@ -342,24 +398,29 @@ class CreateFlowUtils {
name = user.getString("name")
displayName = user.getString("displayName")
}
return RequestDisplayInfo(
RequestDisplayInfo(
name,
displayName,
createCredentialRequestJetpack.type,
CredentialType.PASSKEY,
appLabel,
context.getDrawable(R.drawable.ic_passkey)!!
)
}
// TODO: correctly parsing for other sign-ins
else -> {
return RequestDisplayInfo(
"beckett-bakert@gmail.com",
"Elisa Beckett",
"other-sign-ins",
appLabel.toString(),
context.getDrawable(R.drawable.ic_other_sign_in)!!
is CreateCustomCredentialRequest -> {
// TODO: directly use the display info once made public
val displayInfo = CreateCredentialRequest.DisplayInfo
.parseFromCredentialDataBundle(createCredentialRequest.credentialData)
?: return null
RequestDisplayInfo(
title = displayInfo.userId,
subtitle = displayInfo.userDisplayName,
type = CredentialType.UNKNOWN,
appName = appLabel,
typeIcon = displayInfo.credentialTypeIcon?.loadDrawable(context)
?: context.getDrawable(R.drawable.ic_other_sign_in)!!
)
}
else -> null
}
}
@@ -408,7 +469,7 @@ class CreateFlowUtils {
currentScreenState = initialScreenState,
requestDisplayInfo = requestDisplayInfo,
sortedCreateOptionsPairs = createOptionsPairs.sortedWith(
compareByDescending { it.first.lastUsedTimeMillis }
compareByDescending { it.first.lastUsedTime }
),
hasDefaultProvider = defaultProvider != null,
activeEntry = toActiveEntry(
@@ -430,7 +491,7 @@ class CreateFlowUtils {
isPasskeyFirstUse: Boolean,
): CreateScreenState? {
return if (isPasskeyFirstUse && requestDisplayInfo.type ==
TYPE_PUBLIC_KEY_CREDENTIAL && !isOnPasskeyIntroStateAlready) {
CredentialType.PASSKEY && !isOnPasskeyIntroStateAlready) {
CreateScreenState.PASSKEY_INTRO
} else if ((defaultProvider == null || defaultProvider.createOptions.isEmpty()) &&
createOptionSize > 1) {
@@ -479,12 +540,10 @@ class CreateFlowUtils {
creationEntries: List<Entry>,
context: Context,
): List<CreateOptionInfo> {
return creationEntries.map {
// TODO: handle NPE gracefully
val createEntry = CreateEntry.fromSlice(it.slice)!!
return@map CreateOptionInfo(
// TODO: remove fallbacks
val result: MutableList<CreateOptionInfo> = mutableListOf()
creationEntries.forEach {
val createEntry = CreateEntry.fromSlice(it.slice) ?: return@forEach
result.add(CreateOptionInfo(
providerId = providerId,
entryKey = it.key,
entrySubkey = it.subkey,
@@ -492,32 +551,28 @@ class CreateFlowUtils {
fillInIntent = it.frameworkExtrasIntent,
userProviderDisplayName = createEntry.accountName.toString(),
profileIcon = createEntry.icon?.loadDrawable(context),
passwordCount = CredentialCountInformation.getPasswordCount(
createEntry.credentialCountInformationList
) ?: 0,
passkeyCount = CredentialCountInformation.getPasskeyCount(
createEntry.credentialCountInformationList
) ?: 0,
totalCredentialCount = CredentialCountInformation.getTotalCount(
createEntry.credentialCountInformationList
) ?: 0,
lastUsedTimeMillis = createEntry.lastUsedTimeMillis ?: 0,
footerDescription = createEntry.footerDescription?.toString()
)
passwordCount = createEntry.getPasswordCredentialCount(),
passkeyCount = createEntry.getPublicKeyCredentialCount(),
totalCredentialCount = createEntry.getTotalCredentialCount(),
lastUsedTime = createEntry.lastUsedTime,
footerDescription = createEntry.description?.toString()
))
}
return result
}
private fun toRemoteInfo(
providerId: String,
remoteEntry: Entry?,
): RemoteInfo? {
// TODO: should also call fromSlice after getting the official jetpack code.
return if (remoteEntry != null) {
val structuredRemoteEntry = RemoteCreateEntry.fromSlice(remoteEntry.slice)
?: return null
RemoteInfo(
providerId = providerId,
entryKey = remoteEntry.key,
entrySubkey = remoteEntry.subkey,
pendingIntent = remoteEntry.pendingIntent,
pendingIntent = structuredRemoteEntry.pendingIntent,
fillInIntent = remoteEntry.frameworkExtrasIntent,
)
} else null

View File

@@ -0,0 +1,352 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager
import android.app.PendingIntent
import android.app.slice.Slice
import android.app.slice.SliceSpec
import android.content.Context
import android.content.Intent
import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import android.credentials.ui.Entry
import android.graphics.drawable.Icon
import android.net.Uri
import android.provider.Settings
import androidx.credentials.provider.CreateEntry
import java.time.Instant
// TODO: remove once testing is complete
class GetTestUtils {
companion object {
internal fun newAuthenticationEntry(
context: Context,
key: String,
subkey: String,
): Entry {
val slice = Slice.Builder(
Uri.EMPTY, SliceSpec("AuthenticationAction", 0)
)
val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
val pendingIntent =
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
slice.addAction(
pendingIntent,
Slice.Builder(slice)
.addHints(listOf("androidx.credentials.provider.authenticationAction" +
".SLICE_HINT_PENDING_INTENT"))
.build(),
/*subType=*/null
)
return Entry(
key,
subkey,
slice.build()
)
}
internal fun newActionEntry(
context: Context,
key: String,
subkey: String,
text: String,
subtext: String? = null,
): Entry {
val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
val pendingIntent =
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("Action", 0))
.addText(
text, /*subType=*/null,
listOf("androidx.credentials.provider.action.HINT_ACTION_TITLE")
)
.addText(
subtext, /*subType=*/null,
listOf("androidx.credentials.provider.action.HINT_ACTION_SUBTEXT")
)
sliceBuilder.addAction(
pendingIntent,
Slice.Builder(sliceBuilder)
.addHints(
listOf("androidx.credentials.provider.action.SLICE_HINT_PENDING_INTENT")
)
.build(),
/*subType=*/null
)
return Entry(
key,
subkey,
sliceBuilder.build()
)
}
private const val SLICE_HINT_TYPE_DISPLAY_NAME =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
private const val SLICE_HINT_TITLE =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_USER_NAME"
private const val SLICE_HINT_SUBTITLE =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
private const val SLICE_HINT_ICON =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_PROFILE_ICON"
private const val SLICE_HINT_PENDING_INTENT =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_PENDING_INTENT"
private const val SLICE_HINT_AUTO_ALLOWED =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_AUTO_ALLOWED"
private const val AUTO_SELECT_TRUE_STRING = "true"
private const val AUTO_SELECT_FALSE_STRING = "false"
internal fun newPasswordEntry(
context: Context,
key: String,
subkey: String,
userName: String,
userDisplayName: String?,
lastUsedTime: Instant?,
): Entry {
val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
.setPackage("com.androidauth.androidvault")
intent.putExtra("provider_extra_sample", "testprovider")
val pendingIntent = PendingIntent.getActivity(
context, 1,
intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
or PendingIntent.FLAG_ONE_SHOT)
)
return Entry(
key,
subkey,
toPasswordSlice(userName, userDisplayName, pendingIntent, lastUsedTime),
Intent()
)
}
private fun toPasswordSlice(
title: CharSequence,
subTitle: CharSequence?,
pendingIntent: PendingIntent,
lastUsedTime: Instant?,
icon: Icon? = null,
isAutoSelectAllowed: Boolean = true
): Slice {
val type = TYPE_PASSWORD_CREDENTIAL
val autoSelectAllowed = if (isAutoSelectAllowed) {
AUTO_SELECT_TRUE_STRING
} else {
AUTO_SELECT_FALSE_STRING
}
val sliceBuilder = Slice.Builder(
Uri.EMPTY, SliceSpec(
type, 1
)
)
.addText(
"Password", /*subType=*/null,
listOf(SLICE_HINT_TYPE_DISPLAY_NAME)
)
.addText(
title, /*subType=*/null,
listOf(SLICE_HINT_TITLE)
)
.addText(
subTitle, /*subType=*/null,
listOf(SLICE_HINT_SUBTITLE)
)
.addText(
autoSelectAllowed, /*subType=*/null,
listOf(SLICE_HINT_AUTO_ALLOWED)
)
if (lastUsedTime != null) {
sliceBuilder.addLong(
lastUsedTime.toEpochMilli(),
/*subType=*/null,
listOf(SLICE_HINT_LAST_USED_TIME_MILLIS)
)
}
if (icon != null) {
sliceBuilder.addIcon(
icon, /*subType=*/null,
listOf(SLICE_HINT_ICON)
)
}
sliceBuilder.addAction(
pendingIntent,
Slice.Builder(sliceBuilder)
.addHints(listOf(SLICE_HINT_PENDING_INTENT))
.build(),
/*subType=*/null
)
return sliceBuilder.build()
}
private const val PASSKEY_SLICE_HINT_TYPE_DISPLAY_NAME =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
private const val PASSKEY_SLICE_HINT_TITLE =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_USER_NAME"
private const val PASSKEY_SLICE_HINT_SUBTITLE =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
private const val PASSKEY_SLICE_HINT_LAST_USED_TIME_MILLIS =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
private const val PASSKEY_SLICE_HINT_ICON =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_PROFILE_ICON"
private const val PASSKEY_SLICE_HINT_PENDING_INTENT =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_PENDING_INTENT"
private const val PASSKEY_SLICE_HINT_AUTO_ALLOWED =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_AUTO_ALLOWED"
internal fun newPasskeyEntry(
context: Context,
key: String,
subkey: String,
userName: String,
userDisplayName: String?,
lastUsedTime: Instant?,
): Entry {
val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
.setPackage("com.androidauth.androidvault")
intent.putExtra("provider_extra_sample", "testprovider")
val pendingIntent = PendingIntent.getActivity(
context, 1,
intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
or PendingIntent.FLAG_ONE_SHOT)
)
return Entry(
key, subkey, toPasskeySlice(
userName, userDisplayName, pendingIntent, lastUsedTime
),
Intent()
)
}
private fun toPasskeySlice(
title: CharSequence,
subTitle: CharSequence?,
pendingIntent: PendingIntent,
lastUsedTime: Instant?,
icon: Icon? = null,
isAutoSelectAllowed: Boolean = true
): Slice {
val type = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
val autoSelectAllowed = if (isAutoSelectAllowed) {
AUTO_SELECT_TRUE_STRING
} else {
AUTO_SELECT_FALSE_STRING
}
val sliceBuilder = Slice.Builder(
Uri.EMPTY, SliceSpec(
type, 1
)
)
.addText(
"Passkey", /*subType=*/null,
listOf(PASSKEY_SLICE_HINT_TYPE_DISPLAY_NAME)
)
.addText(
title, /*subType=*/null,
listOf(PASSKEY_SLICE_HINT_TITLE)
)
.addText(
subTitle, /*subType=*/null,
listOf(PASSKEY_SLICE_HINT_SUBTITLE)
)
.addText(
autoSelectAllowed, /*subType=*/null,
listOf(PASSKEY_SLICE_HINT_AUTO_ALLOWED)
)
if (lastUsedTime != null) {
sliceBuilder.addLong(
lastUsedTime.toEpochMilli(),
/*subType=*/null,
listOf(PASSKEY_SLICE_HINT_LAST_USED_TIME_MILLIS)
)
}
if (icon != null) {
sliceBuilder.addIcon(
icon, /*subType=*/null,
listOf(PASSKEY_SLICE_HINT_ICON)
)
}
sliceBuilder.addAction(
pendingIntent,
Slice.Builder(sliceBuilder)
.addHints(listOf(PASSKEY_SLICE_HINT_PENDING_INTENT))
.build(),
/*subType=*/null
)
return sliceBuilder.build()
}
}
}
class CreateTestUtils {
companion object {
private const val TYPE_TOTAL_CREDENTIAL = "TOTAL_CREDENTIAL_COUNT_TYPE"
private const val SLICE_HINT_ACCOUNT_NAME =
"androidx.credentials.provider.createEntry.SLICE_HINT_USER_PROVIDER_ACCOUNT_NAME"
private const val SLICE_HINT_NOTE =
"androidx.credentials.provider.createEntry.SLICE_HINT_NOTE"
private const val SLICE_HINT_ICON =
"androidx.credentials.provider.createEntry.SLICE_HINT_PROFILE_ICON"
private const val SLICE_HINT_CREDENTIAL_COUNT_INFORMATION =
"androidx.credentials.provider.createEntry.SLICE_HINT_CREDENTIAL_COUNT_INFORMATION"
private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
"androidx.credentials.provider.createEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
private const val SLICE_HINT_PENDING_INTENT =
"androidx.credentials.provider.createEntry.SLICE_HINT_PENDING_INTENT"
internal fun newCreateEntry(
context: Context,
key: String,
subkey: String,
providerUserDisplayName: String,
passwordCount: Int?,
passkeyCount: Int?,
totalCredentialCount: Int?,
lastUsedTime: Instant?,
footerDescription: String?,
): Entry {
val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
.setPackage("com.androidauth.androidvault")
intent.putExtra("provider_extra_sample", "testprovider")
val pendingIntent = PendingIntent.getActivity(
context, 1,
intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
or PendingIntent.FLAG_ONE_SHOT)
)
val credCountMap = mutableMapOf<String, Int>()
passwordCount?.let { credCountMap.put(TYPE_PASSWORD_CREDENTIAL, it) }
passkeyCount?.let {
credCountMap.put("androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL", it)
}
totalCredentialCount?.let { credCountMap.put(TYPE_TOTAL_CREDENTIAL, it) }
return Entry(
key,
subkey,
CreateEntry.toSlice(
providerUserDisplayName,
null,
footerDescription,
lastUsedTime,
credCountMap,
pendingIntent
),
Intent()
)
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 The Android Open Source Project
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,8 @@
* limitations under the License.
*/
package com.android.credentialmanager.jetpack.developer
package com.android.credentialmanager.common
/**
* Internal exception used to indicate a parsing error while converting from a framework type to
* a jetpack type.
*
* @hide
*/
internal class FrameworkClassParsingException : Exception()
enum class CredentialType {
UNKNOWN, PASSKEY, PASSWORD,
}

View File

@@ -2,7 +2,6 @@
package com.android.credentialmanager.createflow
import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.result.ActivityResult
import androidx.activity.result.IntentSenderRequest
@@ -37,6 +36,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import com.android.credentialmanager.R
import com.android.credentialmanager.common.CredentialType
import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.common.material.ModalBottomSheetLayout
import com.android.credentialmanager.common.material.ModalBottomSheetValue
@@ -50,7 +50,6 @@ import com.android.credentialmanager.common.ui.TextOnSurfaceVariant
import com.android.credentialmanager.common.ui.ContainerCard
import com.android.credentialmanager.ui.theme.EntryShape
import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -288,11 +287,11 @@ fun ProviderSelectionCard(
text = stringResource(
R.string.choose_provider_title,
when (requestDisplayInfo.type) {
TYPE_PUBLIC_KEY_CREDENTIAL ->
CredentialType.PASSKEY ->
stringResource(R.string.passkeys)
TYPE_PASSWORD_CREDENTIAL ->
CredentialType.PASSWORD ->
stringResource(R.string.passwords)
else -> stringResource(R.string.sign_in_info)
CredentialType.UNKNOWN -> stringResource(R.string.sign_in_info)
}),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(horizontal = 24.dp)
@@ -398,11 +397,11 @@ fun MoreOptionsSelectionCard(
stringResource(
R.string.save_credential_to_title,
when (requestDisplayInfo.type) {
TYPE_PUBLIC_KEY_CREDENTIAL ->
CredentialType.PASSKEY ->
stringResource(R.string.passkey)
TYPE_PASSWORD_CREDENTIAL ->
CredentialType.PASSWORD ->
stringResource(R.string.password)
else -> stringResource(R.string.sign_in_info)
CredentialType.UNKNOWN -> stringResource(R.string.sign_in_info)
}),
style = MaterialTheme.typography.titleMedium,
)
@@ -571,15 +570,15 @@ fun CreationSelectionCard(
)
TextOnSurface(
text = when (requestDisplayInfo.type) {
TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(
CredentialType.PASSKEY -> stringResource(
R.string.choose_create_option_passkey_title,
requestDisplayInfo.appName
)
TYPE_PASSWORD_CREDENTIAL -> stringResource(
CredentialType.PASSWORD -> stringResource(
R.string.choose_create_option_password_title,
requestDisplayInfo.appName
)
else -> stringResource(
CredentialType.UNKNOWN -> stringResource(
R.string.choose_create_option_sign_in_title,
requestDisplayInfo.appName
)
@@ -828,7 +827,7 @@ fun PrimaryCreateOptionRow(
Column() {
// TODO: Add the function to hide/view password when the type is create password
when (requestDisplayInfo.type) {
TYPE_PUBLIC_KEY_CREDENTIAL -> {
CredentialType.PASSKEY -> {
TextOnSurfaceVariant(
text = requestDisplayInfo.title,
style = MaterialTheme.typography.titleLarge,
@@ -846,7 +845,7 @@ fun PrimaryCreateOptionRow(
modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
)
}
TYPE_PASSWORD_CREDENTIAL -> {
CredentialType.PASSWORD -> {
TextOnSurfaceVariant(
text = requestDisplayInfo.title,
style = MaterialTheme.typography.titleLarge,
@@ -859,7 +858,7 @@ fun PrimaryCreateOptionRow(
modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
)
}
else -> {
CredentialType.UNKNOWN -> {
if (requestDisplayInfo.subtitle != null) {
TextOnSurfaceVariant(
text = requestDisplayInfo.title,
@@ -917,8 +916,8 @@ fun MoreOptionsInfoRow(
modifier = Modifier.padding(start = 5.dp),
)
}
if (requestDisplayInfo.type == TYPE_PUBLIC_KEY_CREDENTIAL ||
requestDisplayInfo.type == TYPE_PASSWORD_CREDENTIAL) {
if (requestDisplayInfo.type == CredentialType.PASSKEY ||
requestDisplayInfo.type == CredentialType.PASSWORD) {
if (createOptionInfo.passwordCount != null &&
createOptionInfo.passkeyCount != null
) {

View File

@@ -21,6 +21,8 @@ import android.content.Intent
import android.graphics.drawable.Drawable
import com.android.credentialmanager.common.DialogState
import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.common.CredentialType
import java.time.Instant
data class CreateCredentialUiState(
val enabledProviders: List<EnabledProviderInfo>,
@@ -68,18 +70,18 @@ open class EntryInfo (
)
class CreateOptionInfo(
providerId: String,
entryKey: String,
entrySubkey: String,
pendingIntent: PendingIntent?,
fillInIntent: Intent?,
val userProviderDisplayName: String?,
val profileIcon: Drawable?,
val passwordCount: Int?,
val passkeyCount: Int?,
val totalCredentialCount: Int?,
val lastUsedTimeMillis: Long?,
val footerDescription: String?,
providerId: String,
entryKey: String,
entrySubkey: String,
pendingIntent: PendingIntent?,
fillInIntent: Intent?,
val userProviderDisplayName: String?,
val profileIcon: Drawable?,
val passwordCount: Int?,
val passkeyCount: Int?,
val totalCredentialCount: Int?,
val lastUsedTime: Instant?,
val footerDescription: String?,
) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent)
class RemoteInfo(
@@ -93,7 +95,7 @@ class RemoteInfo(
data class RequestDisplayInfo(
val title: String,
val subtitle: String?,
val type: String,
val type: CredentialType,
val appName: String,
val typeIcon: Drawable,
)

View File

@@ -16,7 +16,6 @@
package com.android.credentialmanager.getflow
import android.credentials.Credential
import android.text.TextUtils
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.result.ActivityResult
@@ -59,6 +58,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import com.android.credentialmanager.R
import com.android.credentialmanager.common.CredentialType
import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.common.material.ModalBottomSheetLayout
import com.android.credentialmanager.common.material.ModalBottomSheetValue
@@ -71,7 +71,6 @@ import com.android.credentialmanager.common.ui.TextSecondary
import com.android.credentialmanager.common.ui.TextOnSurfaceVariant
import com.android.credentialmanager.common.ui.ContainerCard
import com.android.credentialmanager.common.ui.TransparentBackgroundEntry
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
import com.android.credentialmanager.ui.theme.EntryShape
import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
@@ -171,7 +170,7 @@ fun PrimarySelectionCard(
) {
if (sortedUserNameToCredentialEntryList.first()
.sortedCredentialEntryList.first().credentialType
== PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL
== CredentialType.PASSKEY
) R.string.get_dialog_title_use_passkey_for
else R.string.get_dialog_title_use_sign_in_for
} else if (
@@ -534,7 +533,7 @@ fun CredentialEntryRow(
)
TextSecondary(
text = if (
credentialEntryInfo.credentialType == Credential.TYPE_PASSWORD_CREDENTIAL) {
credentialEntryInfo.credentialType == CredentialType.PASSWORD) {
"••••••••••••"
} else {
if (TextUtils.isEmpty(credentialEntryInfo.displayName))

View File

@@ -26,11 +26,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.android.credentialmanager.CredentialManagerRepo
import com.android.credentialmanager.common.CredentialType
import com.android.credentialmanager.common.Constants
import com.android.credentialmanager.common.DialogState
import com.android.credentialmanager.common.ProviderActivityResult
import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
import com.android.internal.util.Preconditions
data class GetCredentialUiState(
@@ -258,9 +258,9 @@ internal class CredentialEntryInfoComparatorByTypeThenTimestamp : Comparator<Cre
override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
// First prefer passkey type for its security benefits
if (p0.credentialType != p1.credentialType) {
if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p0.credentialType) {
if (CredentialType.PASSKEY == p0.credentialType) {
return -1
} else if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p1.credentialType) {
} else if (CredentialType.PASSKEY == p1.credentialType) {
return 1
}
}
@@ -272,9 +272,9 @@ internal class CredentialEntryInfoComparatorByTypeThenTimestamp : Comparator<Cre
} else if (p0.lastUsedTimeMillis > p1.lastUsedTimeMillis) {
return -1
}
} else if (p0.lastUsedTimeMillis != null && p0.lastUsedTimeMillis > 0) {
} else if (p0.lastUsedTimeMillis != null) {
return -1
} else if (p1.lastUsedTimeMillis != null && p1.lastUsedTimeMillis > 0) {
} else if (p1.lastUsedTimeMillis != null) {
return 1
}
return 0

View File

@@ -19,6 +19,9 @@ package com.android.credentialmanager.getflow
import android.app.PendingIntent
import android.content.Intent
import android.graphics.drawable.Drawable
import com.android.credentialmanager.common.CredentialType
import java.time.Instant
data class ProviderInfo(
/**
@@ -62,13 +65,13 @@ class CredentialEntryInfo(
pendingIntent: PendingIntent?,
fillInIntent: Intent?,
/** Type of this credential used for sorting. Not localized so must not be directly displayed. */
val credentialType: String,
val credentialType: CredentialType,
/** Localized type value of this credential used for display purpose. */
val credentialTypeDisplayName: String,
val userName: String,
val displayName: String?,
val icon: Drawable?,
val lastUsedTimeMillis: Long?,
val lastUsedTimeMillis: Instant?,
) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent)
class AuthenticationEntryInfo(

View File

@@ -1,74 +0,0 @@
/*
* 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.jetpack.developer
import android.credentials.Credential
import android.os.Bundle
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
/**
* Base request class for registering a credential.
*
* @property type the credential type
* @property data the request data in the [Bundle] format
* @property requireSystemProvider true if must only be fulfilled by a system provider and false
* otherwise
*/
open class CreateCredentialRequest(
open val type: String,
open val credentialData: Bundle,
open val candidateQueryData: Bundle,
open val requireSystemProvider: Boolean
) {
companion object {
@JvmStatic
fun createFrom(
type: String,
credentialData: Bundle,
candidateQueryData: Bundle,
requireSystemProvider: Boolean
): CreateCredentialRequest {
return try {
when (type) {
Credential.TYPE_PASSWORD_CREDENTIAL ->
CreatePasswordRequest.createFrom(credentialData)
PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
when (credentialData.getString(BUNDLE_KEY_SUBTYPE)) {
CreatePublicKeyCredentialRequest
.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST ->
CreatePublicKeyCredentialRequest.createFrom(credentialData)
CreatePublicKeyCredentialRequestPrivileged
.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV ->
CreatePublicKeyCredentialRequestPrivileged
.createFrom(credentialData)
else -> throw FrameworkClassParsingException()
}
else -> throw FrameworkClassParsingException()
}
} catch (e: FrameworkClassParsingException) {
// Parsing failed but don't crash the process. Instead just output a request with
// the raw framework values.
CreateCustomCredentialRequest(
type,
credentialData,
candidateQueryData,
requireSystemProvider
)
}
}
}
}

View File

@@ -1,50 +0,0 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.jetpack.developer
import android.os.Bundle
/**
* Base custom create request class for registering a credential.
*
* An application can construct a subtype custom request and call
* [CredentialManager.executeCreateCredential] to launch framework UI flows to collect consent and
* any other metadata needed from the user to register a new user credential.
*
* @property type the credential type determined by the credential-type-specific subclass for custom
* use cases
* @property credentialData the full credential creation request data in the [Bundle] format for
* custom use cases
* @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
* the provider during the initial candidate query stage, which should not contain sensitive user
* credential information
* @property requireSystemProvider true if must only be fulfilled by a system provider and false
* otherwise
* @throws IllegalArgumentException If [type] is empty
* @throws NullPointerException If [type] or [credentialData] are null
*/
open class CreateCustomCredentialRequest(
final override val type: String,
final override val credentialData: Bundle,
final override val candidateQueryData: Bundle,
@get:JvmName("requireSystemProvider")
final override val requireSystemProvider: Boolean
) : CreateCredentialRequest(type, credentialData, candidateQueryData, requireSystemProvider) {
init {
require(type.isNotEmpty()) { "type should not be empty" }
}
}

View File

@@ -1,76 +0,0 @@
/*
* 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.jetpack.developer
import android.credentials.Credential
import android.os.Bundle
/**
* A request to save the user password credential with their password provider.
*
* @property id the user id associated with the password
* @property password the password
* @throws NullPointerException If [id] is null
* @throws NullPointerException If [password] is null
* @throws IllegalArgumentException If [password] is empty
*/
class CreatePasswordRequest constructor(
val id: String,
val password: String,
) : CreateCredentialRequest(
type = Credential.TYPE_PASSWORD_CREDENTIAL,
credentialData = toCredentialDataBundle(id, password),
// No credential data should be sent during the query phase.
candidateQueryData = Bundle(),
requireSystemProvider = false,
) {
init {
require(password.isNotEmpty()) { "password should not be empty" }
}
companion object {
const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID"
const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD"
@JvmStatic
internal fun toCredentialDataBundle(id: String, password: String): Bundle {
val bundle = Bundle()
bundle.putString(BUNDLE_KEY_ID, id)
bundle.putString(BUNDLE_KEY_PASSWORD, password)
return bundle
}
@JvmStatic
internal fun toCandidateDataBundle(id: String): Bundle {
val bundle = Bundle()
bundle.putString(BUNDLE_KEY_ID, id)
return bundle
}
@JvmStatic
internal fun createFrom(data: Bundle): CreatePasswordRequest {
try {
val id = data.getString(BUNDLE_KEY_ID)
val password = data.getString(BUNDLE_KEY_PASSWORD)
return CreatePasswordRequest(id!!, password!!)
} catch (e: Exception) {
throw FrameworkClassParsingException()
}
}
}
}

View File

@@ -1,100 +0,0 @@
/*
* 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.jetpack.developer
import android.os.Bundle
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
/**
* A request to register a passkey from the user's public key credential provider.
*
* @property requestJson the privileged request in JSON format in the standard webauthn web json
* shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
* @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
* immediately when there is no available passkey registration offering instead of falling back to
* discovering remote options, and false (default) otherwise
* @throws NullPointerException If [requestJson] is null
* @throws IllegalArgumentException If [requestJson] is empty
*/
class CreatePublicKeyCredentialRequest @JvmOverloads constructor(
val requestJson: String,
@get:JvmName("preferImmediatelyAvailableCredentials")
val preferImmediatelyAvailableCredentials: Boolean = false
) : CreateCredentialRequest(
type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
credentialData = toCredentialDataBundle(requestJson, preferImmediatelyAvailableCredentials),
// The whole request data should be passed during the query phase.
candidateQueryData = toCredentialDataBundle(requestJson, preferImmediatelyAvailableCredentials),
requireSystemProvider = false,
) {
init {
require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
}
/** @hide */
companion object {
const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
"androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
internal const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST =
"androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST"
@JvmStatic
internal fun toCredentialDataBundle(
requestJson: String,
preferImmediatelyAvailableCredentials: Boolean
): Bundle {
val bundle = Bundle()
bundle.putString(BUNDLE_KEY_SUBTYPE,
BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST)
bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentials)
return bundle
}
@JvmStatic
internal fun toCandidateDataBundle(
requestJson: String,
preferImmediatelyAvailableCredentials: Boolean
): Bundle {
val bundle = Bundle()
bundle.putString(BUNDLE_KEY_SUBTYPE,
BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST)
bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentials)
return bundle
}
@Suppress("deprecation") // bundle.get() used for boolean value to prevent default
// boolean value from being returned.
@JvmStatic
internal fun createFrom(data: Bundle): CreatePublicKeyCredentialRequest {
try {
val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
val preferImmediatelyAvailableCredentials =
data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
return CreatePublicKeyCredentialRequest(requestJson!!,
(preferImmediatelyAvailableCredentials!!) as Boolean)
} catch (e: Exception) {
throw FrameworkClassParsingException()
}
}
}
}

View File

@@ -1,183 +0,0 @@
/*
* 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.jetpack.developer
import android.os.Bundle
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
/**
* A privileged request to register a passkey from the user’s public key credential provider, where
* the caller can modify the rp. Only callers with privileged permission, e.g. user’s default
* brower, caBLE, can use this. These permissions will be introduced in an upcoming release.
* TODO("Add specific permission info/annotation")
*
* @property requestJson the privileged request in JSON format in the standard webauthn web json
* shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
* @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
* immediately when there is no available passkey registration offering instead of falling back to
* discovering remote options, and false (default) otherwise
* @property relyingParty the expected true RP ID which will override the one in the [requestJson],
* where rp is defined [here](https://w3c.github.io/webauthn/#rp-id)
* @property clientDataHash a hash that is used to verify the [relyingParty] Identity
* @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash] is
* null
* @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is
* empty
*/
class CreatePublicKeyCredentialRequestPrivileged @JvmOverloads constructor(
val requestJson: String,
val relyingParty: String,
val clientDataHash: String,
@get:JvmName("preferImmediatelyAvailableCredentials")
val preferImmediatelyAvailableCredentials: Boolean = false
) : CreateCredentialRequest(
type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
credentialData = toCredentialDataBundle(
requestJson,
relyingParty,
clientDataHash,
preferImmediatelyAvailableCredentials
),
// The whole request data should be passed during the query phase.
candidateQueryData = toCredentialDataBundle(
requestJson, relyingParty, clientDataHash, preferImmediatelyAvailableCredentials
),
requireSystemProvider = false,
) {
init {
require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
require(relyingParty.isNotEmpty()) { "rp must not be empty" }
require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" }
}
/** A builder for [CreatePublicKeyCredentialRequestPrivileged]. */
class Builder(
private var requestJson: String,
private var relyingParty: String,
private var clientDataHash: String
) {
private var preferImmediatelyAvailableCredentials: Boolean = false
/**
* Sets the privileged request in JSON format.
*/
fun setRequestJson(requestJson: String): Builder {
this.requestJson = requestJson
return this
}
/**
* Sets to true if you prefer the operation to return immediately when there is no available
* passkey registration offering instead of falling back to discovering remote options, and
* false otherwise.
*
* The default value is false.
*/
@Suppress("MissingGetterMatchingBuilder")
fun setPreferImmediatelyAvailableCredentials(
preferImmediatelyAvailableCredentials: Boolean
): Builder {
this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
return this
}
/**
* Sets the expected true RP ID which will override the one in the [requestJson].
*/
fun setRelyingParty(relyingParty: String): Builder {
this.relyingParty = relyingParty
return this
}
/**
* Sets a hash that is used to verify the [relyingParty] Identity.
*/
fun setClientDataHash(clientDataHash: String): Builder {
this.clientDataHash = clientDataHash
return this
}
/** Builds a [CreatePublicKeyCredentialRequestPrivileged]. */
fun build(): CreatePublicKeyCredentialRequestPrivileged {
return CreatePublicKeyCredentialRequestPrivileged(
this.requestJson,
this.relyingParty, this.clientDataHash, this.preferImmediatelyAvailableCredentials
)
}
}
/** @hide */
companion object {
internal const val BUNDLE_KEY_RELYING_PARTY =
"androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
"androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
"androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
internal const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV =
"androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_" +
"PRIVILEGED"
@JvmStatic
internal fun toCredentialDataBundle(
requestJson: String,
relyingParty: String,
clientDataHash: String,
preferImmediatelyAvailableCredentials: Boolean
): Bundle {
val bundle = Bundle()
bundle.putString(
PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV
)
bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
bundle.putString(BUNDLE_KEY_RELYING_PARTY, relyingParty)
bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
bundle.putBoolean(
BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentials
)
return bundle
}
@Suppress("deprecation") // bundle.get() used for boolean value to prevent default
// boolean value from being returned.
@JvmStatic
internal fun createFrom(data: Bundle): CreatePublicKeyCredentialRequestPrivileged {
try {
val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
val rp = data.getString(BUNDLE_KEY_RELYING_PARTY)
val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
val preferImmediatelyAvailableCredentials =
data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
return CreatePublicKeyCredentialRequestPrivileged(
requestJson!!,
rp!!,
clientDataHash!!,
(preferImmediatelyAvailableCredentials!!) as Boolean,
)
} catch (e: Exception) {
throw FrameworkClassParsingException()
}
}
}
}

View File

@@ -1,27 +0,0 @@
/*
* 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.jetpack.developer
import android.os.Bundle
/**
* Base class for a credential with which the user consented to authenticate to the app.
*
* @property type the credential type
* @property data the credential data in the [Bundle] format.
*/
open class Credential(val type: String, val data: Bundle)

View File

@@ -1,68 +0,0 @@
/*
* 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.jetpack.developer
import android.credentials.Credential
import android.os.Bundle
/**
* Base request class for getting a registered credential.
*
* @property type the credential type
* @property data the request data in the [Bundle] format
* @property requireSystemProvider true if must only be fulfilled by a system provider and false
* otherwise
*/
open class GetCredentialOption(
open val type: String,
open val requestData: Bundle,
open val candidateQueryData: Bundle,
open val requireSystemProvider: Boolean,
) {
companion object {
@JvmStatic
fun createFrom(
type: String,
requestData: Bundle,
candidateQueryData: Bundle,
requireSystemProvider: Boolean
): GetCredentialOption {
return try {
when (type) {
Credential.TYPE_PASSWORD_CREDENTIAL ->
GetPasswordOption.createFrom(requestData)
PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
when (requestData.getString(PublicKeyCredential.BUNDLE_KEY_SUBTYPE)) {
GetPublicKeyCredentialOption
.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION ->
GetPublicKeyCredentialOption.createFrom(requestData)
GetPublicKeyCredentialOptionPrivileged
.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED ->
GetPublicKeyCredentialOptionPrivileged.createFrom(requestData)
else -> throw FrameworkClassParsingException()
}
else -> throw FrameworkClassParsingException()
}
} catch (e: FrameworkClassParsingException) {
// Parsing failed but don't crash the process. Instead just output a request with
// the raw framework values.
GetCustomCredentialOption(
type, requestData, candidateQueryData, requireSystemProvider)
}
}
}
}

View File

@@ -1,75 +0,0 @@
/*
* 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.jetpack.developer
/**
* Encapsulates a request to get a user credential.
*
* @property getCredentialOptions the list of [GetCredentialOption] from which the user can choose
* one to authenticate to the app
* @throws IllegalArgumentException If [getCredentialOptions] is empty
*/
class GetCredentialRequest constructor(
val getCredentialOptions: List<GetCredentialOption>,
) {
init {
require(getCredentialOptions.isNotEmpty()) { "credentialRequests should not be empty" }
}
/** A builder for [GetCredentialRequest]. */
class Builder {
private var getCredentialOptions: MutableList<GetCredentialOption> = mutableListOf()
/** Adds a specific type of [GetCredentialOption]. */
fun addGetCredentialOption(getCredentialOption: GetCredentialOption): Builder {
getCredentialOptions.add(getCredentialOption)
return this
}
/** Sets the list of [GetCredentialOption]. */
fun setGetCredentialOptions(getCredentialOptions: List<GetCredentialOption>): Builder {
this.getCredentialOptions = getCredentialOptions.toMutableList()
return this
}
/**
* Builds a [GetCredentialRequest].
*
* @throws IllegalArgumentException If [getCredentialOptions] is empty
*/
fun build(): GetCredentialRequest {
return GetCredentialRequest(getCredentialOptions.toList())
}
}
companion object {
@JvmStatic
fun createFrom(from: android.credentials.GetCredentialRequest): GetCredentialRequest {
return GetCredentialRequest(
from.credentialOptions.map {
GetCredentialOption.createFrom(
it.type,
it.credentialRetrievalData,
it.candidateQueryData,
it.isSystemProviderRequired()
)
}
)
}
}
}

View File

@@ -1,50 +0,0 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.credentialmanager.jetpack.developer
import android.os.Bundle
/**
* Allows extending custom versions of GetCredentialOptions for unique use cases.
*
* @property type the credential type determined by the credential-type-specific subclass
* generated for custom use cases
* @property requestData the request data in the [Bundle] format, generated for custom use cases
* @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
* the provider during the initial candidate query stage, which should not contain sensitive user
* information
* @property requireSystemProvider true if must only be fulfilled by a system provider and false
* otherwise
* @throws IllegalArgumentException If [type] is empty
* @throws NullPointerException If [requestData] or [type] is null
*/
open class GetCustomCredentialOption(
final override val type: String,
final override val requestData: Bundle,
final override val candidateQueryData: Bundle,
@get:JvmName("requireSystemProvider")
final override val requireSystemProvider: Boolean
) : GetCredentialOption(
type,
requestData,
candidateQueryData,
requireSystemProvider
) {
init {
require(type.isNotEmpty()) { "type should not be empty" }
}
}

View File

@@ -1,35 +0,0 @@
/*
* 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.jetpack.developer
import android.credentials.Credential
import android.os.Bundle
/** A request to retrieve the user's saved application password from their password provider. */
class GetPasswordOption : GetCredentialOption(
Credential.TYPE_PASSWORD_CREDENTIAL,
Bundle(),
Bundle(),
false,
) {
companion object {
@JvmStatic
fun createFrom(data: Bundle): GetPasswordOption {
return GetPasswordOption()
}
}
}

View File

@@ -1,85 +0,0 @@
/*
* 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.jetpack.developer
import android.os.Bundle
/**
* A request to get passkeys from the user's public key credential provider.
*
* @property requestJson the privileged request in JSON format in the standard webauthn web json
* shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
* @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
* immediately when there is no available credential instead of falling back to discovering remote
* credentials, and false (default) otherwise
* @throws NullPointerException If [requestJson] is null
* @throws IllegalArgumentException If [requestJson] is empty
*/
class GetPublicKeyCredentialOption @JvmOverloads constructor(
val requestJson: String,
@get:JvmName("preferImmediatelyAvailableCredentials")
val preferImmediatelyAvailableCredentials: Boolean = false,
) : GetCredentialOption(
type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
requestData = toRequestDataBundle(requestJson, preferImmediatelyAvailableCredentials),
candidateQueryData = toRequestDataBundle(requestJson, preferImmediatelyAvailableCredentials),
requireSystemProvider = false
) {
init {
require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
}
/** @hide */
companion object {
internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
"androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION =
"androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION"
@JvmStatic
internal fun toRequestDataBundle(
requestJson: String,
preferImmediatelyAvailableCredentials: Boolean
): Bundle {
val bundle = Bundle()
bundle.putString(
PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION
)
bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentials)
return bundle
}
@Suppress("deprecation") // bundle.get() used for boolean value to prevent default
// boolean value from being returned.
@JvmStatic
internal fun createFrom(data: Bundle): GetPublicKeyCredentialOption {
try {
val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
val preferImmediatelyAvailableCredentials =
data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
return GetPublicKeyCredentialOption(requestJson!!,
(preferImmediatelyAvailableCredentials!!) as Boolean)
} catch (e: Exception) {
throw FrameworkClassParsingException()
}
}
}
}

View File

@@ -1,182 +0,0 @@
/*
* 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.jetpack.developer
import android.os.Bundle
/**
* A privileged request to get passkeys from the user's public key credential provider. The caller
* can modify the RP. Only callers with privileged permission (e.g. user's public browser or caBLE)
* can use this. These permissions will be introduced in an upcoming release.
* TODO("Add specific permission info/annotation")
*
* @property requestJson the privileged request in JSON format in the standard webauthn web json
* shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
* @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
* immediately when there is no available credential instead of falling back to discovering remote
* credentials, and false (default) otherwise
* @property relyingParty the expected true RP ID which will override the one in the [requestJson],
* where relyingParty is defined [here](https://w3c.github.io/webauthn/#rp-id) in more detail
* @property clientDataHash a hash that is used to verify the [relyingParty] Identity
* @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash]
* is null
* @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is
* empty
*/
class GetPublicKeyCredentialOptionPrivileged @JvmOverloads constructor(
val requestJson: String,
val relyingParty: String,
val clientDataHash: String,
@get:JvmName("preferImmediatelyAvailableCredentials")
val preferImmediatelyAvailableCredentials: Boolean = false
) : GetCredentialOption(
type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
requestData = toBundle(
requestJson,
relyingParty,
clientDataHash,
preferImmediatelyAvailableCredentials
),
candidateQueryData = toBundle(
requestJson,
relyingParty,
clientDataHash,
preferImmediatelyAvailableCredentials
),
requireSystemProvider = false,
) {
init {
require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
require(relyingParty.isNotEmpty()) { "rp must not be empty" }
require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" }
}
/** A builder for [GetPublicKeyCredentialOptionPrivileged]. */
class Builder(
private var requestJson: String,
private var relyingParty: String,
private var clientDataHash: String
) {
private var preferImmediatelyAvailableCredentials: Boolean = false
/**
* Sets the privileged request in JSON format.
*/
fun setRequestJson(requestJson: String): Builder {
this.requestJson = requestJson
return this
}
/**
* Sets to true if you prefer the operation to return immediately when there is no available
* credential instead of falling back to discovering remote credentials, and false
* otherwise.
*
* The default value is false.
*/
@Suppress("MissingGetterMatchingBuilder")
fun setPreferImmediatelyAvailableCredentials(
preferImmediatelyAvailableCredentials: Boolean
): Builder {
this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
return this
}
/**
* Sets the expected true RP ID which will override the one in the [requestJson].
*/
fun setRelyingParty(relyingParty: String): Builder {
this.relyingParty = relyingParty
return this
}
/**
* Sets a hash that is used to verify the [relyingParty] Identity.
*/
fun setClientDataHash(clientDataHash: String): Builder {
this.clientDataHash = clientDataHash
return this
}
/** Builds a [GetPublicKeyCredentialOptionPrivileged]. */
fun build(): GetPublicKeyCredentialOptionPrivileged {
return GetPublicKeyCredentialOptionPrivileged(
this.requestJson,
this.relyingParty, this.clientDataHash, this.preferImmediatelyAvailableCredentials
)
}
}
/** @hide */
companion object {
internal const val BUNDLE_KEY_RELYING_PARTY =
"androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
"androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
"androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED =
"androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION" +
"_PRIVILEGED"
@JvmStatic
internal fun toBundle(
requestJson: String,
relyingParty: String,
clientDataHash: String,
preferImmediatelyAvailableCredentials: Boolean
): Bundle {
val bundle = Bundle()
bundle.putString(
PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED
)
bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
bundle.putString(BUNDLE_KEY_RELYING_PARTY, relyingParty)
bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
bundle.putBoolean(
BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentials
)
return bundle
}
@Suppress("deprecation") // bundle.get() used for boolean value to prevent default
// boolean value from being returned.
@JvmStatic
internal fun createFrom(data: Bundle): GetPublicKeyCredentialOptionPrivileged {
try {
val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
val rp = data.getString(BUNDLE_KEY_RELYING_PARTY)
val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
val preferImmediatelyAvailableCredentials =
data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
return GetPublicKeyCredentialOptionPrivileged(
requestJson!!,
rp!!,
clientDataHash!!,
(preferImmediatelyAvailableCredentials!!) as Boolean,
)
} catch (e: Exception) {
throw FrameworkClassParsingException()
}
}
}
}

View File

@@ -1,55 +0,0 @@
/*
* 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.jetpack.developer
import android.os.Bundle
class PasswordCredential constructor(
val id: String,
val password: String,
) : Credential(android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL, toBundle(id, password)) {
init {
require(password.isNotEmpty()) { "password should not be empty" }
}
/** @hide */
companion object {
const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID"
const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD"
@JvmStatic
internal fun toBundle(id: String, password: String): Bundle {
val bundle = Bundle()
bundle.putString(BUNDLE_KEY_ID, id)
bundle.putString(BUNDLE_KEY_PASSWORD, password)
return bundle
}
@JvmStatic
internal fun createFrom(data: Bundle): PasswordCredential {
try {
val id = data.getString(BUNDLE_KEY_ID)
val password = data.getString(BUNDLE_KEY_PASSWORD)
return PasswordCredential(id!!, password!!)
} catch (e: Exception) {
throw FrameworkClassParsingException()
}
}
}
}

View File

@@ -1,60 +0,0 @@
/*
* 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.jetpack.developer
import android.os.Bundle
/**
* Represents the user's passkey credential granted by the user for app sign-in.
*
* @property authenticationResponseJson the public key credential authentication response in
* JSON format that follows the standard webauthn json format shown at
* [this w3c link](https://w3c.github.io/webauthn/#dictdef-authenticationresponsejson)
* @throws NullPointerException If [authenticationResponseJson] is null. This is handled by the
* kotlin runtime
* @throws IllegalArgumentException If [authenticationResponseJson] is empty
*
* @hide
*/
class PublicKeyCredential constructor(
val authenticationResponseJson: String
) : Credential(
TYPE_PUBLIC_KEY_CREDENTIAL,
toBundle(authenticationResponseJson)
) {
init {
require(authenticationResponseJson.isNotEmpty()) {
"authentication response JSON must not be empty" }
}
companion object {
/** The type value for public key credential related operations. */
const val TYPE_PUBLIC_KEY_CREDENTIAL: String =
"androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
/** The Bundle key value for the public key credential subtype (privileged or regular). */
internal const val BUNDLE_KEY_SUBTYPE = "androidx.credentials.BUNDLE_KEY_SUBTYPE"
const val BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON =
"androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON"
@JvmStatic
internal fun toBundle(authenticationResponseJson: String): Bundle {
val bundle = Bundle()
bundle.putString(BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON, authenticationResponseJson)
return bundle
}
}
}

View File

@@ -1,100 +0,0 @@
/*
* 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.jetpack.provider
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.slice.Slice
import android.app.slice.SliceSpec
import android.net.Uri
import android.util.Log
import java.util.Collections
/**
* UI representation for a credential entry used during the get credential flow.
*
* TODO: move to jetpack.
*/
class Action constructor(
val title: CharSequence,
val subTitle: CharSequence?,
val pendingIntent: PendingIntent?,
) {
init {
require(title.isNotEmpty()) { "title must not be empty" }
}
companion object {
private const val TAG = "Action"
internal const val SLICE_HINT_TITLE =
"androidx.credentials.provider.action.HINT_ACTION_TITLE"
internal const val SLICE_HINT_SUBTITLE =
"androidx.credentials.provider.action.HINT_ACTION_SUBTEXT"
internal const val SLICE_HINT_PENDING_INTENT =
"androidx.credentials.provider.action.SLICE_HINT_PENDING_INTENT"
@JvmStatic
fun toSlice(action: Action): Slice {
// TODO("Put the right spec and version value")
val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))
.addText(action.title, /*subType=*/null,
listOf(SLICE_HINT_TITLE))
.addText(action.subTitle, /*subType=*/null,
listOf(SLICE_HINT_SUBTITLE))
if (action.pendingIntent != null) {
sliceBuilder.addAction(action.pendingIntent,
Slice.Builder(sliceBuilder)
.addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
.build(),
/*subType=*/null)
}
return sliceBuilder.build()
}
/**
* Returns an instance of [Action] derived from a [Slice] object.
*
* @param slice the [Slice] object constructed through [toSlice]
*/
@SuppressLint("WrongConstant") // custom conversion between jetpack and framework
@JvmStatic
fun fromSlice(slice: Slice): Action? {
// TODO("Put the right spec and version value")
var title: CharSequence = ""
var subTitle: CharSequence? = null
var pendingIntent: PendingIntent? = null
slice.items.forEach {
if (it.hasHint(SLICE_HINT_TITLE)) {
title = it.text
} else if (it.hasHint(SLICE_HINT_SUBTITLE)) {
subTitle = it.text
} else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
pendingIntent = it.action
}
}
return try {
Action(title, subTitle, pendingIntent)
} catch (e: Exception) {
Log.i(TAG, "fromSlice failed with: " + e.message)
null
}
}
}
}

View File

@@ -1,55 +0,0 @@
/*
* 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.jetpack.provider
import android.app.PendingIntent
import android.app.slice.Slice
import android.util.Log
import androidx.annotation.VisibleForTesting
/**
* UI representation for a credential entry used during the get credential flow.
*
* TODO: move to jetpack.
*/
class AuthenticationAction constructor(
val pendingIntent: PendingIntent
) {
companion object {
private const val TAG = "AuthenticationAction"
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal const val SLICE_HINT_PENDING_INTENT =
"androidx.credentials.provider.authenticationAction.SLICE_HINT_PENDING_INTENT"
@JvmStatic
fun fromSlice(slice: Slice): AuthenticationAction? {
slice.items.forEach {
if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
return try {
AuthenticationAction(it.action)
} catch (e: Exception) {
Log.i(TAG, "fromSlice failed with: " + e.message)
null
}
}
}
return null
}
}
}

View File

@@ -1,243 +0,0 @@
/*
* 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.jetpack.provider
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.slice.Slice
import android.app.slice.SliceSpec
import android.graphics.drawable.Icon
import android.net.Uri
import android.os.Bundle
import android.util.Log
import java.util.Collections
/**
* UI representation for a save entry used during the create credential flow.
*
* TODO: move to jetpack.
*/
class CreateEntry internal constructor(
val accountName: CharSequence,
val pendingIntent: PendingIntent?,
val icon: Icon?,
val lastUsedTimeMillis: Long,
val credentialCountInformationList: List<CredentialCountInformation>,
val footerDescription: CharSequence?,
) {
init {
require(accountName.isNotEmpty()) { "accountName must not be empty" }
}
/**
* A builder for [CreateEntry]
*
* @property accountName the name of the account where the credential will be registered
* @property pendingIntent the [PendingIntent] that will be fired when the user selects
* this entry
*
* @hide
*/
class Builder constructor(
private val accountName: CharSequence,
private val pendingIntent: PendingIntent? = null
) {
private var credentialCountInformationList: MutableList<CredentialCountInformation> =
mutableListOf()
private var icon: Icon? = null
private var lastUsedTimeMillis: Long = 0
private var footerDescription: CharSequence? = null
/** Adds a [CredentialCountInformation] denoting a given credential
* type and the count of credentials that the provider has stored for that
* credential type.
*
* This information will be displayed on the [CreateEntry] to help the user
* make a choice.
*/
@Suppress("MissingGetterMatchingBuilder")
fun addCredentialCountInformation(info: CredentialCountInformation): Builder {
credentialCountInformationList.add(info)
return this
}
/** Sets a list of [CredentialCountInformation]. Each item in the list denotes a given
* credential type and the count of credentials that the provider has stored of that
* credential type.
*
* This information will be displayed on the [CreateEntry] to help the user
* make a choice.
*/
fun setCredentialCountInformationList(infoList: List<CredentialCountInformation>): Builder {
credentialCountInformationList = infoList as MutableList<CredentialCountInformation>
return this
}
/** Sets an icon to be displayed with the entry on the UI */
fun setIcon(icon: Icon?): Builder {
this.icon = icon
return this
}
/** Sets the last time this account was used */
fun setLastUsedTimeMillis(lastUsedTimeMillis: Long): Builder {
this.lastUsedTimeMillis = lastUsedTimeMillis
return this
}
/** Sets the footer description of this */
fun setFooterDescription(footerDescription: CharSequence): Builder {
this.footerDescription = footerDescription
return this
}
/**
* Builds an instance of [CreateEntry]
*
* @throws IllegalArgumentException If [accountName] is empty
*/
fun build(): CreateEntry {
return CreateEntry(accountName, pendingIntent, icon, lastUsedTimeMillis,
credentialCountInformationList, footerDescription)
}
}
companion object {
private const val TAG = "CreateEntry"
internal const val SLICE_HINT_ACCOUNT_NAME =
"androidx.credentials.provider.createEntry.SLICE_HINT_USER_PROVIDER_ACCOUNT_NAME"
internal const val SLICE_HINT_ICON =
"androidx.credentials.provider.createEntry.SLICE_HINT_PROFILE_ICON"
internal const val SLICE_HINT_CREDENTIAL_COUNT_INFORMATION =
"androidx.credentials.provider.createEntry.SLICE_HINT_CREDENTIAL_COUNT_INFORMATION"
internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
"androidx.credentials.provider.createEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
internal const val SLICE_HINT_PENDING_INTENT =
"androidx.credentials.provider.createEntry.SLICE_HINT_PENDING_INTENT"
internal const val SLICE_HINT_FOOTER_DESCRIPTION =
"androidx.credentials.provider.createEntry.SLICE_HINT_FOOTER_DESCRIPTION"
@JvmStatic
fun toSlice(createEntry: CreateEntry): Slice {
// TODO("Use the right type and revision")
val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))
sliceBuilder.addText(createEntry.accountName, /*subType=*/null,
listOf(SLICE_HINT_ACCOUNT_NAME))
.addLong(createEntry.lastUsedTimeMillis, /*subType=*/null, listOf(
SLICE_HINT_LAST_USED_TIME_MILLIS))
if (createEntry.icon != null) {
sliceBuilder.addIcon(createEntry.icon, /*subType=*/null,
listOf(SLICE_HINT_ICON))
}
val credentialCountBundle = convertCredentialCountInfoToBundle(
createEntry.credentialCountInformationList)
if (credentialCountBundle != null) {
sliceBuilder.addBundle(convertCredentialCountInfoToBundle(
createEntry.credentialCountInformationList), null, listOf(
SLICE_HINT_CREDENTIAL_COUNT_INFORMATION))
}
if (createEntry.pendingIntent != null) {
sliceBuilder.addAction(createEntry.pendingIntent,
Slice.Builder(sliceBuilder)
.addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
.build(),
/*subType=*/null)
}
if (createEntry.footerDescription != null) {
sliceBuilder.addText(createEntry.footerDescription, /*subType=*/null,
listOf(SLICE_HINT_FOOTER_DESCRIPTION))
}
return sliceBuilder.build()
}
/**
* Returns an instance of [CreateEntry] derived from a [Slice] object.
*
* @param slice the [Slice] object constructed through [toSlice]
*/
@SuppressLint("WrongConstant") // custom conversion between jetpack and framework
@JvmStatic
fun fromSlice(slice: Slice): CreateEntry? {
// TODO("Put the right spec and version value")
var accountName: CharSequence = ""
var icon: Icon? = null
var pendingIntent: PendingIntent? = null
var credentialCountInfo: List<CredentialCountInformation> = listOf()
var lastUsedTimeMillis: Long = 0
var footerDescription: CharSequence? = null
slice.items.forEach {
if (it.hasHint(SLICE_HINT_ACCOUNT_NAME)) {
accountName = it.text
} else if (it.hasHint(SLICE_HINT_ICON)) {
icon = it.icon
} else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
pendingIntent = it.action
} else if (it.hasHint(SLICE_HINT_CREDENTIAL_COUNT_INFORMATION)) {
credentialCountInfo = convertBundleToCredentialCountInfo(it.bundle)
} else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
lastUsedTimeMillis = it.long
} else if (it.hasHint(SLICE_HINT_FOOTER_DESCRIPTION)) {
footerDescription = it.text
}
}
return try {
CreateEntry(accountName, pendingIntent, icon,
lastUsedTimeMillis, credentialCountInfo, footerDescription)
} catch (e: Exception) {
Log.i(TAG, "fromSlice failed with: " + e.message)
null
}
}
@JvmStatic
internal fun convertBundleToCredentialCountInfo(bundle: Bundle?):
List<CredentialCountInformation> {
val credentialCountList = ArrayList<CredentialCountInformation>()
if (bundle == null) {
return credentialCountList
}
bundle.keySet().forEach {
try {
credentialCountList.add(
CredentialCountInformation(it, bundle.getInt(it)))
} catch (e: Exception) {
Log.i(TAG, "Issue unpacking credential count info bundle: " + e.message)
}
}
return credentialCountList
}
@JvmStatic
internal fun convertCredentialCountInfoToBundle(
credentialCountInformationList: List<CredentialCountInformation>
): Bundle? {
if (credentialCountInformationList.isEmpty()) {
return null
}
val bundle = Bundle()
credentialCountInformationList.forEach {
bundle.putInt(it.type, it.count)
}
return bundle
}
}
}

View File

@@ -1,61 +0,0 @@
/*
* 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.jetpack.provider
import android.credentials.Credential
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
class CredentialCountInformation constructor(
val type: String,
val count: Int
) {
companion object {
@JvmStatic
fun createPasswordCountInformation(count: Int): CredentialCountInformation {
return CredentialCountInformation(Credential.TYPE_PASSWORD_CREDENTIAL, count)
}
@JvmStatic
fun getPasswordCount(infos: List<CredentialCountInformation>): Int? {
return getCountForType(infos, Credential.TYPE_PASSWORD_CREDENTIAL)
}
@JvmStatic
fun createPublicKeyCountInformation(count: Int): CredentialCountInformation {
return CredentialCountInformation(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL, count)
}
@JvmStatic
fun getPasskeyCount(infos: List<CredentialCountInformation>): Int? {
return getCountForType(infos, PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
}
@JvmStatic
fun createTotalCountInformation(count: Int): CredentialCountInformation {
return CredentialCountInformation("TOTAL_COUNT", count)
}
@JvmStatic
fun getTotalCount(infos: List<CredentialCountInformation>): Int? {
return getCountForType(infos, "TOTAL_COUNT")
}
private fun getCountForType(infos: List<CredentialCountInformation>, type: String): Int? {
return infos.firstOrNull { info -> info.type == type }?.count
}
}
}

View File

@@ -1,150 +0,0 @@
/*
* 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.jetpack.provider
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.slice.Slice
import android.app.slice.SliceSpec
import android.graphics.drawable.Icon
import android.net.Uri
import android.util.Log
import java.util.Collections
/**
* UI representation for a credential entry used during the get credential flow.
*
* TODO: move to jetpack.
*/
open class CredentialEntry constructor(
// TODO("Add credential type display name for both CredentialEntry & CreateEntry")
val type: String,
val typeDisplayName: CharSequence,
val username: CharSequence,
val displayName: CharSequence?,
val pendingIntent: PendingIntent?,
// TODO("Consider using Instant or other strongly typed time data type")
val lastUsedTimeMillis: Long,
val icon: Icon?,
var autoSelectAllowed: Boolean
) {
init {
require(type.isNotEmpty()) { "type must not be empty" }
require(username.isNotEmpty()) { "type must not be empty" }
}
companion object {
private const val TAG = "CredentialEntry"
internal const val SLICE_HINT_TYPE_DISPLAY_NAME =
"androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
internal const val SLICE_HINT_USERNAME =
"androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
internal const val SLICE_HINT_DISPLAYNAME =
"androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
"androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
internal const val SLICE_HINT_ICON =
"androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
internal const val SLICE_HINT_PENDING_INTENT =
"androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
internal const val SLICE_HINT_AUTO_ALLOWED =
"androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
internal const val AUTO_SELECT_TRUE_STRING = "true"
internal const val AUTO_SELECT_FALSE_STRING = "false"
@JvmStatic
internal fun toSlice(credentialEntry: CredentialEntry): Slice {
// TODO("Put the right revision value")
val autoSelectAllowed = if (credentialEntry.autoSelectAllowed) {
AUTO_SELECT_TRUE_STRING
} else {
AUTO_SELECT_FALSE_STRING
}
val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(
credentialEntry.type, 1))
.addText(credentialEntry.typeDisplayName, /*subType=*/null,
listOf(SLICE_HINT_TYPE_DISPLAY_NAME))
.addText(credentialEntry.username, /*subType=*/null,
listOf(SLICE_HINT_USERNAME))
.addText(credentialEntry.displayName, /*subType=*/null,
listOf(SLICE_HINT_DISPLAYNAME))
.addLong(credentialEntry.lastUsedTimeMillis, /*subType=*/null,
listOf(SLICE_HINT_LAST_USED_TIME_MILLIS))
.addText(autoSelectAllowed, /*subType=*/null,
listOf(SLICE_HINT_AUTO_ALLOWED))
if (credentialEntry.icon != null) {
sliceBuilder.addIcon(credentialEntry.icon, /*subType=*/null,
listOf(SLICE_HINT_ICON))
}
if (credentialEntry.pendingIntent != null) {
sliceBuilder.addAction(credentialEntry.pendingIntent,
Slice.Builder(sliceBuilder)
.addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
.build(),
/*subType=*/null)
}
return sliceBuilder.build()
}
/**
* Returns an instance of [CredentialEntry] derived from a [Slice] object.
*
* @param slice the [Slice] object constructed through [toSlice]
*/
@SuppressLint("WrongConstant") // custom conversion between jetpack and framework
@JvmStatic
fun fromSlice(slice: Slice): CredentialEntry? {
var typeDisplayName: CharSequence? = null
var username: CharSequence? = null
var displayName: CharSequence? = null
var icon: Icon? = null
var pendingIntent: PendingIntent? = null
var lastUsedTimeMillis: Long = 0
var autoSelectAllowed = false
slice.items.forEach {
if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
typeDisplayName = it.text
} else if (it.hasHint(SLICE_HINT_USERNAME)) {
username = it.text
} else if (it.hasHint(SLICE_HINT_DISPLAYNAME)) {
displayName = it.text
} else if (it.hasHint(SLICE_HINT_ICON)) {
icon = it.icon
} else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
pendingIntent = it.action
} else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
lastUsedTimeMillis = it.long
} else if (it.hasHint(SLICE_HINT_AUTO_ALLOWED)) {
val autoSelectValue = it.text
if (autoSelectValue == AUTO_SELECT_TRUE_STRING) {
autoSelectAllowed = true
}
}
}
return try {
CredentialEntry(slice.spec!!.type, typeDisplayName!!, username!!,
displayName, pendingIntent,
lastUsedTimeMillis, icon, autoSelectAllowed)
} catch (e: Exception) {
Log.i(TAG, "fromSlice failed with: " + e.message)
null
}
}
}
}