diff options
10 files changed, 720 insertions, 363 deletions
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 37453c9bc8d4..d7ce5325d22d 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -47,6 +47,10 @@ import com.android.credentialmanager.getflow.GetCredentialUiState import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toBundle 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 // Consider repo per screen, similar to view model? class CredentialManagerRepo( @@ -63,7 +67,7 @@ class CredentialManagerRepo( requestInfo = intent.extras?.getParcelable( RequestInfo.EXTRA_REQUEST_INFO, RequestInfo::class.java - ) ?: testCreatePasskeyRequestInfo() + ) ?: testGetRequestInfo() providerEnabledList = when (requestInfo.type) { RequestInfo.TYPE_CREATE -> @@ -258,137 +262,108 @@ class CredentialManagerRepo( ) } - private fun newActionEntry( - key: String, - subkey: String, - credentialType: String, - text: String, - subtext: String? = null, - ): Entry { - val slice = Slice.Builder( - Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1) - ).addText( - text, null, listOf(Entry.HINT_ACTION_TITLE) - ) - if (subtext != null) { - slice.addText(subtext, null, listOf(Entry.HINT_ACTION_SUBTEXT)) + 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) + ) } - return Entry( - key, - subkey, - slice.build() - ) - } - private fun newAuthenticationEntry( - key: String, - subkey: String, - credentialType: String, - ): Entry { - val slice = Slice.Builder( - Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1) - ) - return Entry( - key, - subkey, - slice.build() - ) - } + private fun newAuthenticationEntry( + key: String, + subkey: String, + credentialType: String, + ): Entry { + val slice = Slice.Builder( + Entry.CREDENTIAL_MANAGER_ENTRY_URI, 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") + 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 pendingIntent = PendingIntent.getActivity(context, 1, + intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + or PendingIntent.FLAG_ONE_SHOT)) - val slice = Slice.Builder( - Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1) - ).addText( - credentialTypeDisplayName, null, listOf(Entry.HINT_CREDENTIAL_TYPE_DISPLAY_NAME) - ).addText( - userName, null, listOf(Entry.HINT_USER_NAME) - ).addIcon( - Icon.createWithResource(context, R.drawable.ic_passkey), - null, - listOf(Entry.HINT_PROFILE_ICON)) - if (userDisplayName != null) { - slice.addText(userDisplayName, null, listOf(Entry.HINT_PASSKEY_USER_DISPLAY_NAME)) - } - if (lastUsedTimeMillis != null) { - slice.addLong(lastUsedTimeMillis, null, listOf(Entry.HINT_LAST_USED_TIME_MILLIS)) + val credentialEntry = CredentialEntry(credentialType, credentialTypeDisplayName, userName, + userDisplayName, pendingIntent, lastUsedTimeMillis + ?: 0L, Icon.createWithResource(context, R.drawable.ic_passkey), + false) + + return Entry( + key, + subkey, + CredentialEntry.toSlice(credentialEntry), + pendingIntent, + null + ) } - return Entry( - key, - subkey, - slice.build(), - pendingIntent, - null - ) - } - private fun newCreateEntry( - key: String, - subkey: String, - providerDisplayName: String, - passwordCount: Int, - passkeyCount: Int, - totalCredentialCount: Int, - 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 createPasswordRequest = android.service.credentials.CreateCredentialRequest( - android.service.credentials.CallingAppInfo( - context.applicationInfo.packageName, ArraySet<Signature>()), - TYPE_PASSWORD_CREDENTIAL, - toBundle("beckett-bakert@gmail.com", "password123") - ) - val fillInIntent = Intent().putExtra(CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST, - createPasswordRequest) + private fun newCreateEntry( + key: String, + subkey: String, + providerDisplayName: String, + passwordCount: Int, + passkeyCount: Int, + totalCredentialCount: Int, + 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 createPasswordRequest = android.service.credentials.CreateCredentialRequest( + android.service.credentials.CallingAppInfo( + context.applicationInfo.packageName, ArraySet<Signature>()), + TYPE_PASSWORD_CREDENTIAL, + toBundle("beckett-bakert@gmail.com", "password123") + ) + val fillInIntent = Intent().putExtra( + CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST, + createPasswordRequest) - val slice = Slice.Builder( - Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(Entry.VERSION, 1) - ).addText( - providerDisplayName, null, listOf(Entry.HINT_USER_PROVIDER_ACCOUNT_NAME)) - .addIcon( - Icon.createWithResource(context, R.drawable.ic_passkey), - null, - listOf(Entry.HINT_CREDENTIAL_TYPE_ICON)) - .addIcon( - Icon.createWithResource(context, R.drawable.ic_profile), - null, - listOf(Entry.HINT_PROFILE_ICON)) - .addInt( - passwordCount, null, listOf(Entry.HINT_PASSWORD_COUNT)) - .addInt( - passkeyCount, null, listOf(Entry.HINT_PASSKEY_COUNT)) - .addInt( - totalCredentialCount, null, listOf(Entry.HINT_TOTAL_CREDENTIAL_COUNT)) - .addLong(lastUsedTimeMillis, null, listOf(Entry.HINT_LAST_USED_TIME_MILLIS)) - .build() - return Entry( - key, - subkey, - slice, - pendingIntent, - fillInIntent, - ) - } + val createEntry = CreateEntry( + providerDisplayName, pendingIntent, + Icon.createWithResource(context, R.drawable.ic_profile), lastUsedTimeMillis, + listOf( + CredentialCountInformation.createPasswordCountInformation(passwordCount), + CredentialCountInformation.createPublicKeyCountInformation(passkeyCount), + )) + return Entry( + key, + subkey, + CreateEntry.toSlice(createEntry), + pendingIntent, + fillInIntent, + ) + } private fun newRemoteEntry( key: String, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index db676b297c1b..22b2be9fc0d0 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -42,9 +42,10 @@ 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.ActionUi -import com.android.credentialmanager.jetpack.provider.CredentialEntryUi -import com.android.credentialmanager.jetpack.provider.SaveEntryUi +import com.android.credentialmanager.jetpack.provider.Action +import com.android.credentialmanager.jetpack.provider.CredentialCountInformation +import com.android.credentialmanager.jetpack.provider.CredentialEntry +import com.android.credentialmanager.jetpack.provider.CreateEntry import org.json.JSONObject /** Utility functions for converting CredentialManager data structures to or from UI formats. */ @@ -107,7 +108,8 @@ class GetFlowUtils { context: Context, ): List<CredentialEntryInfo> { return credentialEntries.map { - val credentialEntryUi = CredentialEntryUi.fromSlice(it.slice) + // TODO: handle NPE gracefully + val credentialEntry = CredentialEntry.fromSlice(it.slice)!! // Consider directly move the UI object into the class. return@map CredentialEntryInfo( @@ -116,14 +118,14 @@ class GetFlowUtils { entrySubkey = it.subkey, pendingIntent = it.pendingIntent, fillInIntent = it.frameworkExtrasIntent, - credentialType = credentialEntryUi.credentialType.toString(), - credentialTypeDisplayName = credentialEntryUi.credentialTypeDisplayName.toString(), - userName = credentialEntryUi.userName.toString(), - displayName = credentialEntryUi.userDisplayName?.toString(), + credentialType = credentialEntry.type.toString(), + credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(), + userName = credentialEntry.username.toString(), + displayName = credentialEntry.displayName?.toString(), // TODO: proper fallback - icon = credentialEntryUi.entryIcon?.loadDrawable(context) - ?: context.getDrawable(R.drawable.ic_other_sign_in)!!, - lastUsedTimeMillis = credentialEntryUi.lastUsedTimeMillis, + icon = credentialEntry.icon?.loadDrawable(context) + ?: context.getDrawable(R.drawable.ic_other_sign_in)!!, + lastUsedTimeMillis = credentialEntry.lastUsedTimeMillis, ) } } @@ -170,7 +172,8 @@ class GetFlowUtils { providerIcon: Drawable, ): List<ActionEntryInfo> { return actionEntries.map { - val actionEntryUi = ActionUi.fromSlice(it.slice) + // TODO: handle NPE gracefully + val actionEntryUi = Action.fromSlice(it.slice)!! return@map ActionEntryInfo( providerId = providerId, @@ -178,10 +181,10 @@ class GetFlowUtils { entrySubkey = it.subkey, pendingIntent = it.pendingIntent, fillInIntent = it.frameworkExtrasIntent, - title = actionEntryUi.text.toString(), + title = actionEntryUi.title.toString(), // TODO: gracefully fail icon = providerIcon, - subTitle = actionEntryUi.subtext?.toString(), + subTitle = actionEntryUi.subTitle?.toString(), ) } } @@ -383,7 +386,8 @@ class CreateFlowUtils { context: Context, ): List<CreateOptionInfo> { return creationEntries.map { - val saveEntryUi = SaveEntryUi.fromSlice(it.slice) + // TODO: handle NPE gracefully + val createEntry = CreateEntry.fromSlice(it.slice)!! return@map CreateOptionInfo( // TODO: remove fallbacks @@ -392,13 +396,16 @@ class CreateFlowUtils { entrySubkey = it.subkey, pendingIntent = it.pendingIntent, fillInIntent = it.frameworkExtrasIntent, - userProviderDisplayName = saveEntryUi.userProviderAccountName as String, - profileIcon = saveEntryUi.profileIcon?.loadDrawable(context) - ?: requestDisplayInfo.typeIcon, - passwordCount = saveEntryUi.passwordCount ?: 0, - passkeyCount = saveEntryUi.passkeyCount ?: 0, - totalCredentialCount = saveEntryUi.totalCredentialCount ?: 0, - lastUsedTimeMillis = saveEntryUi.lastUsedTimeMillis ?: 0, + userProviderDisplayName = createEntry.accountName.toString(), + profileIcon = createEntry.icon?.loadDrawable(context) + ?: requestDisplayInfo.typeIcon, + passwordCount = CredentialCountInformation.getPasswordCount( + createEntry.credentialCountInformationList) ?: 0, + passkeyCount = CredentialCountInformation.getPasskeyCount( + createEntry.credentialCountInformationList) ?: 0, + totalCredentialCount = CredentialCountInformation.getTotalCount( + createEntry.credentialCountInformationList) ?: 0, + lastUsedTimeMillis = createEntry.lastUsedTimeMillis ?: 0, ) } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PasswordCredential.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PasswordCredential.kt new file mode 100644 index 000000000000..165885843a35 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PasswordCredential.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.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() + } + } + } +}
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/Action.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/Action.kt new file mode 100644 index 000000000000..1abf9113845f --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/Action.kt @@ -0,0 +1,100 @@ +/* + * 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 + } + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/ActionUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/ActionUi.kt deleted file mode 100644 index 19c5c2dfa4fe..000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/ActionUi.kt +++ /dev/null @@ -1,48 +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.slice.Slice -import android.credentials.ui.Entry - -/** - * UI representation for a credential entry used during the get credential flow. - * - * TODO: move to jetpack. - */ -class ActionUi( - val text: CharSequence, - val subtext: CharSequence?, -) { - companion object { - fun fromSlice(slice: Slice): ActionUi { - var text: CharSequence? = null - var subtext: CharSequence? = null - - val items = slice.items - items.forEach { - if (it.hasHint(Entry.HINT_ACTION_TITLE)) { - text = it.text - } else if (it.hasHint(Entry.HINT_ACTION_SUBTEXT)) { - subtext = it.text - } - } - // TODO: fail NPE more elegantly. - return ActionUi(text!!, subtext) - } - } -} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CreateEntry.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CreateEntry.kt new file mode 100644 index 000000000000..bed02f8e22a4 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CreateEntry.kt @@ -0,0 +1,226 @@ +/* + * 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> +) { + + 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 + + /** 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 + } + + /** + * Builds an instance of [CreateEntry] + * + * @throws IllegalArgumentException If [accountName] is empty + */ + fun build(): CreateEntry { + return CreateEntry(accountName, pendingIntent, icon, lastUsedTimeMillis, + credentialCountInformationList) + } + } + + 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" + + @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) + } + 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 + + 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 + } + } + + return try { + CreateEntry(accountName, pendingIntent, icon, + lastUsedTimeMillis, credentialCountInfo) + } 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 + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialCountInformation.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialCountInformation.kt new file mode 100644 index 000000000000..aa77b74ce76a --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialCountInformation.kt @@ -0,0 +1,61 @@ +/* + * 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 + } + } +}
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntry.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntry.kt new file mode 100644 index 000000000000..61a104bd8e3a --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntry.kt @@ -0,0 +1,150 @@ +/* + * 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 + } + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntryUi.kt deleted file mode 100644 index 47b5af0d7fe6..000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntryUi.kt +++ /dev/null @@ -1,89 +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.graphics.drawable.Icon - -/** - * UI representation for a credential entry used during the get credential flow. - * - * TODO: move to jetpack. - */ -class CredentialEntryUi( - val credentialType: CharSequence, - val credentialTypeDisplayName: CharSequence, - val userName: CharSequence, - val userDisplayName: CharSequence?, - val entryIcon: Icon?, - val lastUsedTimeMillis: Long?, - // TODO: Remove note - val note: CharSequence?, -) { - companion object { - // Copied over from jetpack - const val SLICE_HINT_TYPE_DISPLAY_NAME = - "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME" - const val SLICE_HINT_USERNAME = - "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME" - const val SLICE_HINT_DISPLAYNAME = - "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME" - const val SLICE_HINT_LAST_USED_TIME_MILLIS = - "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS" - const val SLICE_HINT_ICON = - "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON" - const val SLICE_HINT_PENDING_INTENT = - "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT" - - /** - * Returns an instance of [CredentialEntryUi] derived from a [Slice] object. - * - * @param slice the [Slice] object constructed through jetpack library - */ - @JvmStatic - fun fromSlice(slice: Slice): CredentialEntryUi { - var username: CharSequence? = null - var displayName: CharSequence = "" - var icon: Icon? = null - var pendingIntent: PendingIntent? = null - var lastUsedTimeMillis: Long = 0 - var note: CharSequence? = null - var typeDisplayName: CharSequence = "" - - 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 - } - } - return CredentialEntryUi( - slice.spec!!.type, typeDisplayName, username!!, displayName, icon, - lastUsedTimeMillis, note, - ) - } - } -} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/SaveEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/SaveEntryUi.kt deleted file mode 100644 index 313f0f97e246..000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/SaveEntryUi.kt +++ /dev/null @@ -1,80 +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.graphics.drawable.Icon - -/** - * UI representation for a save entry used during the create credential flow. - * - * TODO: move to jetpack. - */ -class SaveEntryUi( - val userProviderAccountName: CharSequence?, - val credentialTypeIcon: Icon?, - val profileIcon: Icon?, - val passwordCount: Int?, - val passkeyCount: Int?, - val totalCredentialCount: Int?, - val lastUsedTimeMillis: Long?, -) { - companion object { - const val SLICE_HINT_ACCOUNT_NAME = - "androidx.credentials.provider.createEntry.SLICE_HINT_USER_PROVIDER_ACCOUNT_NAME" - const val SLICE_HINT_ICON = - "androidx.credentials.provider.createEntry.SLICE_HINT_PROFILE_ICON" - const val SLICE_HINT_CREDENTIAL_COUNT_INFORMATION = - "androidx.credentials.provider.createEntry.SLICE_HINT_CREDENTIAL_COUNT_INFORMATION" - const val SLICE_HINT_LAST_USED_TIME_MILLIS = - "androidx.credentials.provider.createEntry.SLICE_HINT_LAST_USED_TIME_MILLIS" - const val SLICE_HINT_PENDING_INTENT = - "androidx.credentials.provider.createEntry.SLICE_HINT_PENDING_INTENT" - - /** - * Returns an instance of [SaveEntryUi] derived from a [Slice] object. - * - * @param slice the [Slice] object constructed through the jetpack library - */ - @JvmStatic - fun fromSlice(slice: Slice): SaveEntryUi { - var accountName: CharSequence? = null - var icon: Icon? = null - var pendingIntent: PendingIntent? = null - var lastUsedTimeMillis: Long = 0 - - 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_LAST_USED_TIME_MILLIS)) { - lastUsedTimeMillis = it.long - } - } - - return SaveEntryUi( - // TODO: Add count parsing - accountName!!, icon, icon, - 0, 0, 0, lastUsedTimeMillis, - ) - } - } -} |