diff options
| author | 2024-01-18 00:03:50 +0000 | |
|---|---|---|
| committer | 2024-01-24 21:40:48 +0000 | |
| commit | 053de9dd21cde01594e0974e182c4849b81f4e3e (patch) | |
| tree | a8601276576f731301fb90241c6a319d0177d326 | |
| parent | cb1f505968cf66c70c9a2f0b9ee3a3da552f4f4f (diff) | |
Credman UI/UX - Add "Sign-In options" to dropdown presentation
Bug: 319276221
Change-Id: Ida33c8b65fbfd9f88a9bb8cc7a3b3c00938e36e0
9 files changed, 213 insertions, 49 deletions
diff --git a/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml b/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml index 5becc86927d2..f13402c7206d 100644 --- a/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml +++ b/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml @@ -23,7 +23,7 @@ android:shape="rectangle" android:top="1dp"> <shape> - <corners android:radius="16dp" /> + <corners android:radius="4dp" /> <solid android:color="@color/dropdown_container" /> </shape> </item> diff --git a/packages/CredentialManager/res/drawable/more_options_list_item.xml b/packages/CredentialManager/res/drawable/more_options_list_item.xml new file mode 100644 index 000000000000..d7b509ee48fd --- /dev/null +++ b/packages/CredentialManager/res/drawable/more_options_list_item.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 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. + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" tools:ignore="NewApi" + android:color="@android:color/transparent"> + <item + android:bottom="1dp" + android:shape="rectangle" + android:top="1dp"> + <shape> + <corners android:bottomLeftRadius="4dp" + android:bottomRightRadius="4dp"/> + <solid android:color="@color/sign_in_options_container" /> + </shape> + </item> +</ripple>
\ No newline at end of file diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml new file mode 100644 index 000000000000..929756cdf9cc --- /dev/null +++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml @@ -0,0 +1,42 @@ +<!-- + ~ Copyright (C) 2024 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. + --> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/content" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin" + android:elevation="3dp"> + + <ImageView + android:id="@android:id/icon1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_alignParentStart="true" + android:contentDescription="@string/provider_icon_content_description" + android:background="@null"/> + <TextView + android:id="@android:id/text1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_toEndOf="@android:id/icon1" + android:minWidth="@dimen/autofill_dropdown_textview_min_width" + android:maxWidth="@dimen/autofill_dropdown_textview_max_width" + style="@style/autofill.TextTitle"/> + +</RelativeLayout> diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml index cb6c6b473244..1fe5e0ed41f9 100644 --- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml +++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml @@ -17,22 +17,25 @@ android:id="@android:id/content" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:maxWidth="@dimen/autofill_dropdown_layout_width" + android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin" android:elevation="3dp"> <ImageView android:id="@android:id/icon1" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:contentDescription="@string/provider_icon_content_description" android:layout_centerVertical="true" android:layout_alignParentStart="true" android:background="@null"/> <TextView android:id="@android:id/text1" - android:layout_width="@dimen/autofill_dropdown_text_width" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_toEndOf="@android:id/icon1" + android:minWidth="@dimen/autofill_dropdown_textview_min_width" + android:maxWidth="@dimen/autofill_dropdown_textview_max_width" style="@style/autofill.TextTitle"/> <TextView android:id="@android:id/text2" @@ -40,6 +43,8 @@ android:layout_height="wrap_content" android:layout_below="@android:id/text1" android:layout_toEndOf="@android:id/icon1" + android:minWidth="@dimen/autofill_dropdown_textview_min_width" + android:maxWidth="@dimen/autofill_dropdown_textview_max_width" style="@style/autofill.TextSubtitle"/> </RelativeLayout> diff --git a/packages/CredentialManager/res/values/colors.xml b/packages/CredentialManager/res/values/colors.xml index dcb7ef9c3ed8..7cb1d01972b7 100644 --- a/packages/CredentialManager/res/values/colors.xml +++ b/packages/CredentialManager/res/values/colors.xml @@ -20,4 +20,6 @@ <color name="text_primary">#1A1B20</color> <color name="text_secondary">#44474F</color> <color name="dropdown_container">#F3F3FA</color> + <color name="sign_in_options_container">#DADADA</color> + <color name="sign_in_options_icon_color">#1B1B1B</color> </resources>
\ No newline at end of file diff --git a/packages/CredentialManager/res/values/dimens.xml b/packages/CredentialManager/res/values/dimens.xml index 2a4719d027e2..3a8c78f6d854 100644 --- a/packages/CredentialManager/res/values/dimens.xml +++ b/packages/CredentialManager/res/values/dimens.xml @@ -18,11 +18,13 @@ <resources> <dimen name="autofill_view_top_padding">12dp</dimen> - <dimen name="autofill_view_right_padding">24dp</dimen> + <dimen name="autofill_view_right_padding">12dp</dimen> <dimen name="autofill_view_bottom_padding">12dp</dimen> <dimen name="autofill_view_left_padding">16dp</dimen> <dimen name="autofill_view_icon_to_text_padding">10dp</dimen> <dimen name="autofill_icon_size">24dp</dimen> - <dimen name="autofill_dropdown_layout_width">296dp</dimen> - <dimen name="autofill_dropdown_text_width">240dp</dimen> + <dimen name="autofill_dropdown_textview_min_width">112dp</dimen> + <dimen name="autofill_dropdown_textview_max_width">230dp</dimen> + <dimen name="dropdown_layout_horizontal_margin">24dp</dimen> + <integer name="autofill_max_visible_datasets">3</integer> </resources>
\ No newline at end of file diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index 605e77bef34e..f98164b8788c 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -168,4 +168,9 @@ <string name="get_dialog_option_headline_use_a_different_device">Use a different device</string> <!-- Text shown on a snackbar when the app cancelled the UI. [CHAR LIMIT=120] --> <string name="request_cancelled_by">Request cancelled by <xliff:g id="app_name" example="YouTube">%1$s</xliff:g></string> + + <!-- Strings for dropdown presentation. --> + <!-- Text shown in the dropdown presentation to select more sign in options. [CHAR LIMIT=120] --> + <string name="dropdown_presentation_more_sign_in_options_text">Sign-in options</string> + <string name="provider_icon_content_description">Credential provider icon</string> </resources>
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt index 03ac605222ba..985f3228f402 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt @@ -30,6 +30,7 @@ import android.credentials.ui.GetCredentialProviderData import android.os.Bundle import android.os.CancellationSignal import android.os.OutcomeReceiver +import android.provider.Settings import android.credentials.Credential import android.service.autofill.AutofillService import android.service.autofill.Dataset @@ -48,7 +49,9 @@ import android.view.autofill.IAutoFillManagerClient import android.view.autofill.AutofillId import android.widget.inline.InlinePresentationSpec import android.credentials.CredentialManager +import android.widget.RemoteViews import androidx.autofill.inline.v1.InlineSuggestionUi +import androidx.core.content.ContextCompat import androidx.credentials.provider.CustomCredentialEntry import androidx.credentials.provider.PasswordCredentialEntry import androidx.credentials.provider.PublicKeyCredentialEntry @@ -115,7 +118,7 @@ class CredentialAutofillService : AutofillService() { } val getCredRequest: GetCredentialRequest? = getCredManRequest(structure, sessionId, - requestId) + requestId) if (getCredRequest == null) { Log.i(TAG, "No credential manager request found") callback.onFailure("No credential manager request found") @@ -307,10 +310,14 @@ class CredentialAutofillService : AutofillService() { val inlineMaxSuggestedCount = inlineSuggestionsRequest?.maxSuggestionCount ?: 0 val inlinePresentationSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs val inlinePresentationSpecsCount = inlinePresentationSpecs?.size ?: 0 - var maxItemCount = totalEntryCount - if (inlineMaxSuggestedCount > 0) { - maxItemCount = maxItemCount.coerceAtMost(inlineMaxSuggestedCount) - } + val maxDropdownDisplayLimit = this.resources.getInteger( + com.android.credentialmanager.R.integer.autofill_max_visible_datasets) + var maxInlineItemCount = totalEntryCount + maxInlineItemCount = maxInlineItemCount.coerceAtMost(inlineMaxSuggestedCount) + val lastDropdownDatasetIndex = Settings.Global.getInt(this.contentResolver, + Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS, + (maxDropdownDisplayLimit - 1).coerceAtMost(totalEntryCount - 1)) + var i = 0 var datasetAdded = false @@ -333,13 +340,8 @@ class CredentialAutofillService : AutofillService() { Log.e(TAG, "PendingIntent was missing from the entry.") return@usernameLoop } - if (inlinePresentationSpecs == null) { - Log.i(TAG, "Inline presentation spec is null, " + - "building dropdown presentation only") - } - if (i >= maxItemCount) { - Log.e(TAG, "Skipping because reached the max item count.") - return@usernameLoop + if (i >= maxInlineItemCount && i >= lastDropdownDatasetIndex) { + return@usernameLoop; } val icon: Icon = if (primaryEntry.icon == null) { // The empty entry icon has non-null icon reference but null drawable reference. @@ -351,38 +353,26 @@ class CredentialAutofillService : AutofillService() { } // Create inline presentation var inlinePresentation: InlinePresentation? = null - var spec: InlinePresentationSpec? - if (inlinePresentationSpecs != null) { - if (i < inlinePresentationSpecsCount) { - spec = inlinePresentationSpecs[i] + if (inlinePresentationSpecs != null && i < maxInlineItemCount) { + val spec: InlinePresentationSpec? = if (i < inlinePresentationSpecsCount) { + inlinePresentationSpecs[i] } else { - spec = inlinePresentationSpecs[inlinePresentationSpecsCount - 1] + inlinePresentationSpecs[inlinePresentationSpecsCount - 1] } - val displayName: String = if (primaryEntry.credentialType == - CredentialType.PASSKEY && primaryEntry.displayName != null) { - primaryEntry.displayName!! - } else { - primaryEntry.userName - } - val sliceBuilder = InlineSuggestionUi - .newContentBuilder(pendingIntent) - .setTitle(displayName) - sliceBuilder.setStartIcon(icon) - if (primaryEntry.credentialType == - CredentialType.PASSKEY && duplicateDisplayNamesForPasskeys[displayName] - == true) { - sliceBuilder.setSubtitle(primaryEntry.userName) - } - inlinePresentation = InlinePresentation( - sliceBuilder.build().slice, spec, /* pinned= */ false) + inlinePresentation = createInlinePresentation(primaryEntry, pendingIntent, icon, + spec!!, duplicateDisplayNamesForPasskeys) + } + var dropdownPresentation: RemoteViews? = null + if (i < lastDropdownDatasetIndex) { + dropdownPresentation = RemoteViewsFactory + .createDropdownPresentation(this, icon, primaryEntry) } - val dropdownPresentation = RemoteViewsFactory.createDropdownPresentation( - this, icon, primaryEntry) - i++ val dataSetBuilder = Dataset.Builder() val presentationBuilder = Presentations.Builder() - .setMenuPresentation(dropdownPresentation) + if (dropdownPresentation != null) { + presentationBuilder.setMenuPresentation(dropdownPresentation) + } if (inlinePresentation != null) { presentationBuilder.setInlinePresentation(inlinePresentation) } @@ -398,6 +388,12 @@ class CredentialAutofillService : AutofillService() { .setAuthenticationExtras(fillInIntent.extras) .build()) datasetAdded = true + i++ + + if (i == lastDropdownDatasetIndex && bottomSheetPendingIntent != null) { + addDropdownMoreOptionsPresentation(bottomSheetPendingIntent, autofillId, + fillResponseBuilder) + } } val pinnedSpec = getLastInlinePresentationSpec(inlinePresentationSpecs, inlinePresentationSpecsCount) @@ -408,6 +404,49 @@ class CredentialAutofillService : AutofillService() { return datasetAdded } + private fun createInlinePresentation(primaryEntry: CredentialEntryInfo, + pendingIntent: PendingIntent, + icon: Icon, + spec: InlinePresentationSpec, + duplicateDisplayNameForPasskeys: MutableMap<String, 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) + sliceBuilder.setStartIcon(icon) + if (primaryEntry.credentialType == + CredentialType.PASSKEY && duplicateDisplayNameForPasskeys[displayName] == true) { + sliceBuilder.setSubtitle(primaryEntry.userName) + } + return InlinePresentation( + sliceBuilder.build().slice, spec, /* pinned= */ false) + } + + private fun addDropdownMoreOptionsPresentation( + bottomSheetPendingIntent: PendingIntent, + autofillId: AutofillId, + fillResponseBuilder: FillResponse.Builder) { + val presentationBuilder = Presentations.Builder() + .setMenuPresentation(RemoteViewsFactory.createMoreSignInOptionsPresentation(this)) + + fillResponseBuilder.addDataset( + Dataset.Builder() + .setField( + autofillId, + Field.Builder().setPresentations( + presentationBuilder.build()) + .build()) + .setAuthentication(bottomSheetPendingIntent.intentSender) + .build() + ) + } + private fun getLastInlinePresentationSpec( inlinePresentationSpecs: List<InlinePresentationSpec>?, inlinePresentationSpecsCount: Int @@ -534,9 +573,9 @@ class CredentialAutofillService : AutofillService() { } private fun getCredManRequest( - structure: AssistStructure, - sessionId: Int, - requestId: Int + structure: AssistStructure, + sessionId: Int, + requestId: Int ): GetCredentialRequest? { val credentialOptions: MutableList<CredentialOption> = mutableListOf() traverseStructure(structure, credentialOptions) 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 e039dead043e..68f1c861d51b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt @@ -44,7 +44,7 @@ class RemoteViewsFactory { if (credentialEntryInfo.credentialType == CredentialType.UNKNOWN) { return remoteViews } - setRemoteViewsPaddings(remoteViews, context) + setRemoteViewsPaddings(remoteViews, context, /* primaryTextBottomPadding=*/0) if (credentialEntryInfo.credentialType == CredentialType.PASSKEY) { val displayName = credentialEntryInfo.displayName ?: credentialEntryInfo.userName remoteViews.setTextViewText(android.R.id.text1, displayName) @@ -81,8 +81,46 @@ class RemoteViewsFactory { return remoteViews } + fun createMoreSignInOptionsPresentation(context: Context): RemoteViews { + var layoutId: Int = com.android.credentialmanager.R.layout + .credman_dropdown_bottom_sheet + val remoteViews = RemoteViews(context.packageName, layoutId) + setRemoteViewsPaddings(remoteViews, context) + remoteViews.setTextViewText(android.R.id.text1, ContextCompat.getString(context, + com.android.credentialmanager + .R.string.dropdown_presentation_more_sign_in_options_text)) + + val textColorPrimary = ContextCompat.getColor(context, + com.android.credentialmanager.R.color.text_primary) + remoteViews.setTextColor(android.R.id.text1, textColorPrimary) + val icon = Icon.createWithResource(context, com + .android.credentialmanager.R.drawable.more_horiz_24px) + icon.setTint(ContextCompat.getColor(context, + com.android.credentialmanager.R.color.sign_in_options_icon_color)) + remoteViews.setImageViewIcon(android.R.id.icon1, icon) + remoteViews.setBoolean( + android.R.id.icon1, setAdjustViewBoundsMethodName, true); + remoteViews.setInt( + android.R.id.icon1, + setMaxHeightMethodName, + context.resources.getDimensionPixelSize( + 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, setBackgroundResourceMethodName, drawableId); + return remoteViews + } + private fun setRemoteViewsPaddings( remoteViews: RemoteViews, context: Context) { + val bottomPadding = context.resources.getDimensionPixelSize( + com.android.credentialmanager.R.dimen.autofill_view_bottom_padding) + setRemoteViewsPaddings(remoteViews, context, bottomPadding) + } + + private fun setRemoteViewsPaddings( + remoteViews: RemoteViews, context: Context, primaryTextBottomPadding: Int) { val leftPadding = context.resources.getDimensionPixelSize( com.android.credentialmanager.R.dimen.autofill_view_left_padding) val iconToTextPadding = context.resources.getDimensionPixelSize( @@ -104,7 +142,7 @@ class RemoteViewsFactory { iconToTextPadding, /* top=*/topPadding, /* right=*/rightPadding, - /* bottom=*/0) + primaryTextBottomPadding) remoteViews.setViewPadding( android.R.id.text2, iconToTextPadding, |