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:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user