diff options
author | 2024-05-01 00:22:15 +0000 | |
---|---|---|
committer | 2024-05-10 22:37:40 +0000 | |
commit | 6cd52640e835d960f174ed881fb26c68e381e252 (patch) | |
tree | c1658b49707acda1c6b949761ba962fe678e6da4 /packages/CredentialManager/src | |
parent | 8e08da9f2b1d15f0cf5308c7ce4e9bb61e5975f9 (diff) |
Add action chips for Credman-Autofill inline and dropdown presentation
Bug: 337927541, 337926814
Test: manual
Change-Id: Ieef7db5d974bb8f05b1f8e059c4928f66f27d0c6
Diffstat (limited to 'packages/CredentialManager/src')
3 files changed, 369 insertions, 172 deletions
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt index 50ebdd5e3ce7..0da32bddd928 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt @@ -18,6 +18,7 @@ package com.android.credentialmanager.autofill import android.app.PendingIntent import android.app.assist.AssistStructure +import android.content.ComponentName import android.content.Context import android.credentials.CredentialManager import android.credentials.GetCredentialRequest @@ -34,6 +35,10 @@ import android.os.CancellationSignal import android.os.OutcomeReceiver import android.os.ResultReceiver import android.service.autofill.AutofillService +import com.android.credentialmanager.model.get.ProviderInfo +import androidx.core.graphics.drawable.toBitmap +import com.android.credentialmanager.model.get.ActionEntryInfo +import com.android.credentialmanager.model.EntryInfo import android.service.autofill.Dataset import android.service.autofill.Field import android.service.autofill.FillCallback @@ -63,7 +68,8 @@ import com.android.credentialmanager.getflow.ProviderDisplayInfo import com.android.credentialmanager.getflow.toProviderDisplayInfo import com.android.credentialmanager.ktx.credentialEntry import com.android.credentialmanager.model.CredentialType -import com.android.credentialmanager.model.get.CredentialEntryInfo +import java.util.ArrayList +import java.util.Objects import java.util.concurrent.Executors import org.json.JSONException import org.json.JSONObject @@ -121,8 +127,11 @@ class CredentialAutofillService : AutofillService() { val responseClientState = Bundle() responseClientState.putBoolean(WEBVIEW_REQUESTED_CREDENTIAL_KEY, false) - val getCredRequest: GetCredentialRequest? = getCredManRequest(structure, sessionId, - requestId, resultReceiver, responseClientState) + val uniqueAutofillIdsForRequest: MutableSet<AutofillId> = mutableSetOf() + val getCredRequest: GetCredentialRequest? = getCredManRequest( + structure, sessionId, + requestId, resultReceiver, responseClientState, uniqueAutofillIdsForRequest + ) // TODO(b/324635774): Use callback for validating. If the request is coming // directly from the view, there should be a corresponding callback, otherwise // we should fail fast, @@ -132,14 +141,17 @@ class CredentialAutofillService : AutofillService() { return } val credentialManager: CredentialManager = - getSystemService(Context.CREDENTIAL_SERVICE) as CredentialManager + getSystemService(Context.CREDENTIAL_SERVICE) as CredentialManager val outcome = object : OutcomeReceiver<GetCandidateCredentialsResponse, GetCandidateCredentialsException> { override fun onResult(result: GetCandidateCredentialsResponse) { Log.i(TAG, "getCandidateCredentials onResult") - val fillResponse = convertToFillResponse(result, request, - responseClientState, GetFlowUtils.extractTypePriorityMap(getCredRequest)) + val fillResponse = convertToFillResponse( + result, request, + responseClientState, GetFlowUtils.extractTypePriorityMap(getCredRequest), + uniqueAutofillIdsForRequest + ) if (fillResponse != null) { callback.onSuccess(fillResponse) } else { @@ -195,58 +207,131 @@ class CredentialAutofillService : AutofillService() { private fun convertToFillResponse( getCredResponse: GetCandidateCredentialsResponse, - filLRequest: FillRequest, + fillRequest: FillRequest, responseClientState: Bundle, typePriorityMap: Map<String, Int>, + uniqueAutofillIdsForRequest: MutableSet<AutofillId> ): FillResponse? { val candidateProviders = getCredResponse.candidateProviderDataList if (candidateProviders.isEmpty()) { return null } - + val primaryProviderComponentName = getCredResponse.primaryProviderComponentName val entryIconMap: Map<String, Icon> = getEntryToIconMap(candidateProviders) val autofillIdToProvidersMap: Map<AutofillId, ArrayList<GetCredentialProviderData>> = - mapAutofillIdToProviders(candidateProviders) + mapAutofillIdToProviders( + uniqueAutofillIdsForRequest, + candidateProviders, + primaryProviderComponentName + ) val fillResponseBuilder = FillResponse.Builder() fillResponseBuilder.setFlags(FillResponse.FLAG_CREDENTIAL_MANAGER_RESPONSE) - var validFillResponse = false autofillIdToProvidersMap.forEach { (autofillId, providers) -> - validFillResponse = processProvidersForAutofillId( - filLRequest, autofillId, providers, entryIconMap, fillResponseBuilder, - getCredResponse.intent, typePriorityMap) - .or(validFillResponse) + var credentialDatasetAdded = addCredentialDatasetsForAutofillId(fillRequest, + autofillId, providers, entryIconMap, fillResponseBuilder, typePriorityMap) + if (!credentialDatasetAdded && primaryProviderComponentName != null) { + val providerList = GetFlowUtils.toProviderList( + providers, + this@CredentialAutofillService + ) + val primaryProviderInfo = + providerList.find { provider -> primaryProviderComponentName + .flattenToString().equals(provider.id) } + if (primaryProviderInfo != null) { + addActionDatasetsForAutofillId( + fillRequest, + autofillId, + primaryProviderInfo, + fillResponseBuilder + ) + } + } } - if (!validFillResponse) { - return null + for (autofillId in uniqueAutofillIdsForRequest) { + addMoreOptionsDataset( + fillRequest, + autofillId, + fillResponseBuilder, + getCredResponse.intent.putExtra( + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, ArrayList(candidateProviders) + ) + ) } fillResponseBuilder.setClientState(responseClientState) return fillResponseBuilder.build() } - private fun processProvidersForAutofillId( - filLRequest: FillRequest, - autofillId: AutofillId, - providerDataList: ArrayList<GetCredentialProviderData>, - entryIconMap: Map<String, Icon>, - fillResponseBuilder: FillResponse.Builder, - bottomSheetIntent: Intent, - typePriorityMap: Map<String, Int>, + private fun addActionDatasetsForAutofillId( + fillRequest: FillRequest, + autofillId: AutofillId, + primaryProvider: ProviderInfo, + fillResponseBuilder: FillResponse.Builder, + ): Boolean { + var index = 0 + var datasetAdded = false + primaryProvider.actionEntryList.forEach { actionEntry -> + if (index >= maxDatasetDisplayLimit(primaryProvider.actionEntryList.size)) { + return@forEach + } + val pendingIntent = actionEntry.pendingIntent + if (pendingIntent == null) { + Log.e(TAG, "Pending intent for action chip is null") + return@forEach + } + + val icon: Icon? = Icon.createWithBitmap(actionEntry.icon.toBitmap()) + if (icon == null) { + Log.e(TAG, "Icon for action chip is null") + return@forEach + } + + val presentations = constructPresentations( + fillRequest, + index, + actionEntry, + pendingIntent, + icon, + actionEntry.title, + actionEntry.subTitle, + primaryProvider.actionEntryList.size + ) + + fillResponseBuilder.addDataset( + Dataset.Builder() + .setField( + autofillId, + Field.Builder().setPresentations(presentations).build() + ) + .setAuthentication(pendingIntent.intentSender) + .build() + ) + datasetAdded = true + + index++ + } + + return datasetAdded + } + + private fun addCredentialDatasetsForAutofillId( + fillRequest: FillRequest, + autofillId: AutofillId, + providerDataList: List<GetCredentialProviderData>, + entryIconMap: Map<String, Icon>, + fillResponseBuilder: FillResponse.Builder, + typePriorityMap: Map<String, Int>, ): Boolean { val providerList = GetFlowUtils.toProviderList( providerDataList, - this@CredentialAutofillService) + this@CredentialAutofillService + ) if (providerList.isEmpty()) { return false } val providerDisplayInfo: ProviderDisplayInfo = - toProviderDisplayInfo(providerList, typePriorityMap) + toProviderDisplayInfo(providerList, typePriorityMap) var totalEntryCount = providerDisplayInfo.sortedUserNameToCredentialEntryList.size - val inlineSuggestionsRequest = filLRequest.inlineSuggestionsRequest - val inlinePresentationSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs - val inlinePresentationSpecsCount = inlinePresentationSpecs?.size ?: 0 - val maxDatasetDisplayLimit = this.resources.getInteger( - com.android.credentialmanager.R.integer.autofill_max_visible_datasets) - .coerceAtMost(totalEntryCount) + var i = 0 var datasetAdded = false @@ -260,8 +345,6 @@ class CredentialAutofillService : AutofillService() { } } } - bottomSheetIntent.putExtra( - ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, providerDataList) providerDisplayInfo.sortedUserNameToCredentialEntryList.forEach usernameLoop@{ val primaryEntry = it.sortedCredentialEntryList.first() val pendingIntent = primaryEntry.pendingIntent @@ -271,7 +354,7 @@ class CredentialAutofillService : AutofillService() { Log.e(TAG, "PendingIntent was missing from the entry.") return@usernameLoop } - if (i >= maxDatasetDisplayLimit) { + if (i >= maxDatasetDisplayLimit(totalEntryCount)) { return@usernameLoop } val icon: Icon = if (primaryEntry.icon == null) { @@ -280,116 +363,172 @@ class CredentialAutofillService : AutofillService() { getDefaultIcon() } else { entryIconMap[primaryEntry.entryKey + primaryEntry.entrySubkey] - ?: getDefaultIcon() - } - // Create inline presentation - var inlinePresentation: InlinePresentation? = null - if (inlinePresentationSpecs != null && i < maxDatasetDisplayLimit) { - val spec: InlinePresentationSpec? = if (i < inlinePresentationSpecsCount) { - inlinePresentationSpecs[i] - } else { - inlinePresentationSpecs[inlinePresentationSpecsCount - 1] - } - if (spec != null) { - inlinePresentation = createInlinePresentation(primaryEntry, pendingIntent, icon, - InlinePresentationsFactory.modifyInlinePresentationSpec - (this@CredentialAutofillService, spec), - duplicateDisplayNamesForPasskeys) - } - } - var dropdownPresentation: RemoteViews? = null - if (i < maxDatasetDisplayLimit) { - dropdownPresentation = RemoteViewsFactory.createDropdownPresentation( - this, icon, primaryEntry, /*isFirstEntry= */ i == 0, - /*isLastEntry= */ (totalEntryCount - i == 1)) + ?: getDefaultIcon() } - - val dataSetBuilder = Dataset.Builder() - val presentationBuilder = Presentations.Builder() - if (dropdownPresentation != null) { - presentationBuilder.setMenuPresentation(dropdownPresentation) + val displayName = primaryEntry.displayName + val title: String = if (primaryEntry.credentialType == CredentialType.PASSKEY && + displayName != null + ) { + displayName + } else { + primaryEntry.userName } - if (inlinePresentation != null) { - presentationBuilder.setInlinePresentation(inlinePresentation) + val subtitle = if (primaryEntry.credentialType == + CredentialType.PASSKEY && duplicateDisplayNamesForPasskeys[title] == true + ) { + primaryEntry.userName + } else { + null } - + val presentations = + constructPresentations( + fillRequest, i, primaryEntry, pendingIntent, + icon, title, subtitle, totalEntryCount + ) fillResponseBuilder.addDataset( - dataSetBuilder - .setField( - autofillId, - Field.Builder().setPresentations( - presentationBuilder.build()) - .build()) - .setAuthentication(pendingIntent.intentSender) - .setCredentialFillInIntent(fillInIntent) - .build()) + Dataset.Builder() + .setField( + autofillId, + Field.Builder().setPresentations( + presentations + ) + .build() + ) + .setAuthentication(pendingIntent.intentSender) + .setCredentialFillInIntent(fillInIntent) + .build() + ) datasetAdded = true i++ } - val pinnedSpec = getLastInlinePresentationSpec(inlinePresentationSpecs, - inlinePresentationSpecsCount) - if (datasetAdded) { - addDropdownMoreOptionsPresentation(bottomSheetIntent, autofillId, fillResponseBuilder) - if (pinnedSpec != null) { - addPinnedInlineSuggestion(pinnedSpec, autofillId, - fillResponseBuilder, bottomSheetIntent) + return datasetAdded + } + + private fun addMoreOptionsDataset( + fillRequest: FillRequest, + autofillId: AutofillId, + fillResponseBuilder: FillResponse.Builder, + bottomSheetIntent: Intent + ) { + val inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest + val inlinePresentationSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs + val inlinePresentationSpecsCount = inlinePresentationSpecs?.size ?: 0 + val pinnedSpec = getLastInlinePresentationSpec( + inlinePresentationSpecs, + inlinePresentationSpecsCount + ) + addDropdownMoreOptionsPresentation(bottomSheetIntent, autofillId, fillResponseBuilder) + if (pinnedSpec != null) { + addPinnedInlineSuggestion( + pinnedSpec, autofillId, + fillResponseBuilder, bottomSheetIntent + ) + } + } + + private fun constructPresentations( + fillRequest: FillRequest, + index: Int, + entry: EntryInfo, + pendingIntent: PendingIntent, + icon: Icon, + title: String, + subtitle: String?, + totalEntryCount: Int + ): Presentations { + val inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest + val inlinePresentationSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs + val inlinePresentationSpecsCount = inlinePresentationSpecs?.size ?: 0 + + // Create inline presentation + var inlinePresentation: InlinePresentation? = null + if (inlinePresentationSpecs != null && index < maxDatasetDisplayLimit(totalEntryCount)) { + val spec: InlinePresentationSpec? = if (index < inlinePresentationSpecsCount) { + inlinePresentationSpecs[index] + } else { + inlinePresentationSpecs[inlinePresentationSpecsCount - 1] + } + if (spec != null) { + inlinePresentation = createInlinePresentation( + pendingIntent, icon, + InlinePresentationsFactory.modifyInlinePresentationSpec + (this@CredentialAutofillService, spec), + title, subtitle, entry is ActionEntryInfo + ) } } - return datasetAdded + var dropdownPresentation: RemoteViews? = null + if (index < maxDatasetDisplayLimit(totalEntryCount)) { + dropdownPresentation = RemoteViewsFactory.createDropdownPresentation( + this, icon, entry, /*isFirstEntry= */ index == 0, + /*isLastEntry= */ (totalEntryCount - index == 1) + ) + } + + val presentationBuilder = Presentations.Builder() + if (dropdownPresentation != null) { + presentationBuilder.setMenuPresentation(dropdownPresentation) + } + if (inlinePresentation != null) { + presentationBuilder.setInlinePresentation(inlinePresentation) + } + return presentationBuilder.build() } + private fun maxDatasetDisplayLimit(totalEntryCount: Int) = this.resources.getInteger( + com.android.credentialmanager.R.integer.autofill_max_visible_datasets + ).coerceAtMost(totalEntryCount) + private fun createInlinePresentation( - primaryEntry: CredentialEntryInfo, - pendingIntent: PendingIntent, - icon: Icon, - spec: InlinePresentationSpec, - duplicateDisplayNameForPasskeys: MutableMap<String, Boolean> + pendingIntent: PendingIntent, + icon: Icon, + spec: InlinePresentationSpec, + title: String, + subtitle: String?, + isActionEntry: Boolean ): InlinePresentation { - val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY && - primaryEntry.displayName != null) { - primaryEntry.displayName!! - } else { - primaryEntry.userName - } val sliceBuilder = InlineSuggestionUi - .newContentBuilder(pendingIntent) - .setTitle(displayName) + .newContentBuilder(pendingIntent) + .setTitle(title) icon.setTintBlendMode(BlendMode.DST) sliceBuilder.setStartIcon(icon) - if (primaryEntry.credentialType == - CredentialType.PASSKEY && duplicateDisplayNameForPasskeys[displayName] == true) { - sliceBuilder.setSubtitle(primaryEntry.userName) + if (subtitle != null && !isActionEntry) { + sliceBuilder.setSubtitle(subtitle) } return InlinePresentation( - sliceBuilder.build().slice, spec, /* pinned= */ false) + sliceBuilder.build().slice, spec, /* pinned= */ false + ) } private fun addDropdownMoreOptionsPresentation( - bottomSheetIntent: Intent, - autofillId: AutofillId, - fillResponseBuilder: FillResponse.Builder + bottomSheetIntent: Intent, + autofillId: AutofillId, + fillResponseBuilder: FillResponse.Builder ) { val presentationBuilder = Presentations.Builder() - .setMenuPresentation( - RemoteViewsFactory.createMoreSignInOptionsPresentation(this)) + .setMenuPresentation( + RemoteViewsFactory.createMoreSignInOptionsPresentation(this) + ) val pendingIntent = setUpBottomSheetPendingIntent(bottomSheetIntent) fillResponseBuilder.addDataset( - Dataset.Builder() - .setId(AutofillManager.PINNED_DATASET_ID) - .setField( - autofillId, - Field.Builder().setPresentations( - presentationBuilder.build()) - .build()) - .setAuthentication(pendingIntent.intentSender) + Dataset.Builder() + .setId(AutofillManager.PINNED_DATASET_ID) + .setField( + autofillId, + Field.Builder().setPresentations( + presentationBuilder.build() + ) .build() + ) + .setAuthentication(pendingIntent.intentSender) + .build() ) } private fun getLastInlinePresentationSpec( - inlinePresentationSpecs: List<InlinePresentationSpec>?, - inlinePresentationSpecsCount: Int + inlinePresentationSpecs: List<InlinePresentationSpec>?, + inlinePresentationSpecsCount: Int ): InlinePresentationSpec? { if (inlinePresentationSpecs != null) { return inlinePresentationSpecs[inlinePresentationSpecsCount - 1] @@ -398,40 +537,47 @@ class CredentialAutofillService : AutofillService() { } private fun addPinnedInlineSuggestion( - spec: InlinePresentationSpec, - autofillId: AutofillId, - fillResponseBuilder: FillResponse.Builder, - bottomSheetIntent: Intent + spec: InlinePresentationSpec, + autofillId: AutofillId, + fillResponseBuilder: FillResponse.Builder, + bottomSheetIntent: Intent ) { val pendingIntent = setUpBottomSheetPendingIntent(bottomSheetIntent) val dataSetBuilder = Dataset.Builder() val sliceBuilder = InlineSuggestionUi - .newContentBuilder(pendingIntent) - .setStartIcon(Icon.createWithResource(this, - com.android.credentialmanager.R.drawable.more_horiz_24px)) + .newContentBuilder(pendingIntent) + .setStartIcon( + Icon.createWithResource( + this, + com.android.credentialmanager.R.drawable.more_horiz_24px + ) + ) val presentationBuilder = Presentations.Builder() - .setInlinePresentation(InlinePresentation( - sliceBuilder.build().slice, spec, /* pinned= */ true)) + .setInlinePresentation( + InlinePresentation( + sliceBuilder.build().slice, spec, /* pinned= */ true + ) + ) fillResponseBuilder.addDataset( - dataSetBuilder - .setId(AutofillManager.PINNED_DATASET_ID) - .setField( - autofillId, - Field.Builder().setPresentations( - presentationBuilder.build() - ).build() - ) - .setAuthentication(pendingIntent.intentSender) - .build() + dataSetBuilder + .setId(AutofillManager.PINNED_DATASET_ID) + .setField( + autofillId, + Field.Builder().setPresentations( + presentationBuilder.build() + ).build() + ) + .setAuthentication(pendingIntent.intentSender) + .build() ) } private fun setUpBottomSheetPendingIntent(intent: Intent): PendingIntent { intent.setAction(java.util.UUID.randomUUID().toString()) return PendingIntent.getActivity(this, /*requestCode=*/0, intent, - PendingIntent.FLAG_MUTABLE, /*options=*/null) + PendingIntent.FLAG_MUTABLE, /*options=*/null) } /** @@ -465,17 +611,33 @@ class CredentialAutofillService : AutofillService() { * } */ private fun mapAutofillIdToProviders( - providerList: List<GetCredentialProviderData> + uniqueAutofillIdsForRequest: Set<AutofillId>, + providerList: List<GetCredentialProviderData>, + primaryProviderComponentName: ComponentName? ): Map<AutofillId, ArrayList<GetCredentialProviderData>> { val autofillIdToProviders: MutableMap<AutofillId, ArrayList<GetCredentialProviderData>> = mutableMapOf() + var primaryProvider: GetCredentialProviderData? = null providerList.forEach { provider -> + if (primaryProviderComponentName != null && Objects.equals(ComponentName + .unflattenFromString(provider + .providerFlattenedComponentName), primaryProviderComponentName)) { + primaryProvider = provider + } val autofillIdToCredentialEntries: MutableMap<AutofillId, ArrayList<Entry>> = mapAutofillIdToCredentialEntries(provider.credentialEntries) autofillIdToCredentialEntries.forEach { (autofillId, entries) -> autofillIdToProviders.getOrPut(autofillId) { ArrayList() } - .add(copyProviderInfo(provider, entries)) + .add(copyProviderInfo(provider, entries)) + } + } + // adds primary provider action entries for autofill IDs without credential entries + uniqueAutofillIdsForRequest.forEach { autofillId -> + if (!autofillIdToProviders.containsKey(autofillId) && primaryProvider != null) { + autofillIdToProviders.put( + autofillId, + ArrayList(listOf(copyProviderInfoForActionsOnly(primaryProvider!!)))) } } return autofillIdToProviders @@ -526,19 +688,35 @@ class CredentialAutofillService : AutofillService() { ) } + private fun copyProviderInfoForActionsOnly( + providerInfo: GetCredentialProviderData, + ): GetCredentialProviderData { + return GetCredentialProviderData( + providerInfo.providerFlattenedComponentName, + emptyList(), + providerInfo.actionChips, + emptyList(), + null + ) + } + override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { TODO("Not yet implemented") } private fun getCredManRequest( - structure: AssistStructure, - sessionId: Int, - requestId: Int, - resultReceiver: ResultReceiver, - responseClientState: Bundle + structure: AssistStructure, + sessionId: Int, + requestId: Int, + resultReceiver: ResultReceiver, + responseClientState: Bundle, + uniqueAutofillIdsForRequest: MutableSet<AutofillId> ): GetCredentialRequest? { val credentialOptions: MutableList<CredentialOption> = mutableListOf() - traverseStructureForRequest(structure, credentialOptions, responseClientState, sessionId) + traverseStructureForRequest( + structure, credentialOptions, responseClientState, + sessionId, uniqueAutofillIdsForRequest + ) if (credentialOptions.isNotEmpty()) { val dataBundle = Bundle() @@ -558,7 +736,8 @@ class CredentialAutofillService : AutofillService() { structure: AssistStructure, cmRequests: MutableList<CredentialOption>, responseClientState: Bundle, - sessionId: Int + sessionId: Int, + uniqueAutofillIdsForRequest: MutableSet<AutofillId> ) { val traversedViewNodes: MutableSet<AutofillId> = mutableSetOf() val credentialOptionsFromHints: MutableMap<String, CredentialOption> = mutableMapOf() @@ -570,7 +749,7 @@ class CredentialAutofillService : AutofillService() { windowNodes.forEach { windowNode: AssistStructure.WindowNode -> traverseNodeForRequest( windowNode.rootViewNode, cmRequests, responseClientState, traversedViewNodes, - credentialOptionsFromHints, sessionId) + credentialOptionsFromHints, sessionId, uniqueAutofillIdsForRequest) } } @@ -580,7 +759,8 @@ class CredentialAutofillService : AutofillService() { responseClientState: Bundle, traversedViewNodes: MutableSet<AutofillId>, credentialOptionsFromHints: MutableMap<String, CredentialOption>, - sessionId: Int + sessionId: Int, + uniqueAutofillIdsForRequest: MutableSet<AutofillId> ) { viewNode.autofillId?.let { val domain = viewNode.webDomain @@ -590,7 +770,9 @@ class CredentialAutofillService : AutofillService() { WEBVIEW_REQUESTED_CREDENTIAL_KEY, true) } cmRequests.addAll(getCredentialOptionsFromViewNode(viewNode, it, responseClientState, - traversedViewNodes, credentialOptionsFromHints, sessionId)) + traversedViewNodes, credentialOptionsFromHints, sessionId, + uniqueAutofillIdsForRequest) + ) traversedViewNodes.add(it) } @@ -600,8 +782,10 @@ class CredentialAutofillService : AutofillService() { } children.forEach { childNode: AssistStructure.ViewNode -> - traverseNodeForRequest(childNode, cmRequests, responseClientState, traversedViewNodes, - credentialOptionsFromHints, sessionId) + traverseNodeForRequest( + childNode, cmRequests, responseClientState, traversedViewNodes, + credentialOptionsFromHints, sessionId, uniqueAutofillIdsForRequest + ) } } @@ -611,7 +795,8 @@ class CredentialAutofillService : AutofillService() { responseClientState: Bundle, traversedViewNodes: MutableSet<AutofillId>, credentialOptionsFromHints: MutableMap<String, CredentialOption>, - sessionId: Int + sessionId: Int, + uniqueAutofillIdsForRequest: MutableSet<AutofillId> ): MutableList<CredentialOption> { val credentialOptions: MutableList<CredentialOption> = mutableListOf() if (Flags.autofillCredmanDevIntegration() && viewNode.pendingCredentialRequest != null) { @@ -641,8 +826,9 @@ class CredentialAutofillService : AutofillService() { CredentialProviderService.EXTRA_AUTOFILL_ID, associatedAutofillIds ) + uniqueAutofillIdsForRequest.addAll(associatedAutofillIds) } - } + } } // TODO(b/325502552): clean up cred option logic in autofill hint val credentialHints: MutableList<String> = mutableListOf() diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt index 7bb08d2c26e8..98e1690ace92 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt @@ -21,8 +21,9 @@ import android.util.Log import com.android.credentialmanager.common.Constants import android.widget.RemoteViews import androidx.core.content.ContextCompat +import com.android.credentialmanager.model.get.ActionEntryInfo import com.android.credentialmanager.model.get.CredentialEntryInfo -import com.android.credentialmanager.model.CredentialType +import com.android.credentialmanager.model.EntryInfo import android.graphics.drawable.Icon class RemoteViewsFactory { @@ -39,37 +40,47 @@ class RemoteViewsFactory { fun createDropdownPresentation( context: Context, icon: Icon, - credentialEntryInfo: CredentialEntryInfo, + entryInfo: EntryInfo, isFirstEntry: Boolean, isLastEntry: Boolean, ): RemoteViews { var layoutId: Int = com.android.credentialmanager.R.layout .credman_dropdown_presentation_layout val remoteViews = RemoteViews(context.packageName, layoutId) - val displayName = credentialEntryInfo.displayName ?: credentialEntryInfo.userName - remoteViews.setTextViewText(android.R.id.text1, displayName) - val secondaryText = getSecondaryText(credentialEntryInfo) - if (secondaryText.isNullOrBlank()) { - Log.w(Constants.LOG_TAG, "Secondary text for dropdown is null") - } else { - remoteViews.setTextViewText(android.R.id.text2, secondaryText) + if (entryInfo is CredentialEntryInfo) { + val displayName = entryInfo.displayName ?: entryInfo.userName + remoteViews.setTextViewText(android.R.id.text1, displayName) + val secondaryText = getSecondaryText(entryInfo) + if (secondaryText.isNullOrBlank()) { + Log.w(Constants.LOG_TAG, "Secondary text for dropdown credential entry is null") + } else { + remoteViews.setTextViewText(android.R.id.text2, secondaryText) + } + remoteViews.setContentDescription( + android.R.id.icon1, entryInfo + .providerDisplayName + ) + } else if (entryInfo is ActionEntryInfo) { + remoteViews.setTextViewText(android.R.id.text1, entryInfo.title) + remoteViews.setTextViewText(android.R.id.text2, entryInfo.subTitle) } - remoteViews.setImageViewIcon(android.R.id.icon1, icon); + remoteViews.setImageViewIcon(android.R.id.icon1, icon) remoteViews.setBoolean( - android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true); + android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true + ) remoteViews.setInt( android.R.id.icon1, SET_MAX_HEIGHT_METHOD_NAME, context.resources.getDimensionPixelSize( - com.android.credentialmanager.R.dimen.autofill_icon_size)); - remoteViews.setContentDescription(android.R.id.icon1, credentialEntryInfo - .providerDisplayName); + com.android.credentialmanager.R.dimen.autofill_icon_size + ) + ) val drawableId = if (isFirstEntry) com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one else com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_middle remoteViews.setInt( - android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId); + android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId) if (isFirstEntry) remoteViews.setViewPadding( com.android.credentialmanager.R.id.credential_card, /* left=*/0, @@ -94,8 +105,8 @@ class RemoteViewsFactory { * providerDisplayName. Both credential type and provider display name should not be empty. */ private fun getSecondaryText(credentialEntryInfo: CredentialEntryInfo): String? { - return listOf(if (credentialEntryInfo.displayName != null - && (credentialEntryInfo.displayName != credentialEntryInfo.userName)) + return listOf(if (credentialEntryInfo.displayName != null && + (credentialEntryInfo.displayName != credentialEntryInfo.userName)) (credentialEntryInfo.userName) else null, credentialEntryInfo.credentialTypeDisplayName, credentialEntryInfo.providerDisplayName).filterNot { it.isNullOrBlank() } @@ -113,16 +124,16 @@ class RemoteViewsFactory { com.android.credentialmanager .R.string.dropdown_presentation_more_sign_in_options_text)) remoteViews.setBoolean( - android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true); + android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true) remoteViews.setInt( android.R.id.icon1, SET_MAX_HEIGHT_METHOD_NAME, context.resources.getDimensionPixelSize( - com.android.credentialmanager.R.dimen.autofill_icon_size)); + com.android.credentialmanager.R.dimen.autofill_icon_size)) val drawableId = com.android.credentialmanager.R.drawable.more_options_list_item remoteViews.setInt( - android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId); + android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId) return remoteViews } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index 8e7886119a34..19f5a99f46fa 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -292,12 +292,12 @@ private fun toGetScreenState( providerDisplayInfo.remoteEntry == null && providerDisplayInfo.authenticationEntryList.all { it.isUnlockedAndEmpty }) GetScreenState.UNLOCKED_AUTH_ENTRIES_ONLY + else if (isRequestForAllOptions) + GetScreenState.ALL_SIGN_IN_OPTIONS_ONLY else if (providerDisplayInfo.sortedUserNameToCredentialEntryList.isEmpty() && providerDisplayInfo.authenticationEntryList.isEmpty() && providerDisplayInfo.remoteEntry != null) GetScreenState.REMOTE_ONLY - else if (isRequestForAllOptions) - GetScreenState.ALL_SIGN_IN_OPTIONS_ONLY else if (isBiometricFlow(providerDisplayInfo, isFlowAutoSelectable(providerDisplayInfo))) GetScreenState.BIOMETRIC_SELECTION else GetScreenState.PRIMARY_SELECTION |