From dc9ba485d2366660027829da253a0eb8b20febf5 Mon Sep 17 00:00:00 2001 From: Daniel <danieljkim@google.com> Date: Thu, 2 Nov 2023 04:53:19 +0000 Subject: [PATCH] Refactor processing cred response A single credential request is requested from gathering multiple credential options. Then we have been processing the response as if it was a single request/response. In reality, the request is more like a batched request and the response is a batched response. By mapping provider info list to autofill id, we can produce a set of provider info list. Each provider list will be an indenpendent response of the batched response. Then we can process the provider info list for each autofill id independently, sorting them and applying inline max count logic. Test: local device testing Bug: 299321128 Change-Id: Id83b1d5e5c4f6d66da9259c3943502c3ec8a48bb --- .../autofill/CredentialAutofillService.kt | 227 ++++++++++++------ 1 file changed, 152 insertions(+), 75 deletions(-) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt index 81cbd5acaca60..d7b7cc0ef7f6a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt @@ -50,6 +50,7 @@ import androidx.credentials.provider.PublicKeyCredentialEntry import com.android.credentialmanager.GetFlowUtils import com.android.credentialmanager.getflow.CredentialEntryInfo import com.android.credentialmanager.getflow.ProviderDisplayInfo +import com.android.credentialmanager.getflow.ProviderInfo import com.android.credentialmanager.getflow.toProviderDisplayInfo import org.json.JSONObject import java.util.concurrent.Executors @@ -155,6 +156,31 @@ class CredentialAutofillService : AutofillService() { } val entryIconMap: Map<String, Icon> = getEntryToIconMap(getCredResponse.candidateProviderDataList) + val autofillIdToProvidersMap: Map<AutofillId, List<ProviderInfo>> = + mapAutofillIdToProviders(providerList) + val fillResponseBuilder = FillResponse.Builder() + var validFillResponse = false + autofillIdToProvidersMap.forEach { (autofillId, providers) -> + validFillResponse = processProvidersForAutofillId( + filLRequest, autofillId, providers, entryIconMap, fillResponseBuilder) + .or(validFillResponse) + } + if (!validFillResponse) { + return null + } + return fillResponseBuilder.build() + } + + private fun processProvidersForAutofillId( + filLRequest: FillRequest, + autofillId: AutofillId, + providerList: List<ProviderInfo>, + entryIconMap: Map<String, Icon>, + fillResponseBuilder: FillResponse.Builder + ): Boolean { + if (providerList.isEmpty()) { + return false + } var totalEntryCount = 0 providerList.forEach { provider -> totalEntryCount += provider.credentialEntryList.size @@ -169,77 +195,139 @@ class CredentialAutofillService : AutofillService() { maxItemCount = maxItemCount.coerceAtMost(inlineMaxSuggestedCount) } var i = 0 - val fillResponseBuilder = FillResponse.Builder() - var emptyFillResponse = true + var datasetAdded = false providerDisplayInfo.sortedUserNameToCredentialEntryList.forEach usernameLoop@ { val primaryEntry = it.sortedCredentialEntryList.first() - // In regular CredMan bottomsheet, only one primary entry per username is displayed. - // But since the credential requests from different fields are allocated into a single - // request for autofill, there will be duplicate primary entries, especially for - // username/pw autofill fields. These primary entries will be the same entries except - // their autofillIds will point to different autofill fields. Process all primary - // fields. - // TODO(b/307435163): Merge credential options - it.sortedCredentialEntryList.forEach entryLoop@ { credentialEntry -> - if (!isSameCredentialEntry(primaryEntry, credentialEntry)) { - // Encountering different credential entry means all the duplicate primary - // entries have been processed. - return@usernameLoop - } - val autofillId: AutofillId? = credentialEntry - .fillInIntent - ?.getParcelableExtra( - CredentialProviderService.EXTRA_AUTOFILL_ID, - AutofillId::class.java) - val pendingIntent = credentialEntry.pendingIntent - if (autofillId == null || pendingIntent == null) { - Log.e(TAG, "AutofillId or pendingIntent was missing from the entry.") - return@entryLoop - } - var inlinePresentation: InlinePresentation? = null - // Create inline presentation - if (inlinePresentationSpecs != null && i < maxItemCount) { - val spec: InlinePresentationSpec - if (i < inlinePresentationSpecsCount) { - spec = inlinePresentationSpecs[i] - } else { - spec = inlinePresentationSpecs[inlinePresentationSpecsCount - 1] - } - val sliceBuilder = InlineSuggestionUi - .newContentBuilder(pendingIntent) - .setTitle(credentialEntry.userName) - val icon: Icon = - entryIconMap[credentialEntry.entryKey + credentialEntry.entrySubkey] - ?: getDefaultIcon() - sliceBuilder.setStartIcon(icon) - inlinePresentation = InlinePresentation( - sliceBuilder.build().slice, spec, /* pinned= */ false) - } - i++ + val pendingIntent = primaryEntry.pendingIntent + if (pendingIntent == null || primaryEntry.fillInIntent == null) { + // FillInIntent will not be null because autofillId was retrieved from it. + Log.e(TAG, "PendingIntent was missing from the entry.") + return@usernameLoop + } + if (inlinePresentationSpecs == null || i >= maxItemCount) { + Log.e(TAG, "Skipping because reached the max item count.") + return@usernameLoop + } + // Create inline presentation + val spec: InlinePresentationSpec + if (i < inlinePresentationSpecsCount) { + spec = inlinePresentationSpecs[i] + } else { + spec = inlinePresentationSpecs[inlinePresentationSpecsCount - 1] + } + val sliceBuilder = InlineSuggestionUi + .newContentBuilder(pendingIntent) + .setTitle(primaryEntry.userName) + val icon: Icon = + entryIconMap[primaryEntry.entryKey + primaryEntry.entrySubkey] + ?: getDefaultIcon() + sliceBuilder.setStartIcon(icon) + val inlinePresentation = InlinePresentation( + sliceBuilder.build().slice, spec, /* pinned= */ false) + i++ - val dataSetBuilder = Dataset.Builder() - val presentationBuilder = Presentations.Builder() - if (inlinePresentation != null) { - presentationBuilder.setInlinePresentation(inlinePresentation) - } - fillResponseBuilder.addDataset( - dataSetBuilder - .setField( - autofillId, - Field.Builder().setPresentations( - presentationBuilder.build()) - .build()) - .setAuthentication(pendingIntent.intentSender) - .setAuthenticationExtras(credentialEntry.fillInIntent.extras) - .build()) - emptyFillResponse = false + val dataSetBuilder = Dataset.Builder() + val presentationBuilder = Presentations.Builder() + .setInlinePresentation(inlinePresentation) + + fillResponseBuilder.addDataset( + dataSetBuilder + .setField( + autofillId, + Field.Builder().setPresentations( + presentationBuilder.build()) + .build()) + .setAuthentication(pendingIntent.intentSender) + .setAuthenticationExtras(primaryEntry.fillInIntent.extras) + .build()) + datasetAdded = true + } + return datasetAdded + } + + /** + * Maps Autofill Id to provider list. For example, passing in a provider info + * + * ProviderInfo { + * id1, + * displayName1 + * [entry1(autofillId1), entry2(autofillId2), entry3(autofillId3)], + * ... + * } + * + * will result in + * + * { autofillId1: ProviderInfo { + * id1, + * displayName1, + * [entry1(autofillId1)], + * ... + * }, autofillId2: ProviderInfo { + * id1, + * displayName1, + * [entry2(autofillId2)], + * ... + * }, autofillId3: ProviderInfo { + * id1, + * displayName1, + * [entry3(autofillId3)], + * ... + * } + * } + */ + private fun mapAutofillIdToProviders( + providerList: List<ProviderInfo> + ): Map<AutofillId, List<ProviderInfo>> { + val autofillIdToProviders: MutableMap<AutofillId, MutableList<ProviderInfo>> = + mutableMapOf() + providerList.forEach { provider -> + val autofillIdToCredentialEntries: + MutableMap<AutofillId, MutableList<CredentialEntryInfo>> = + mapAutofillIdToCredentialEntries(provider.credentialEntryList) + autofillIdToCredentialEntries.forEach { (autofillId, entries) -> + autofillIdToProviders.getOrPut(autofillId) { mutableListOf() } + .add(copyProviderInfo(provider, entries)) } } - if (emptyFillResponse) { - return null + return autofillIdToProviders + } + + private fun mapAutofillIdToCredentialEntries( + credentialEntryList: List<CredentialEntryInfo> + ): MutableMap<AutofillId, MutableList<CredentialEntryInfo>> { + val autofillIdToCredentialEntries: + MutableMap<AutofillId, MutableList<CredentialEntryInfo>> = mutableMapOf() + credentialEntryList.forEach entryLoop@ { credentialEntry -> + val autofillId: AutofillId? = credentialEntry + .fillInIntent + ?.getParcelableExtra( + CredentialProviderService.EXTRA_AUTOFILL_ID, + AutofillId::class.java) + if (autofillId == null) { + Log.e(TAG, "AutofillId is missing from credential entry. Credential" + + " Integration might be disabled.") + return@entryLoop + } + autofillIdToCredentialEntries.getOrPut(autofillId) { mutableListOf() } + .add(credentialEntry) } - return fillResponseBuilder.build() + return autofillIdToCredentialEntries + } + + private fun copyProviderInfo( + providerInfo: ProviderInfo, + credentialList: List<CredentialEntryInfo> + ): ProviderInfo { + return ProviderInfo( + providerInfo.id, + providerInfo.icon, + providerInfo.displayName, + credentialList, + providerInfo.authenticationEntryList, + providerInfo.remoteEntry, + providerInfo.actionEntryList + ) } override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { @@ -353,15 +441,4 @@ class CredentialAutofillService : AutofillService() { } return result } - - private fun isSameCredentialEntry( - info1: CredentialEntryInfo, - info2: CredentialEntryInfo - ): Boolean { - return info1.providerId == info2.providerId && - info1.lastUsedTimeMillis == info2.lastUsedTimeMillis && - info1.credentialType == info2.credentialType && - info1.displayName == info2.displayName && - info1.userName == info2.userName - } } \ No newline at end of file -- GitLab