diff options
19 files changed, 2018 insertions, 89 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 258acf9acaed..8c06bdd6be5d 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -37928,21 +37928,23 @@ package android.service.autofill { } public static final class Dataset.Builder { - ctor public Dataset.Builder(@NonNull android.widget.RemoteViews); + ctor @Deprecated public Dataset.Builder(@NonNull android.widget.RemoteViews); + ctor public Dataset.Builder(@NonNull android.service.autofill.Presentations); ctor public Dataset.Builder(); method @NonNull public android.service.autofill.Dataset build(); method @NonNull public android.service.autofill.Dataset.Builder setAuthentication(@Nullable android.content.IntentSender); + method @NonNull public android.service.autofill.Dataset.Builder setField(@NonNull android.view.autofill.AutofillId, @Nullable android.service.autofill.Field); method @NonNull public android.service.autofill.Dataset.Builder setId(@Nullable String); - method @NonNull public android.service.autofill.Dataset.Builder setInlinePresentation(@NonNull android.service.autofill.InlinePresentation); - method @NonNull public android.service.autofill.Dataset.Builder setInlinePresentation(@NonNull android.service.autofill.InlinePresentation, @NonNull android.service.autofill.InlinePresentation); - method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue); - method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @NonNull android.widget.RemoteViews); - method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern); - method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern, @NonNull android.widget.RemoteViews); - method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @NonNull android.widget.RemoteViews, @NonNull android.service.autofill.InlinePresentation); - method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @NonNull android.widget.RemoteViews, @NonNull android.service.autofill.InlinePresentation, @NonNull android.service.autofill.InlinePresentation); - method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern, @NonNull android.widget.RemoteViews, @NonNull android.service.autofill.InlinePresentation); - method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern, @NonNull android.widget.RemoteViews, @NonNull android.service.autofill.InlinePresentation, @NonNull android.service.autofill.InlinePresentation); + method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setInlinePresentation(@NonNull android.service.autofill.InlinePresentation); + method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setInlinePresentation(@NonNull android.service.autofill.InlinePresentation, @NonNull android.service.autofill.InlinePresentation); + method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue); + method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @NonNull android.widget.RemoteViews); + method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern); + method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern, @NonNull android.widget.RemoteViews); + method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @NonNull android.widget.RemoteViews, @NonNull android.service.autofill.InlinePresentation); + method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @NonNull android.widget.RemoteViews, @NonNull android.service.autofill.InlinePresentation, @NonNull android.service.autofill.InlinePresentation); + method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern, @NonNull android.widget.RemoteViews, @NonNull android.service.autofill.InlinePresentation); + method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern, @NonNull android.widget.RemoteViews, @NonNull android.service.autofill.InlinePresentation, @NonNull android.service.autofill.InlinePresentation); } public final class DateTransformation implements android.os.Parcelable android.service.autofill.Transformation { @@ -37959,6 +37961,19 @@ package android.service.autofill { field @NonNull public static final android.os.Parcelable.Creator<android.service.autofill.DateValueSanitizer> CREATOR; } + public final class Field { + method @Nullable public android.service.autofill.Presentations getPresentations(); + method @Nullable public android.view.autofill.AutofillValue getValue(); + } + + public static final class Field.Builder { + ctor public Field.Builder(); + method @NonNull public android.service.autofill.Field build(); + method @NonNull public android.service.autofill.Field.Builder setFilter(@Nullable java.util.regex.Pattern); + method @NonNull public android.service.autofill.Field.Builder setPresentations(@NonNull android.service.autofill.Presentations); + method @NonNull public android.service.autofill.Field.Builder setValue(@NonNull android.view.autofill.AutofillValue); + } + public final class FieldClassification { method @NonNull public java.util.List<android.service.autofill.FieldClassification.Match> getMatches(); } @@ -38041,11 +38056,14 @@ package android.service.autofill { method @NonNull public android.service.autofill.FillResponse.Builder addDataset(@Nullable android.service.autofill.Dataset); method @NonNull public android.service.autofill.FillResponse build(); method @NonNull public android.service.autofill.FillResponse.Builder disableAutofill(long); - method @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews); - method @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews, @Nullable android.service.autofill.InlinePresentation); - method @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews, @Nullable android.service.autofill.InlinePresentation, @Nullable android.service.autofill.InlinePresentation); + method @Deprecated @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews); + method @Deprecated @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews, @Nullable android.service.autofill.InlinePresentation); + method @Deprecated @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews, @Nullable android.service.autofill.InlinePresentation, @Nullable android.service.autofill.InlinePresentation); + method @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.service.autofill.Presentations); method @NonNull public android.service.autofill.FillResponse.Builder setClientState(@Nullable android.os.Bundle); + method @NonNull public android.service.autofill.FillResponse.Builder setDialogHeader(@NonNull android.widget.RemoteViews); method @NonNull public android.service.autofill.FillResponse.Builder setFieldClassificationIds(@NonNull android.view.autofill.AutofillId...); + method @NonNull public android.service.autofill.FillResponse.Builder setFillDialogTriggerIds(@NonNull android.view.autofill.AutofillId...); method @NonNull public android.service.autofill.FillResponse.Builder setFlags(int); method @NonNull public android.service.autofill.FillResponse.Builder setFooter(@NonNull android.widget.RemoteViews); method @NonNull public android.service.autofill.FillResponse.Builder setHeader(@NonNull android.widget.RemoteViews); @@ -38090,6 +38108,22 @@ package android.service.autofill { public interface OnClickAction { } + public final class Presentations { + method @Nullable public android.widget.RemoteViews getDialogPresentation(); + method @Nullable public android.service.autofill.InlinePresentation getInlinePresentation(); + method @Nullable public android.service.autofill.InlinePresentation getInlineTooltipPresentation(); + method @Nullable public android.widget.RemoteViews getMenuPresentation(); + } + + public static final class Presentations.Builder { + ctor public Presentations.Builder(); + method @NonNull public android.service.autofill.Presentations build(); + method @NonNull public android.service.autofill.Presentations.Builder setDialogPresentation(@NonNull android.widget.RemoteViews); + method @NonNull public android.service.autofill.Presentations.Builder setInlinePresentation(@NonNull android.service.autofill.InlinePresentation); + method @NonNull public android.service.autofill.Presentations.Builder setInlineTooltipPresentation(@NonNull android.service.autofill.InlinePresentation); + method @NonNull public android.service.autofill.Presentations.Builder setMenuPresentation(@NonNull android.widget.RemoteViews); + } + public final class RegexValidator implements android.os.Parcelable android.service.autofill.Validator { ctor public RegexValidator(@NonNull android.view.autofill.AutofillId, @NonNull java.util.regex.Pattern); method public int describeContents(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 28f9ab442f45..a1f751fb82d0 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -10638,9 +10638,9 @@ package android.service.autofill { } public static final class Dataset.Builder { - ctor public Dataset.Builder(@NonNull android.service.autofill.InlinePresentation); + ctor @Deprecated public Dataset.Builder(@NonNull android.service.autofill.InlinePresentation); method @NonNull public android.service.autofill.Dataset.Builder setContent(@NonNull android.view.autofill.AutofillId, @Nullable android.content.ClipData); - method @NonNull public android.service.autofill.Dataset.Builder setFieldInlinePresentation(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern, @NonNull android.service.autofill.InlinePresentation); + method @Deprecated @NonNull public android.service.autofill.Dataset.Builder setFieldInlinePresentation(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern, @NonNull android.service.autofill.InlinePresentation); } public abstract class InlineSuggestionRenderService extends android.app.Service { diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java index 86341a908ad7..cfb690916ecc 100644 --- a/core/java/android/service/autofill/Dataset.java +++ b/core/java/android/service/autofill/Dataset.java @@ -65,6 +65,16 @@ import java.util.regex.Pattern; * can be shown by the keyboard as a suggestion. To use this feature, the Dataset should contain * an {@link InlinePresentation} representing how the inline suggestion UI will be rendered. * + * <a name="FillDialogUI"></a> + * <h3>Fill Dialog UI</h3> + * + * <p>The fill dialog UI is a more conspicuous and efficient interface than dropdown UI. If autofill + * suggestions are available when the user clicks on a field that supports filling the dialog UI, + * Autofill will pop up a fill dialog. The dialog will take up a larger area to display the + * datasets, so it is easy for users to pay attention to the datasets and selecting a dataset. + * If the user focuses on the view before suggestions are available, will fall back to dropdown UI + * or inline suggestions. + * * <a name="Authentication"></a> * <h3>Dataset authentication</h3> * @@ -92,10 +102,9 @@ import java.util.regex.Pattern; * <ol> * <li>If the view's {@link android.view.View#getAutofillValue() autofill value} is not * {@link AutofillValue#isText() text} or is empty, all datasets are shown. - * <li>Datasets that have a filter regex (set through - * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern)} or - * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}) and whose - * regex matches the view's text value converted to lower case are shown. + * <li>Datasets that have a filter regex (set through {@link Field.Builder#setFilter(Pattern)} + * and {@link Dataset.Builder#setField(AutofillId, Field)}) and whose regex matches the view's + * text value converted to lower case are shown. * <li>Datasets that do not require authentication, have a field value that is * {@link AutofillValue#isText() text} and whose {@link AutofillValue#getTextValue() value} starts * with the lower case value of the view's text are shown. @@ -107,11 +116,13 @@ public final class Dataset implements Parcelable { private final ArrayList<AutofillId> mFieldIds; private final ArrayList<AutofillValue> mFieldValues; private final ArrayList<RemoteViews> mFieldPresentations; + private final ArrayList<RemoteViews> mFieldDialogPresentations; private final ArrayList<InlinePresentation> mFieldInlinePresentations; private final ArrayList<InlinePresentation> mFieldInlineTooltipPresentations; private final ArrayList<DatasetFieldFilter> mFieldFilters; @Nullable private final ClipData mFieldContent; private final RemoteViews mPresentation; + private final RemoteViews mDialogPresentation; @Nullable private final InlinePresentation mInlinePresentation; @Nullable private final InlinePresentation mInlineTooltipPresentation; private final IntentSender mAuthentication; @@ -121,11 +132,13 @@ public final class Dataset implements Parcelable { mFieldIds = builder.mFieldIds; mFieldValues = builder.mFieldValues; mFieldPresentations = builder.mFieldPresentations; + mFieldDialogPresentations = builder.mFieldDialogPresentations; mFieldInlinePresentations = builder.mFieldInlinePresentations; mFieldInlineTooltipPresentations = builder.mFieldInlineTooltipPresentations; mFieldFilters = builder.mFieldFilters; mFieldContent = builder.mFieldContent; mPresentation = builder.mPresentation; + mDialogPresentation = builder.mDialogPresentation; mInlinePresentation = builder.mInlinePresentation; mInlineTooltipPresentation = builder.mInlineTooltipPresentation; mAuthentication = builder.mAuthentication; @@ -153,6 +166,12 @@ public final class Dataset implements Parcelable { } /** @hide */ + public RemoteViews getFieldDialogPresentation(int index) { + final RemoteViews customPresentation = mFieldDialogPresentations.get(index); + return customPresentation != null ? customPresentation : mDialogPresentation; + } + + /** @hide */ public @Nullable InlinePresentation getFieldInlinePresentation(int index) { final InlinePresentation inlinePresentation = mFieldInlinePresentations.get(index); return inlinePresentation != null ? inlinePresentation : mInlinePresentation; @@ -219,6 +238,9 @@ public final class Dataset implements Parcelable { if (mFieldPresentations != null) { builder.append(", fieldPresentations=").append(mFieldPresentations.size()); } + if (mFieldDialogPresentations != null) { + builder.append(", fieldDialogPresentations=").append(mFieldDialogPresentations.size()); + } if (mFieldInlinePresentations != null) { builder.append(", fieldInlinePresentations=").append(mFieldInlinePresentations.size()); } @@ -232,6 +254,9 @@ public final class Dataset implements Parcelable { if (mPresentation != null) { builder.append(", hasPresentation"); } + if (mDialogPresentation != null) { + builder.append(", hasDialogPresentation"); + } if (mInlinePresentation != null) { builder.append(", hasInlinePresentation"); } @@ -264,11 +289,13 @@ public final class Dataset implements Parcelable { private ArrayList<AutofillId> mFieldIds; private ArrayList<AutofillValue> mFieldValues; private ArrayList<RemoteViews> mFieldPresentations; + private ArrayList<RemoteViews> mFieldDialogPresentations; private ArrayList<InlinePresentation> mFieldInlinePresentations; private ArrayList<InlinePresentation> mFieldInlineTooltipPresentations; private ArrayList<DatasetFieldFilter> mFieldFilters; @Nullable private ClipData mFieldContent; private RemoteViews mPresentation; + private RemoteViews mDialogPresentation; @Nullable private InlinePresentation mInlinePresentation; @Nullable private InlinePresentation mInlineTooltipPresentation; private IntentSender mAuthentication; @@ -279,7 +306,9 @@ public final class Dataset implements Parcelable { * Creates a new builder. * * @param presentation The presentation used to visualize this dataset. + * @deprecated Use {@link #Builder(Presentations)} instead. */ + @Deprecated public Builder(@NonNull RemoteViews presentation) { Objects.requireNonNull(presentation, "presentation must be non-null"); mPresentation = presentation; @@ -294,19 +323,34 @@ public final class Dataset implements Parcelable { * as inline suggestions. If the dataset supports inline suggestions, * this should not be null. * @hide + * @deprecated Use {@link #Builder(Presentations)} instead. */ @SystemApi + @Deprecated public Builder(@NonNull InlinePresentation inlinePresentation) { Objects.requireNonNull(inlinePresentation, "inlinePresentation must be non-null"); mInlinePresentation = inlinePresentation; } /** + * Creates a new builder. + * + * @param presentations The presentations used to visualize this dataset. + */ + public Builder(@NonNull Presentations presentations) { + Objects.requireNonNull(presentations, "presentations must be non-null"); + + mPresentation = presentations.getMenuPresentation(); + mInlinePresentation = presentations.getInlinePresentation(); + mInlineTooltipPresentation = presentations.getInlineTooltipPresentation(); + mDialogPresentation = presentations.getDialogPresentation(); + } + + /** * Creates a new builder for a dataset where each field will be visualized independently. * - * <p>When using this constructor, fields must be set through - * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} or - * {@link #setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}. + * <p>When using this constructor, a presentation must be provided for each field through + * {@link #setField(AutofillId, Field)}. */ public Builder() { } @@ -318,7 +362,9 @@ public final class Dataset implements Parcelable { * @throws IllegalStateException if {@link #build()} was already called. * * @return this builder. + * @deprecated Use {@link #Builder(Presentations)} instead. */ + @Deprecated public @NonNull Builder setInlinePresentation( @NonNull InlinePresentation inlinePresentation) { throwIfDestroyed(); @@ -339,7 +385,9 @@ public final class Dataset implements Parcelable { * @throws IllegalStateException if {@link #build()} was already called. * * @return this builder. + * @deprecated Use {@link #Builder(Presentations)} instead. */ + @Deprecated public @NonNull Builder setInlinePresentation( @NonNull InlinePresentation inlinePresentation, @NonNull InlinePresentation inlineTooltipPresentation) { @@ -479,7 +527,7 @@ public final class Dataset implements Parcelable { "Content items cannot contain an Intent: content=" + content); } } - setLifeTheUniverseAndEverything(id, null, null, null, null); + setLifeTheUniverseAndEverything(id, null, null, null, null, null, null); mFieldContent = content; return this; } @@ -509,10 +557,12 @@ public final class Dataset implements Parcelable { * @throws IllegalStateException if {@link #build()} was already called. * * @return this builder. + * @deprecated Use {@link #setField(AutofillId, Field)} instead. */ + @Deprecated public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value) { throwIfDestroyed(); - setLifeTheUniverseAndEverything(id, value, null, null, null); + setLifeTheUniverseAndEverything(id, value, null, null, null, null, null); return this; } @@ -537,12 +587,14 @@ public final class Dataset implements Parcelable { * @throws IllegalStateException if {@link #build()} was already called. * * @return this builder. + * @deprecated Use {@link #setField(AutofillId, Field)} instead. */ + @Deprecated public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, @NonNull RemoteViews presentation) { throwIfDestroyed(); Objects.requireNonNull(presentation, "presentation cannot be null"); - setLifeTheUniverseAndEverything(id, value, presentation, null, null); + setLifeTheUniverseAndEverything(id, value, presentation, null, null, null, null); return this; } @@ -572,13 +624,16 @@ public final class Dataset implements Parcelable { * @return this builder. * @throws IllegalStateException if the builder was constructed without a * {@link RemoteViews presentation} or {@link #build()} was already called. + * @deprecated Use {@link #setField(AutofillId, Field)} instead. */ + @Deprecated public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, @Nullable Pattern filter) { throwIfDestroyed(); Preconditions.checkState(mPresentation != null, "Dataset presentation not set on constructor"); - setLifeTheUniverseAndEverything(id, value, null, null, new DatasetFieldFilter(filter)); + setLifeTheUniverseAndEverything( + id, value, null, null, null, new DatasetFieldFilter(filter), null); return this; } @@ -610,13 +665,15 @@ public final class Dataset implements Parcelable { * @throws IllegalStateException if {@link #build()} was already called. * * @return this builder. + * @deprecated Use {@link #setField(AutofillId, Field)} instead. */ + @Deprecated public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, @Nullable Pattern filter, @NonNull RemoteViews presentation) { throwIfDestroyed(); Objects.requireNonNull(presentation, "presentation cannot be null"); - setLifeTheUniverseAndEverything(id, value, presentation, null, - new DatasetFieldFilter(filter)); + setLifeTheUniverseAndEverything(id, value, presentation, null, null, + new DatasetFieldFilter(filter), null); return this; } @@ -641,13 +698,16 @@ public final class Dataset implements Parcelable { * @throws IllegalStateException if {@link #build()} was already called. * * @return this builder. + * @deprecated Use {@link #setField(AutofillId, Field)} instead. */ + @Deprecated public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation) { throwIfDestroyed(); Objects.requireNonNull(presentation, "presentation cannot be null"); Objects.requireNonNull(inlinePresentation, "inlinePresentation cannot be null"); - setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, null); + setLifeTheUniverseAndEverything( + id, value, presentation, inlinePresentation, null, null, null); return this; } @@ -672,7 +732,9 @@ public final class Dataset implements Parcelable { * @throws IllegalStateException if {@link #build()} was already called. * * @return this builder. + * @deprecated Use {@link #setField(AutofillId, Field)} instead. */ + @Deprecated public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation, @NonNull InlinePresentation inlineTooltipPresentation) { @@ -682,7 +744,7 @@ public final class Dataset implements Parcelable { Objects.requireNonNull(inlineTooltipPresentation, "inlineTooltipPresentation cannot be null"); setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, - inlineTooltipPresentation, null); + inlineTooltipPresentation, null, null); return this; } @@ -718,15 +780,17 @@ public final class Dataset implements Parcelable { * @throws IllegalStateException if {@link #build()} was already called. * * @return this builder. + * @deprecated Use {@link #setField(AutofillId, Field)} instead. */ + @Deprecated public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, @Nullable Pattern filter, @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation) { throwIfDestroyed(); Objects.requireNonNull(presentation, "presentation cannot be null"); Objects.requireNonNull(inlinePresentation, "inlinePresentation cannot be null"); - setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, - new DatasetFieldFilter(filter)); + setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, null, + new DatasetFieldFilter(filter), null); return this; } @@ -756,7 +820,9 @@ public final class Dataset implements Parcelable { * @throws IllegalStateException if {@link #build()} was already called. * * @return this builder. + * @deprecated Use {@link #setField(AutofillId, Field)} instead. */ + @Deprecated public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, @Nullable Pattern filter, @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation, @@ -767,7 +833,91 @@ public final class Dataset implements Parcelable { Objects.requireNonNull(inlineTooltipPresentation, "inlineTooltipPresentation cannot be null"); setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, - inlineTooltipPresentation, new DatasetFieldFilter(filter)); + inlineTooltipPresentation, new DatasetFieldFilter(filter), null); + return this; + } + + /** + * Sets the value of a field. + * + * Before Android 13, this information could be provided using several overloaded + * setValue(...) methods. This method replaces those with a Builder pattern. + * For example, in the old workflow, the app sets a field would be: + * <pre class="prettyprint"> + * Dataset.Builder dataset = new Dataset.Builder(); + * if (filter != null) { + * if (presentation != null) { + * if (inlinePresentation != null) { + * dataset.setValue(id, value, filter, presentation, inlinePresentation) + * } else { + * dataset.setValue(id, value, filter, presentation); + * } + * } else { + * dataset.setValue(id, value, filter); + * } + * } else { + * if (presentation != null) { + * if (inlinePresentation != null) { + * dataset.setValue(id, value, presentation, inlinePresentation) + * } else { + * dataset.setValue(id, value, presentation); + * } + * } else { + * dataset.setValue(id, value); + * } + * } + * </pre> + * <p>The new workflow would be: + * <pre class="prettyprint"> + * Field.Builder fieldBuilder = new Field.Builder(); + * if (value != null) { + * fieldBuilder.setValue(value); + * } + * if (filter != null) { + * fieldBuilder.setFilter(filter); + * } + * Presentations.Builder presentationsBuilder = new Presentations.Builder(id); + * if (presentation != null) { + * presentationsBuilder.setMenuPresentation(presentation); + * } + * if (inlinePresentation != null) { + * presentationsBuilder.setInlinePresentation(inlinePresentation); + * } + * if (dialogPresentation != null) { + * presentationsBuilder.setDialogPresentation(dialogPresentation); + * } + * fieldBuilder.setPresentations(presentationsBuilder.build()); + * dataset.setField(id, fieldBuilder.build()); + * </pre> + * + * @see Field + * + * @param id id returned by {@link + * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. + * @param field the fill information about the field. + * + * @throws IllegalStateException if {@link #build()} was already called. + * + * @return this builder. + */ + public @NonNull Builder setField(@NonNull AutofillId id, @Nullable Field field) { + throwIfDestroyed(); + if (field == null) { + setLifeTheUniverseAndEverything(id, null, null, null, null, null, null); + } else { + final DatasetFieldFilter filter = field.getFilter(); + final Presentations presentations = field.getPresentations(); + if (presentations == null) { + setLifeTheUniverseAndEverything(id, field.getValue(), null, null, null, + filter, null); + } else { + setLifeTheUniverseAndEverything(id, field.getValue(), + presentations.getMenuPresentation(), + presentations.getInlinePresentation(), + presentations.getInlineTooltipPresentation(), filter, + presentations.getDialogPresentation()); + } + } return this; } @@ -793,39 +943,34 @@ public final class Dataset implements Parcelable { * @throws IllegalStateException if {@link #build()} was already called. * * @return this builder. - * + * @deprecated Use {@link #setField(AutofillId, Field)} instead. * @hide */ + @Deprecated @SystemApi public @NonNull Builder setFieldInlinePresentation(@NonNull AutofillId id, @Nullable AutofillValue value, @Nullable Pattern filter, @NonNull InlinePresentation inlinePresentation) { throwIfDestroyed(); Objects.requireNonNull(inlinePresentation, "inlinePresentation cannot be null"); - setLifeTheUniverseAndEverything(id, value, null, inlinePresentation, - new DatasetFieldFilter(filter)); + setLifeTheUniverseAndEverything(id, value, null, inlinePresentation, null, + new DatasetFieldFilter(filter), null); return this; } private void setLifeTheUniverseAndEverything(@NonNull AutofillId id, @Nullable AutofillValue value, @Nullable RemoteViews presentation, @Nullable InlinePresentation inlinePresentation, - @Nullable DatasetFieldFilter filter) { - setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, null, - filter); - } - - private void setLifeTheUniverseAndEverything(@NonNull AutofillId id, - @Nullable AutofillValue value, @Nullable RemoteViews presentation, - @Nullable InlinePresentation inlinePresentation, @Nullable InlinePresentation tooltip, - @Nullable DatasetFieldFilter filter) { + @Nullable DatasetFieldFilter filter, + @Nullable RemoteViews dialogPresentation) { Objects.requireNonNull(id, "id cannot be null"); if (mFieldIds != null) { final int existingIdx = mFieldIds.indexOf(id); if (existingIdx >= 0) { mFieldValues.set(existingIdx, value); mFieldPresentations.set(existingIdx, presentation); + mFieldDialogPresentations.set(existingIdx, dialogPresentation); mFieldInlinePresentations.set(existingIdx, inlinePresentation); mFieldInlineTooltipPresentations.set(existingIdx, tooltip); mFieldFilters.set(existingIdx, filter); @@ -835,6 +980,7 @@ public final class Dataset implements Parcelable { mFieldIds = new ArrayList<>(); mFieldValues = new ArrayList<>(); mFieldPresentations = new ArrayList<>(); + mFieldDialogPresentations = new ArrayList<>(); mFieldInlinePresentations = new ArrayList<>(); mFieldInlineTooltipPresentations = new ArrayList<>(); mFieldFilters = new ArrayList<>(); @@ -842,6 +988,7 @@ public final class Dataset implements Parcelable { mFieldIds.add(id); mFieldValues.add(value); mFieldPresentations.add(presentation); + mFieldDialogPresentations.add(dialogPresentation); mFieldInlinePresentations.add(inlinePresentation); mFieldInlineTooltipPresentations.add(tooltip); mFieldFilters.add(filter); @@ -853,10 +1000,7 @@ public final class Dataset implements Parcelable { * <p>You should not interact with this builder once this method is called. * * @throws IllegalStateException if no field was set (through - * {@link #setValue(AutofillId, AutofillValue)} or - * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} or - * {@link #setValue(AutofillId, AutofillValue, RemoteViews, InlinePresentation)}), - * or if {@link #build()} was already called. + * {@link #setField(AutofillId, Field)}), or if {@link #build()} was already called. * * @return The built dataset. */ @@ -897,11 +1041,13 @@ public final class Dataset implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeParcelable(mPresentation, flags); + parcel.writeParcelable(mDialogPresentation, flags); parcel.writeParcelable(mInlinePresentation, flags); parcel.writeParcelable(mInlineTooltipPresentation, flags); parcel.writeTypedList(mFieldIds, flags); parcel.writeTypedList(mFieldValues, flags); parcel.writeTypedList(mFieldPresentations, flags); + parcel.writeTypedList(mFieldDialogPresentations, flags); parcel.writeTypedList(mFieldInlinePresentations, flags); parcel.writeTypedList(mFieldInlineTooltipPresentations, flags); parcel.writeTypedList(mFieldFilters, flags); @@ -913,8 +1059,12 @@ public final class Dataset implements Parcelable { public static final @NonNull Creator<Dataset> CREATOR = new Creator<Dataset>() { @Override public Dataset createFromParcel(Parcel parcel) { - final RemoteViews presentation = parcel.readParcelable(null, android.widget.RemoteViews.class); - final InlinePresentation inlinePresentation = parcel.readParcelable(null, android.service.autofill.InlinePresentation.class); + final RemoteViews presentation = parcel.readParcelable(null, + android.widget.RemoteViews.class); + final RemoteViews dialogPresentation = parcel.readParcelable(null, + android.widget.RemoteViews.class); + final InlinePresentation inlinePresentation = parcel.readParcelable(null, + android.service.autofill.InlinePresentation.class); final InlinePresentation inlineTooltipPresentation = parcel.readParcelable(null, android.service.autofill.InlinePresentation.class); final ArrayList<AutofillId> ids = @@ -923,27 +1073,41 @@ public final class Dataset implements Parcelable { parcel.createTypedArrayList(AutofillValue.CREATOR); final ArrayList<RemoteViews> presentations = parcel.createTypedArrayList(RemoteViews.CREATOR); + final ArrayList<RemoteViews> dialogPresentations = + parcel.createTypedArrayList(RemoteViews.CREATOR); final ArrayList<InlinePresentation> inlinePresentations = parcel.createTypedArrayList(InlinePresentation.CREATOR); final ArrayList<InlinePresentation> inlineTooltipPresentations = parcel.createTypedArrayList(InlinePresentation.CREATOR); final ArrayList<DatasetFieldFilter> filters = parcel.createTypedArrayList(DatasetFieldFilter.CREATOR); - final ClipData fieldContent = parcel.readParcelable(null, android.content.ClipData.class); - final IntentSender authentication = parcel.readParcelable(null, android.content.IntentSender.class); + final ClipData fieldContent = parcel.readParcelable(null, + android.content.ClipData.class); + final IntentSender authentication = parcel.readParcelable(null, + android.content.IntentSender.class); final String datasetId = parcel.readString(); // Always go through the builder to ensure the data ingested by // the system obeys the contract of the builder to avoid attacks // using specially crafted parcels. - final Builder builder = (presentation != null) ? new Builder(presentation) - : new Builder(); - if (inlinePresentation != null) { + final Builder builder; + if (presentation != null || inlinePresentation != null || dialogPresentation != null) { + final Presentations.Builder presentationsBuilder = new Presentations.Builder(); + if (presentation != null) { + presentationsBuilder.setMenuPresentation(presentation); + } + if (inlinePresentation != null) { + presentationsBuilder.setInlinePresentation(inlinePresentation); + } if (inlineTooltipPresentation != null) { - builder.setInlinePresentation(inlinePresentation, inlineTooltipPresentation); - } else { - builder.setInlinePresentation(inlinePresentation); + presentationsBuilder.setInlineTooltipPresentation(inlineTooltipPresentation); + } + if (dialogPresentation != null) { + presentationsBuilder.setDialogPresentation(dialogPresentation); } + builder = new Builder(presentationsBuilder.build()); + } else { + builder = new Builder(); } if (fieldContent != null) { @@ -954,13 +1118,15 @@ public final class Dataset implements Parcelable { final AutofillId id = ids.get(i); final AutofillValue value = values.get(i); final RemoteViews fieldPresentation = presentations.get(i); + final RemoteViews fieldDialogPresentation = dialogPresentations.get(i); final InlinePresentation fieldInlinePresentation = i < inlinePresentationsSize ? inlinePresentations.get(i) : null; final InlinePresentation fieldInlineTooltipPresentation = i < inlinePresentationsSize ? inlineTooltipPresentations.get(i) : null; final DatasetFieldFilter filter = filters.get(i); builder.setLifeTheUniverseAndEverything(id, value, fieldPresentation, - fieldInlinePresentation, fieldInlineTooltipPresentation, filter); + fieldInlinePresentation, fieldInlineTooltipPresentation, filter, + fieldDialogPresentation); } builder.setAuthentication(authentication); builder.setId(datasetId); @@ -986,7 +1152,7 @@ public final class Dataset implements Parcelable { @Nullable public final Pattern pattern; - private DatasetFieldFilter(@Nullable Pattern pattern) { + DatasetFieldFilter(@Nullable Pattern pattern) { this.pattern = pattern; } diff --git a/core/java/android/service/autofill/Field.java b/core/java/android/service/autofill/Field.java new file mode 100644 index 000000000000..b7c0d82f41f5 --- /dev/null +++ b/core/java/android/service/autofill/Field.java @@ -0,0 +1,165 @@ +/* + * 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 android.service.autofill; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.view.autofill.AutofillId; +import android.view.autofill.AutofillValue; + +import com.android.internal.util.DataClass; + +import java.util.regex.Pattern; + +/** + * This class is used to set all information of a field. Such as the + * {@link AutofillId} of the field, the {@link AutofillValue} to be autofilled, + * a <a href="#Filtering">explicit filter</a>, and presentations to be visualized, + * etc. + */ +public final class Field { + + /** + * The value to be autofilled. Pass {@code null} if you do not have the value + * but the target view is a logical part of the dataset. For example, if the + * dataset needs authentication and you have no access to the value. + */ + private @Nullable AutofillValue mValue; + + /** + * Regex used to determine if the dataset should be shown in the autofill UI; + * when {@code null}, it disables filtering on that dataset (this is the recommended + * approach when {@code value} is not {@code null} and field contains sensitive data + * such as passwords). + * + * @see Dataset.DatasetFieldFilter + * @hide + */ + private @Nullable Dataset.DatasetFieldFilter mFilter; + + /** + * The presentations used to visualize this field in Autofill UI. + */ + private @Nullable Presentations mPresentations; + + + /* package-private */ Field( + @Nullable AutofillValue value, + @Nullable Dataset.DatasetFieldFilter filter, + @Nullable Presentations presentations) { + this.mValue = value; + this.mFilter = filter; + this.mPresentations = presentations; + } + + /** + * The value to be autofilled. Pass {@code null} if you do not have the value + * but the target view is a logical part of the dataset. For example, if the + * dataset needs authentication and you have no access to the value. + */ + @DataClass.Generated.Member + public @Nullable AutofillValue getValue() { + return mValue; + } + + /** + * Regex used to determine if the dataset should be shown in the autofill UI; + * when {@code null}, it disables filtering on that dataset (this is the recommended + * approach when {@code value} is not {@code null} and field contains sensitive data + * such as passwords). + * + * @see Dataset.DatasetFieldFilter + * @hide + */ + public @Nullable Dataset.DatasetFieldFilter getFilter() { + return mFilter; + } + + /** + * The presentations used to visualize this field in Autofill UI. + */ + public @Nullable Presentations getPresentations() { + return mPresentations; + } + + /** + * A builder for {@link Field} + */ + public static final class Builder { + + private @Nullable AutofillValue mValue = null; + private @Nullable Dataset.DatasetFieldFilter mFilter = null; + private @Nullable Presentations mPresentations = null; + private boolean mDestroyed = false; + + public Builder() { + } + + /** + * The value to be autofilled. Pass {@code null} if you do not have the value + * but the target view is a logical part of the dataset. For example, if the + * dataset needs authentication and you have no access to the value. + */ + public @NonNull Builder setValue(@NonNull AutofillValue value) { + checkNotUsed(); + mValue = value; + return this; + } + + /** + * Regex used to determine if the dataset should be shown in the autofill UI; + * when {@code null}, it disables filtering on that dataset (this is the recommended + * approach when {@code value} is not {@code null} and field contains sensitive data + * such as passwords). + */ + @SuppressLint("MissingGetterMatchingBuilder") + public @NonNull Builder setFilter(@Nullable Pattern value) { + checkNotUsed(); + mFilter = new Dataset.DatasetFieldFilter(value); + return this; + } + + /** + * The presentations used to visualize this field in Autofill UI. + */ + public @NonNull Builder setPresentations(@NonNull Presentations value) { + checkNotUsed(); + mPresentations = value; + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull Field build() { + checkNotUsed(); + mDestroyed = true; // Mark builder used + + Field o = new Field( + mValue, + mFilter, + mPresentations); + return o; + } + + private void checkNotUsed() { + if (mDestroyed) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } +} diff --git a/core/java/android/service/autofill/FillRequest.java b/core/java/android/service/autofill/FillRequest.java index 43bd4102ffb5..f820f0389f0d 100644 --- a/core/java/android/service/autofill/FillRequest.java +++ b/core/java/android/service/autofill/FillRequest.java @@ -98,6 +98,12 @@ public final class FillRequest implements Parcelable { // The flag value 0x20 has been defined in AutofillManager. + /** + * Indicates the request is coming from the activity just started. + * @hide + */ + public static final @RequestFlags int FLAG_ACTIVITY_START = 0x40; + /** @hide */ public static final int INVALID_REQUEST_ID = Integer.MIN_VALUE; @@ -160,13 +166,13 @@ public final class FillRequest implements Parcelable { - // Code below generated by codegen v1.0.15. + // Code below generated by codegen v1.0.23. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code // // To regenerate run: - // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/FillRequest.java + // $ codegen $ANDROID_BUILD_TOP/./frameworks/base/core/java/android/service/autofill/FillRequest.java // // To exclude the generated code from IntelliJ auto-formatting enable (one-time): // Settings > Editor > Code Style > Formatter Control @@ -178,7 +184,8 @@ public final class FillRequest implements Parcelable { FLAG_MANUAL_REQUEST, FLAG_COMPATIBILITY_MODE_REQUEST, FLAG_PASSWORD_INPUT_TYPE, - FLAG_VIEW_NOT_FOCUSED + FLAG_VIEW_NOT_FOCUSED, + FLAG_ACTIVITY_START }) @Retention(RetentionPolicy.SOURCE) @DataClass.Generated.Member @@ -202,6 +209,8 @@ public final class FillRequest implements Parcelable { return "FLAG_PASSWORD_INPUT_TYPE"; case FLAG_VIEW_NOT_FOCUSED: return "FLAG_VIEW_NOT_FOCUSED"; + case FLAG_ACTIVITY_START: + return "FLAG_ACTIVITY_START"; default: return Integer.toHexString(value); } } @@ -264,7 +273,8 @@ public final class FillRequest implements Parcelable { FLAG_MANUAL_REQUEST | FLAG_COMPATIBILITY_MODE_REQUEST | FLAG_PASSWORD_INPUT_TYPE - | FLAG_VIEW_NOT_FOCUSED); + | FLAG_VIEW_NOT_FOCUSED + | FLAG_ACTIVITY_START); this.mInlineSuggestionsRequest = inlineSuggestionsRequest; onConstructed(); @@ -384,7 +394,7 @@ public final class FillRequest implements Parcelable { byte flg = in.readByte(); int id = in.readInt(); List<FillContext> fillContexts = new ArrayList<>(); - in.readParcelableList(fillContexts, FillContext.class.getClassLoader(), android.service.autofill.FillContext.class); + in.readParcelableList(fillContexts, FillContext.class.getClassLoader()); Bundle clientState = (flg & 0x4) == 0 ? null : in.readBundle(); int flags = in.readInt(); InlineSuggestionsRequest inlineSuggestionsRequest = (flg & 0x10) == 0 ? null : (InlineSuggestionsRequest) in.readTypedObject(InlineSuggestionsRequest.CREATOR); @@ -401,7 +411,8 @@ public final class FillRequest implements Parcelable { FLAG_MANUAL_REQUEST | FLAG_COMPATIBILITY_MODE_REQUEST | FLAG_PASSWORD_INPUT_TYPE - | FLAG_VIEW_NOT_FOCUSED); + | FLAG_VIEW_NOT_FOCUSED + | FLAG_ACTIVITY_START); this.mInlineSuggestionsRequest = inlineSuggestionsRequest; onConstructed(); @@ -422,10 +433,10 @@ public final class FillRequest implements Parcelable { }; @DataClass.Generated( - time = 1589280816805L, - codegenVersion = "1.0.15", + time = 1643052544776L, + codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/service/autofill/FillRequest.java", - inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_PASSWORD_INPUT_TYPE\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_VIEW_NOT_FOCUSED\npublic static final int INVALID_REQUEST_ID\nprivate final int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)") + inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_PASSWORD_INPUT_TYPE\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_VIEW_NOT_FOCUSED\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_ACTIVITY_START\npublic static final int INVALID_REQUEST_ID\nprivate final int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java index d94988ebea66..903e77fcb3d7 100644 --- a/core/java/android/service/autofill/FillResponse.java +++ b/core/java/android/service/autofill/FillResponse.java @@ -79,11 +79,14 @@ public final class FillResponse implements Parcelable { private final @Nullable RemoteViews mPresentation; private final @Nullable InlinePresentation mInlinePresentation; private final @Nullable InlinePresentation mInlineTooltipPresentation; + private final @Nullable RemoteViews mDialogPresentation; + private final @Nullable RemoteViews mDialogHeader; private final @Nullable RemoteViews mHeader; private final @Nullable RemoteViews mFooter; private final @Nullable IntentSender mAuthentication; private final @Nullable AutofillId[] mAuthenticationIds; private final @Nullable AutofillId[] mIgnoredIds; + private final @Nullable AutofillId[] mFillDialogTriggerIds; private final long mDisableDuration; private final @Nullable AutofillId[] mFieldClassificationIds; private final int mFlags; @@ -99,10 +102,13 @@ public final class FillResponse implements Parcelable { mPresentation = builder.mPresentation; mInlinePresentation = builder.mInlinePresentation; mInlineTooltipPresentation = builder.mInlineTooltipPresentation; + mDialogPresentation = builder.mDialogPresentation; + mDialogHeader = builder.mDialogHeader; mHeader = builder.mHeader; mFooter = builder.mFooter; mAuthentication = builder.mAuthentication; mAuthenticationIds = builder.mAuthenticationIds; + mFillDialogTriggerIds = builder.mFillDialogTriggerIds; mIgnoredIds = builder.mIgnoredIds; mDisableDuration = builder.mDisableDuration; mFieldClassificationIds = builder.mFieldClassificationIds; @@ -144,6 +150,16 @@ public final class FillResponse implements Parcelable { } /** @hide */ + public @Nullable RemoteViews getDialogPresentation() { + return mDialogPresentation; + } + + /** @hide */ + public @Nullable RemoteViews getDialogHeader() { + return mDialogHeader; + } + + /** @hide */ public @Nullable RemoteViews getHeader() { return mHeader; } @@ -164,6 +180,11 @@ public final class FillResponse implements Parcelable { } /** @hide */ + public @Nullable AutofillId[] getFillDialogTriggerIds() { + return mFillDialogTriggerIds; + } + + /** @hide */ public @Nullable AutofillId[] getIgnoredIds() { return mIgnoredIds; } @@ -229,6 +250,8 @@ public final class FillResponse implements Parcelable { private RemoteViews mPresentation; private InlinePresentation mInlinePresentation; private InlinePresentation mInlineTooltipPresentation; + private RemoteViews mDialogPresentation; + private RemoteViews mDialogHeader; private RemoteViews mHeader; private RemoteViews mFooter; private IntentSender mAuthentication; @@ -236,6 +259,7 @@ public final class FillResponse implements Parcelable { private AutofillId[] mIgnoredIds; private long mDisableDuration; private AutofillId[] mFieldClassificationIds; + private AutofillId[] mFillDialogTriggerIds; private int mFlags; private boolean mDestroyed; private UserData mUserData; @@ -243,7 +267,7 @@ public final class FillResponse implements Parcelable { private boolean mSupportsInlineSuggestions; /** - * Triggers a custom UI before before autofilling the screen with any data set in this + * Triggers a custom UI before autofilling the screen with any data set in this * response. * * <p><b>Note:</b> Although the name of this method suggests that it should be used just for @@ -277,7 +301,7 @@ public final class FillResponse implements Parcelable { * example a credit card whose CVV needs to be entered. * * <p>If you provide an authentication intent you must also provide a presentation - * which is used to visualize visualize the response for triggering the authentication + * which is used to visualize the response for triggering the authentication * flow. * * <p><b>Note:</b> Do not make the provided pending intent @@ -306,7 +330,11 @@ public final class FillResponse implements Parcelable { * {@link #setFooter(RemoteViews) footer} are already set for this builder. * * @see android.app.PendingIntent#getIntentSender() + * @deprecated Use + * {@link #setAuthentication(AutofillId[], IntentSender, Presentations)} + * instead. */ + @Deprecated @NonNull public Builder setAuthentication(@NonNull AutofillId[] ids, @Nullable IntentSender authentication, @Nullable RemoteViews presentation) { @@ -327,7 +355,7 @@ public final class FillResponse implements Parcelable { } /** - * Triggers a custom UI before before autofilling the screen with any data set in this + * Triggers a custom UI before autofilling the screen with any data set in this * response. * * <p><b>Note:</b> Although the name of this method suggests that it should be used just for @@ -365,7 +393,11 @@ public final class FillResponse implements Parcelable { * {@link #setFooter(RemoteViews) footer} are already set for this builder. * * @see android.app.PendingIntent#getIntentSender() + * @deprecated Use + * {@link #setAuthentication(AutofillId[], IntentSender, Presentations)} + * instead. */ + @Deprecated @NonNull public Builder setAuthentication(@NonNull AutofillId[] ids, @Nullable IntentSender authentication, @Nullable RemoteViews presentation, @@ -374,13 +406,18 @@ public final class FillResponse implements Parcelable { } /** - * Triggers a custom UI before before autofilling the screen with any data set in this + * Triggers a custom UI before autofilling the screen with any data set in this * response. * * <p>This method like * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews, InlinePresentation)} * but allows setting an {@link InlinePresentation} for the inline suggestion tooltip. + * + * @deprecated Use + * {@link #setAuthentication(AutofillId[], IntentSender, Presentations)} + * instead. */ + @Deprecated @NonNull public Builder setAuthentication(@SuppressLint("ArrayReturn") @NonNull AutofillId[] ids, @Nullable IntentSender authentication, @Nullable RemoteViews presentation, @@ -388,6 +425,105 @@ public final class FillResponse implements Parcelable { @Nullable InlinePresentation inlineTooltipPresentation) { throwIfDestroyed(); throwIfDisableAutofillCalled(); + return setAuthentication(ids, authentication, presentation, + inlinePresentation, inlineTooltipPresentation, null); + } + + /** + * Triggers a custom UI before autofilling the screen with any data set in this + * response. + * + * <p><b>Note:</b> Although the name of this method suggests that it should be used just for + * authentication flow, it can be used for other advanced flows; see {@link AutofillService} + * for examples. + * + * <p>This is typically useful when a user interaction is required to unlock their + * data vault if you encrypt the data set labels and data set data. It is recommended + * to encrypt only the sensitive data and not the data set labels which would allow + * auth on the data set level leading to a better user experience. Note that if you + * use sensitive data as a label, for example an email address, then it should also + * be encrypted. The provided {@link android.app.PendingIntent intent} must be an + * {@link Activity} which implements your authentication flow. Also if you provide an auth + * intent you also need to specify the presentation view to be shown in the fill UI + * for the user to trigger your authentication flow. + * + * <p>When a user triggers autofill, the system launches the provided intent + * whose extras will have the + * {@link android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen + * content} and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE + * client state}. Once you complete your authentication flow you should set the + * {@link Activity} result to {@link android.app.Activity#RESULT_OK} and set the + * {@link android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra + * with the fully populated {@link FillResponse response} (or {@code null} if the screen + * cannot be autofilled). + * + * <p>For example, if you provided an empty {@link FillResponse response} because the + * user's data was locked and marked that the response needs an authentication then + * in the response returned if authentication succeeds you need to provide all + * available data sets some of which may need to be further authenticated, for + * example a credit card whose CVV needs to be entered. + * + * <p>If you provide an authentication intent you must also provide a presentation + * which is used to visualize the response for triggering the authentication + * flow. + * + * <p><b>Note:</b> Do not make the provided pending intent + * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the + * platform needs to fill in the authentication arguments. + * + * <p><b>Note:</b> {@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} does + * not work with {@link InlinePresentation}.</p> + * + * @param ids id of Views that when focused will display the authentication UI. + * @param authentication Intent to an activity with your authentication flow. + * @param presentations The presentations to visualize the response. + * + * @throws IllegalArgumentException if any of the following occurs: + * <ul> + * <li>{@code ids} is {@code null}</li> + * <li>{@code ids} is empty</li> + * <li>{@code ids} contains a {@code null} element</li> + * <li>{@code authentication} is {@code null}, but either or both of + * {@code presentations.getPresentation()} and + * {@code presentations.getInlinePresentation()} is non-{@code null}</li> + * <li>{@code authentication} is non-{{@code null}, but both + * {@code presentations.getPresentation()} and + * {@code presentations.getInlinePresentation()} are {@code null}</li> + * </ul> + * + * @throws IllegalStateException if a {@link #setHeader(RemoteViews) header} or a + * {@link #setFooter(RemoteViews) footer} are already set for this builder. + * + * @return This builder. + */ + @NonNull + public Builder setAuthentication(@SuppressLint("ArrayReturn") @NonNull AutofillId[] ids, + @Nullable IntentSender authentication, + @Nullable Presentations presentations) { + throwIfDestroyed(); + throwIfDisableAutofillCalled(); + if (presentations == null) { + return setAuthentication(ids, authentication, null, null, null, null); + } + return setAuthentication(ids, authentication, + presentations.getMenuPresentation(), + presentations.getInlinePresentation(), + presentations.getInlineTooltipPresentation(), + presentations.getDialogPresentation()); + } + + /** + * Triggers a custom UI before autofilling the screen with any data set in this + * response. + */ + @NonNull + private Builder setAuthentication(@SuppressLint("ArrayReturn") @NonNull AutofillId[] ids, + @Nullable IntentSender authentication, @Nullable RemoteViews presentation, + @Nullable InlinePresentation inlinePresentation, + @Nullable InlinePresentation inlineTooltipPresentation, + @Nullable RemoteViews dialogPresentation) { + throwIfDestroyed(); + throwIfDisableAutofillCalled(); if (mHeader != null || mFooter != null) { throw new IllegalStateException("Already called #setHeader() or #setFooter()"); } @@ -400,6 +536,7 @@ public final class FillResponse implements Parcelable { mPresentation = presentation; mInlinePresentation = inlinePresentation; mInlineTooltipPresentation = inlineTooltipPresentation; + mDialogPresentation = dialogPresentation; mAuthenticationIds = assertValid(ids); return this; } @@ -552,7 +689,7 @@ public final class FillResponse implements Parcelable { * * @throws IllegalArgumentException if {@code duration} is not a positive number. * @throws IllegalStateException if either {@link #addDataset(Dataset)}, - * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, + * {@link #setAuthentication(AutofillId[], IntentSender, Presentations)}, * {@link #setSaveInfo(SaveInfo)}, {@link #setClientState(Bundle)}, or * {@link #setFieldClassificationIds(AutofillId...)} was already called. */ @@ -591,8 +728,8 @@ public final class FillResponse implements Parcelable { * @return this builder * * @throws IllegalStateException if an - * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) authentication} was - * already set for this builder. + * {@link #setAuthentication(AutofillId[], IntentSender, Presentations) + * authentication} was already set for this builder. */ // TODO(b/69796626): make it sticky / update javadoc @NonNull @@ -623,7 +760,7 @@ public final class FillResponse implements Parcelable { * @return this builder * * @throws IllegalStateException if the FillResponse - * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) + * {@link #setAuthentication(AutofillId[], IntentSender, Presentations) * requires authentication}. */ // TODO(b/69796626): make it sticky / update javadoc @@ -643,7 +780,7 @@ public final class FillResponse implements Parcelable { * * @return this builder * @throws IllegalStateException if the FillResponse - * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) + * {@link #setAuthentication(AutofillId[], IntentSender, Presentations) * requires authentication}. */ @NonNull @@ -674,13 +811,46 @@ public final class FillResponse implements Parcelable { } /** + * Sets the presentation of header in fill dialog UI. The header should have + * a prompt for what datasets are shown in the dialog. If this is not set, + * the dialog only shows your application icon. + * + * More details about the fill dialog, see + * <a href="Dataset.html#FillDialogUI">fill dialog UI</a> + */ + @NonNull + public Builder setDialogHeader(@NonNull RemoteViews header) { + throwIfDestroyed(); + Objects.requireNonNull(header); + mDialogHeader = header; + return this; + } + + /** + * Sets which fields are used for the fill dialog UI. + * + * More details about the fill dialog, see + * <a href="Dataset.html#FillDialogUI">fill dialog UI</a> + * + * @throws IllegalStateException if {@link #build()} was already called. + * @throws NullPointerException if {@code ids} or any element on it is {@code null}. + */ + @NonNull + public Builder setFillDialogTriggerIds(@NonNull AutofillId... ids) { + throwIfDestroyed(); + Preconditions.checkArrayElementsNotNull(ids, "ids"); + mFillDialogTriggerIds = ids; + return this; + } + + /** * Builds a new {@link FillResponse} instance. * * @throws IllegalStateException if any of the following conditions occur: * <ol> * <li>{@link #build()} was already called. * <li>No call was made to {@link #addDataset(Dataset)}, - * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, + * {@link #setAuthentication(AutofillId[], IntentSender, Presentations)}, * {@link #setSaveInfo(SaveInfo)}, {@link #disableAutofill(long)}, * {@link #setClientState(Bundle)}, * or {@link #setFieldClassificationIds(AutofillId...)}. @@ -767,6 +937,12 @@ public final class FillResponse implements Parcelable { if (mInlineTooltipPresentation != null) { builder.append(", hasInlineTooltipPresentation"); } + if (mDialogPresentation != null) { + builder.append(", hasDialogPresentation"); + } + if (mDialogHeader != null) { + builder.append(", hasDialogHeader"); + } if (mHeader != null) { builder.append(", hasHeader"); } @@ -779,6 +955,10 @@ public final class FillResponse implements Parcelable { if (mAuthenticationIds != null) { builder.append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds)); } + if (mFillDialogTriggerIds != null) { + builder.append(", fillDialogTriggerIds=") + .append(Arrays.toString(mFillDialogTriggerIds)); + } builder.append(", disableDuration=").append(mDisableDuration); if (mFlags != 0) { builder.append(", flags=").append(mFlags); @@ -815,6 +995,9 @@ public final class FillResponse implements Parcelable { parcel.writeParcelable(mPresentation, flags); parcel.writeParcelable(mInlinePresentation, flags); parcel.writeParcelable(mInlineTooltipPresentation, flags); + parcel.writeParcelable(mDialogPresentation, flags); + parcel.writeParcelable(mDialogHeader, flags); + parcel.writeParcelableArray(mFillDialogTriggerIds, flags); parcel.writeParcelable(mHeader, flags); parcel.writeParcelable(mFooter, flags); parcel.writeParcelable(mUserData, flags); @@ -850,9 +1033,18 @@ public final class FillResponse implements Parcelable { final RemoteViews presentation = parcel.readParcelable(null, android.widget.RemoteViews.class); final InlinePresentation inlinePresentation = parcel.readParcelable(null, android.service.autofill.InlinePresentation.class); final InlinePresentation inlineTooltipPresentation = parcel.readParcelable(null, android.service.autofill.InlinePresentation.class); + final RemoteViews dialogPresentation = parcel.readParcelable(null, android.widget.RemoteViews.class); if (authenticationIds != null) { builder.setAuthentication(authenticationIds, authentication, presentation, - inlinePresentation, inlineTooltipPresentation); + inlinePresentation, inlineTooltipPresentation, dialogPresentation); + } + final RemoteViews dialogHeader = parcel.readParcelable(null, android.widget.RemoteViews.class); + if (dialogHeader != null) { + builder.setDialogHeader(dialogHeader); + } + final AutofillId[] triggerIds = parcel.readParcelableArray(null, AutofillId.class); + if (triggerIds != null) { + builder.setFillDialogTriggerIds(triggerIds); } final RemoteViews header = parcel.readParcelable(null, android.widget.RemoteViews.class); if (header != null) { diff --git a/core/java/android/service/autofill/Presentations.java b/core/java/android/service/autofill/Presentations.java new file mode 100644 index 000000000000..e8ac628ebd12 --- /dev/null +++ b/core/java/android/service/autofill/Presentations.java @@ -0,0 +1,278 @@ +/* + * 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 android.service.autofill; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.widget.RemoteViews; + +import com.android.internal.util.DataClass; + +/** + * Holds presentations used to visualize autofill suggestions for each available UI type. + * + * @see Field + */ +@DataClass(genBuilder = true) +public final class Presentations { + + /** + * The presentation used to visualize this field in fill UI. + * + * <p>Note: Before Android 13, this was referred to simply as "presentation" in the SDK. + * + * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color + * or background color: Autofill on different platforms may have different themes. + */ + private @Nullable RemoteViews mMenuPresentation; + + /** + * The {@link InlinePresentation} used to visualize this dataset as inline suggestions. + * If the dataset supports inline suggestions, this should not be null. + */ + private @Nullable InlinePresentation mInlinePresentation; + + /** + * The presentation used to visualize this field in the + * <a href="Dataset.html#FillDialogUI">fill dialog UI</a> + * + * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color + * or background color: Autofill on different platforms may have different themes. + */ + private @Nullable RemoteViews mDialogPresentation; + + /** + * The {@link InlinePresentation} used to show the tooltip for the + * {@code mInlinePresentation}. If the set this field, the + * {@code mInlinePresentation} should not be null. + */ + private @Nullable InlinePresentation mInlineTooltipPresentation; + + private static RemoteViews defaultMenuPresentation() { + return null; + } + + private static InlinePresentation defaultInlinePresentation() { + return null; + } + + private static RemoteViews defaultDialogPresentation() { + return null; + } + + private static InlinePresentation defaultInlineTooltipPresentation() { + return null; + } + + private void onConstructed() { + if (mMenuPresentation == null + && mInlinePresentation == null + && mDialogPresentation == null) { + throw new IllegalStateException("All presentations are null."); + } + if (mInlineTooltipPresentation != null && mInlinePresentation == null) { + throw new IllegalStateException( + "The inline presentation is required for mInlineTooltipPresentation."); + } + } + + + + // Code below generated by codegen v1.0.23. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/./frameworks/base/core/java/android/service/autofill/Presentations.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + /* package-private */ Presentations( + @Nullable RemoteViews menuPresentation, + @Nullable InlinePresentation inlinePresentation, + @Nullable RemoteViews dialogPresentation, + @Nullable InlinePresentation inlineTooltipPresentation) { + this.mMenuPresentation = menuPresentation; + this.mInlinePresentation = inlinePresentation; + this.mDialogPresentation = dialogPresentation; + this.mInlineTooltipPresentation = inlineTooltipPresentation; + + onConstructed(); + } + + /** + * The presentation used to visualize this field in fill UI. + * + * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color + * or background color: Autofill on different platforms may have different themes. + */ + @DataClass.Generated.Member + public @Nullable RemoteViews getMenuPresentation() { + return mMenuPresentation; + } + + /** + * The {@link InlinePresentation} used to visualize this dataset as inline suggestions. + * If the dataset supports inline suggestions, this should not be null. + */ + @DataClass.Generated.Member + public @Nullable InlinePresentation getInlinePresentation() { + return mInlinePresentation; + } + + /** + * The presentation used to visualize this field in the fill dialog UI. + * + * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color + * or background color: Autofill on different platforms may have different themes. + */ + @DataClass.Generated.Member + public @Nullable RemoteViews getDialogPresentation() { + return mDialogPresentation; + } + + /** + * The {@link InlinePresentation} used to show the tooltip for the + * {@code mInlinePresentation}. If the set this field, the + * {@code mInlinePresentation} should not be null. + */ + @DataClass.Generated.Member + public @Nullable InlinePresentation getInlineTooltipPresentation() { + return mInlineTooltipPresentation; + } + + /** + * A builder for {@link Presentations} + */ + @SuppressWarnings("WeakerAccess") + @DataClass.Generated.Member + public static final class Builder { + + private @Nullable RemoteViews mMenuPresentation; + private @Nullable InlinePresentation mInlinePresentation; + private @Nullable RemoteViews mDialogPresentation; + private @Nullable InlinePresentation mInlineTooltipPresentation; + + private long mBuilderFieldsSet = 0L; + + public Builder() { + } + + /** + * The presentation used to visualize this field in fill UI. + * + * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color + * or background color: Autofill on different platforms may have different themes. + */ + @DataClass.Generated.Member + public @NonNull Builder setMenuPresentation(@NonNull RemoteViews value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1; + mMenuPresentation = value; + return this; + } + + /** + * The {@link InlinePresentation} used to visualize this dataset as inline suggestions. + * If the dataset supports inline suggestions, this should not be null. + */ + @DataClass.Generated.Member + public @NonNull Builder setInlinePresentation(@NonNull InlinePresentation value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; + mInlinePresentation = value; + return this; + } + + /** + * The presentation used to visualize this field in the fill dialog UI. + * + * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color + * or background color: Autofill on different platforms may have different themes. + */ + @DataClass.Generated.Member + public @NonNull Builder setDialogPresentation(@NonNull RemoteViews value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x4; + mDialogPresentation = value; + return this; + } + + /** + * The {@link InlinePresentation} used to show the tooltip for the + * {@code mInlinePresentation}. If the set this field, the + * {@code mInlinePresentation} should not be null. + */ + @DataClass.Generated.Member + public @NonNull Builder setInlineTooltipPresentation(@NonNull InlinePresentation value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x8; + mInlineTooltipPresentation = value; + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull Presentations build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x10; // Mark builder used + + if ((mBuilderFieldsSet & 0x1) == 0) { + mMenuPresentation = defaultMenuPresentation(); + } + if ((mBuilderFieldsSet & 0x2) == 0) { + mInlinePresentation = defaultInlinePresentation(); + } + if ((mBuilderFieldsSet & 0x4) == 0) { + mDialogPresentation = defaultDialogPresentation(); + } + if ((mBuilderFieldsSet & 0x8) == 0) { + mInlineTooltipPresentation = defaultInlineTooltipPresentation(); + } + Presentations o = new Presentations( + mMenuPresentation, + mInlinePresentation, + mDialogPresentation, + mInlineTooltipPresentation); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x10) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } + + @DataClass.Generated( + time = 1643083242164L, + codegenVersion = "1.0.23", + sourceFile = "frameworks/base/core/java/android/service/autofill/Presentations.java", + inputSignatures = "private @android.annotation.Nullable android.widget.RemoteViews mMenuPresentation\nprivate @android.annotation.Nullable android.service.autofill.InlinePresentation mInlinePresentation\nprivate @android.annotation.Nullable android.widget.RemoteViews mDialogPresentation\nprivate @android.annotation.Nullable android.service.autofill.InlinePresentation mInlineTooltipPresentation\nprivate static android.widget.RemoteViews defaultMenuPresentation()\nprivate static android.service.autofill.InlinePresentation defaultInlinePresentation()\nprivate static android.widget.RemoteViews defaultDialogPresentation()\nprivate static android.service.autofill.InlinePresentation defaultInlineTooltipPresentation()\nprivate void onConstructed()\nclass Presentations extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genBuilder=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 75592730067a..4ff7e2297ea0 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -8178,9 +8178,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // to User. Ideally View should handle the event when isVisibleToUser() // becomes true where it should issue notifyViewEntered(). afm.notifyViewEntered(this); + } else { + afm.enableFillRequestActivityStarted(); } } else if (!enter && !isFocused()) { afm.notifyViewExited(this); + } else if (enter) { + afm.enableFillRequestActivityStarted(); } } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index eaa12e53c321..386b277156e3 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -563,6 +563,8 @@ public final class ViewRootImpl implements ViewParent, @Nullable int mContentCaptureEnabled = CONTENT_CAPTURE_ENABLED_NOT_CHECKED; boolean mPerformContentCapture; + boolean mPerformAutoFill; + boolean mReportNextDraw; /** @@ -842,6 +844,7 @@ public final class ViewRootImpl implements ViewParent, mPreviousTransparentRegion = new Region(); mFirst = true; // true for the first time the view is added mPerformContentCapture = true; // also true for the first time the view is added + mPerformAutoFill = true; mAdded = false; mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context); @@ -4353,6 +4356,18 @@ public final class ViewRootImpl implements ViewParent, if (mPerformContentCapture) { performContentCaptureInitialReport(); } + + if (mPerformAutoFill) { + notifyEnterForAutoFillIfNeeded(); + } + } + + private void notifyEnterForAutoFillIfNeeded() { + mPerformAutoFill = false; + final AutofillManager afm = getAutofillManager(); + if (afm != null) { + afm.notifyViewEnteredForActivityStarted(mView); + } } /** diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index bb13c1e78964..60ccf67249b7 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -16,6 +16,7 @@ package android.view.autofill; +import static android.service.autofill.FillRequest.FLAG_ACTIVITY_START; import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE; import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED; @@ -538,6 +539,8 @@ public final class AutofillManager { */ public static final int NO_SESSION = Integer.MAX_VALUE; + private static final boolean HAS_FILL_DIALOG_UI_FEATURE = false; + private final IAutoFillManager mService; private final Object mLock = new Object(); @@ -629,6 +632,29 @@ public final class AutofillManager { @GuardedBy("mLock") @Nullable private Executor mRequestCallbackExecutor; + /** + * Indicates whether there are any fields that need to do a fill request + * after the activity starts. + * + * Note: This field will be set to true multiple times if there are many + * autofillable views. So needs to check mIsFillRequested at the same time to + * avoid re-trigger autofill. + */ + private boolean mRequireAutofill; + + /** + * Indicates whether there is already a field to do a fill request after + * the activity started. + * + * Autofill will automatically trigger a fill request after activity + * start if there is any field is autofillable. But if there is a field that + * triggered autofill, it is unnecessary to trigger again through + * AutofillManager#notifyViewEnteredForActivityStarted. + */ + private boolean mIsFillRequested; + + @Nullable private List<AutofillId> mFillDialogTriggerIds; + /** @hide */ public interface AutofillClient { /** @@ -766,6 +792,8 @@ public final class AutofillManager { mContext = Objects.requireNonNull(context, "context cannot be null"); mService = service; mOptions = context.getAutofillOptions(); + mIsFillRequested = false; + mRequireAutofill = false; if (mOptions != null) { sDebug = (mOptions.loggingLevel & FLAG_ADD_CLIENT_DEBUG) != 0; @@ -1042,6 +1070,39 @@ public final class AutofillManager { notifyViewEntered(view, 0); } + /** + * The view is autofillable, marked to perform a fill request after layout if + * the field does not trigger a fill request. + * + * @hide + */ + public void enableFillRequestActivityStarted() { + mRequireAutofill = true; + } + + private boolean hasFillDialogUiFeature() { + return HAS_FILL_DIALOG_UI_FEATURE; + } + + /** + * Notify autofill to do a fill request while the activity started. + * + * @hide + */ + public void notifyViewEnteredForActivityStarted(@NonNull View view) { + if (!hasAutofillFeature() || !hasFillDialogUiFeature()) { + return; + } + + if (!mRequireAutofill || mIsFillRequested) { + return; + } + + int flags = FLAG_ACTIVITY_START; + flags |= FLAG_VIEW_NOT_FOCUSED; + notifyViewEntered(view, flags); + } + @GuardedBy("mLock") private boolean shouldIgnoreViewEnteredLocked(@NonNull AutofillId id, int flags) { if (isDisabledByServiceLocked()) { @@ -1082,6 +1143,7 @@ public final class AutofillManager { } AutofillCallback callback; synchronized (mLock) { + mIsFillRequested = true; callback = notifyViewEnteredLocked(view, flags); } @@ -2026,6 +2088,8 @@ public final class AutofillManager { mFillableIds = null; mSaveTriggerId = null; mIdShownFillUi = null; + mIsFillRequested = false; + mRequireAutofill = false; if (resetEnteredIds) { mEnteredIds = null; } @@ -3031,6 +3095,29 @@ public final class AutofillManager { client.autofillClientRunOnUiThread(runnable); } + private void setFillDialogTriggerIds(@Nullable List<AutofillId> ids) { + mFillDialogTriggerIds = ids; + } + + /** + * Checks the id of autofill whether supported the fill dialog. + * + * @hide + */ + public boolean isShowFillDialog(AutofillId id) { + if (!hasFillDialogUiFeature() || mFillDialogTriggerIds == null) { + return false; + } + final int size = mFillDialogTriggerIds.size(); + for (int i = 0; i < size; i++) { + AutofillId fillId = mFillDialogTriggerIds.get(i); + if (fillId.equalsIgnoreSession(id)) { + return true; + } + } + return false; + } + /** * Implementation of the accessibility based compatibility. */ @@ -3736,6 +3823,13 @@ public final class AutofillManager { new FillCallback(callback, id)); } } + + public void notifyFillDialogTriggerIds(List<AutofillId> ids) { + final AutofillManager afm = mAfm.get(); + if (afm != null) { + afm.post(() -> afm.setFillDialogTriggerIds(ids)); + } + } } private static final class AugmentedAutofillManagerClient diff --git a/core/java/android/view/autofill/IAutoFillManagerClient.aidl b/core/java/android/view/autofill/IAutoFillManagerClient.aidl index 64507aac54cb..2e5967cc32d1 100644 --- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl +++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl @@ -148,4 +148,9 @@ oneway interface IAutoFillManagerClient { */ void requestFillFromClient(int id, in InlineSuggestionsRequest request, in IFillCallback callback); + + /** + * Notifies autofill ids that require to show the fill dialog. + */ + void notifyFillDialogTriggerIds(in List<AutofillId> ids); } diff --git a/core/res/res/layout/autofill_fill_dialog.xml b/core/res/res/layout/autofill_fill_dialog.xml new file mode 100644 index 000000000000..cce9593f1f2a --- /dev/null +++ b/core/res/res/layout/autofill_fill_dialog.xml @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<!-- NOTE: outer layout is required to provide proper shadow. --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/autofill_dialog_picker" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/autofill_save_outer_top_margin" + android:padding="@dimen/autofill_save_outer_top_padding" + android:elevation="@dimen/autofill_elevation" + android:background="?android:attr/colorBackground" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:paddingStart="@dimen/autofill_save_inner_padding" + android:paddingEnd="@dimen/autofill_save_inner_padding" + android:orientation="vertical"> + + <ImageView + android:id="@+id/autofill_service_icon" + android:scaleType="fitStart" + android:visibility="gone" + android:layout_width="@dimen/autofill_dialog_icon_size" + android:layout_height="@dimen/autofill_dialog_icon_size"/> + + <LinearLayout + android:id="@+id/autofill_dialog_header" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:paddingStart="@dimen/autofill_save_inner_padding" + android:paddingEnd="@dimen/autofill_save_inner_padding" + android:visibility="gone" + android:foreground="?attr/listChoiceBackgroundIndicator" + style="@style/AutofillDatasetPicker" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/autofill_dialog_container" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:paddingStart="@dimen/autofill_save_inner_padding" + android:paddingEnd="@dimen/autofill_save_inner_padding" + android:visibility="gone" + android:foreground="?attr/listChoiceBackgroundIndicator" + style="@style/AutofillDatasetPicker" /> + + <ListView + android:id="@+id/autofill_dialog_list" + android:layout_weight="1" + android:layout_width="fill_parent" + android:layout_height="0dp" + android:drawSelectorOnTop="true" + android:clickable="true" + android:divider="@null" + android:visibility="gone" + android:paddingStart="@dimen/autofill_save_inner_padding" + android:paddingEnd="@dimen/autofill_save_inner_padding" + android:foreground="?attr/listChoiceBackgroundIndicator" + style="@style/AutofillDatasetPicker" /> + + <com.android.internal.widget.ButtonBarLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:padding="@dimen/autofill_save_button_bar_padding" + android:clipToPadding="false" + android:orientation="horizontal"> + + <Button + android:id="@+id/autofill_dialog_no" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="?android:attr/buttonBarButtonStyle" + android:text="@string/dismiss_action"> + </Button> + + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1" + android:visibility="invisible"> + </Space> + + <Button + android:id="@+id/autofill_dialog_yes" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/Widget.DeviceDefault.Button.Colored" + android:text="@string/autofill_save_yes" + android:visibility="gone" > + </Button> + + </com.android.internal.widget.ButtonBarLayout> + +</LinearLayout> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 771c0724fb01..4874e6529620 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -877,6 +877,9 @@ <!-- Maximum number of datasets that are visible in the UX picker without scrolling --> <integer name="autofill_max_visible_datasets">3</integer> + <!-- Size of an icon in the Autolfill fill dialog --> + <dimen name="autofill_dialog_icon_size">56dp</dimen> + <!-- Size of a slice shortcut view --> <dimen name="slice_shortcut_size">56dp</dimen> <!-- Size of action icons in a slice --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 8d51dbeec690..2327404af4c8 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3510,6 +3510,7 @@ <java-symbol type="layout" name="autofill_dataset_picker"/> <java-symbol type="layout" name="autofill_dataset_picker_fullscreen"/> <java-symbol type="layout" name="autofill_dataset_picker_header_footer"/> + <java-symbol type="layout" name="autofill_fill_dialog"/> <java-symbol type="id" name="autofill" /> <java-symbol type="id" name="autofill_dataset_footer"/> <java-symbol type="id" name="autofill_dataset_header"/> @@ -3522,6 +3523,13 @@ <java-symbol type="id" name="autofill_save_no" /> <java-symbol type="id" name="autofill_save_title" /> <java-symbol type="id" name="autofill_save_yes" /> + <java-symbol type="id" name="autofill_service_icon" /> + <java-symbol type="id" name="autofill_dialog_picker"/> + <java-symbol type="id" name="autofill_dialog_header"/> + <java-symbol type="id" name="autofill_dialog_container"/> + <java-symbol type="id" name="autofill_dialog_list"/> + <java-symbol type="id" name="autofill_dialog_no" /> + <java-symbol type="id" name="autofill_dialog_yes" /> <java-symbol type="string" name="autofill_error_cannot_autofill" /> <java-symbol type="string" name="autofill_picker_no_suggestions" /> <java-symbol type="string" name="autofill_picker_some_suggestions" /> diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index aa42e8deb581..1cff3744687e 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -284,7 +284,9 @@ final class AutofillManagerServiceImpl } final Session session = mSessions.get(sessionId); if (session != null && uid == session.uid) { - session.setAuthenticationResultLocked(data, authenticationId); + synchronized (session.mLock) { + session.setAuthenticationResultLocked(data, authenticationId); + } } } @@ -374,7 +376,9 @@ final class AutofillManagerServiceImpl + " hc=" + hasCallback + " f=" + flags + " aa=" + forAugmentedAutofillOnly; mMaster.logRequestLocked(historyItem); - newSession.updateLocked(autofillId, virtualBounds, value, ACTION_START_SESSION, flags); + synchronized (newSession.mLock) { + newSession.updateLocked(autofillId, virtualBounds, value, ACTION_START_SESSION, flags); + } if (forAugmentedAutofillOnly) { // Must embed the flag in the response, at the high-end side of the long. diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index a4bf52a3ed1b..095c1fc80d3e 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -17,6 +17,7 @@ package com.android.server.autofill; import static android.service.autofill.AutofillFieldClassificationService.EXTRA_SCORES; +import static android.service.autofill.FillRequest.FLAG_ACTIVITY_START; import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE; import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED; @@ -149,9 +150,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState private static final String EXTRA_REQUEST_ID = "android.service.autofill.extra.REQUEST_ID"; + final Object mLock; + private final AutofillManagerServiceImpl mService; private final Handler mHandler; - private final Object mLock; private final AutoFillUI mUi; private final MetricsLogger mMetricsLogger = new MetricsLogger(); @@ -428,6 +430,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState /** Whether the client is using {@link android.view.autofill.AutofillRequestCallback}. */ @GuardedBy("mLock") private boolean mClientSuggestionsEnabled; + + /** Whether the fill dialog UI is disabled. */ + @GuardedBy("mLock") + private boolean mFillDialogDisabled; } /** @@ -860,7 +866,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mService.getRemoteInlineSuggestionRenderServiceLocked(); if ((mSessionFlags.mInlineSupportedByService || mSessionFlags.mClientSuggestionsEnabled) && remoteRenderService != null - && isViewFocusedLocked(flags)) { + && (isViewFocusedLocked(flags) || (isRequestFromActivityStarted(flags)))) { final Consumer<InlineSuggestionsRequest> inlineSuggestionsRequestConsumer; if (mSessionFlags.mClientSuggestionsEnabled) { final int finalRequestId = requestId; @@ -906,6 +912,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState requestAssistStructureLocked(requestId, flags); } + private boolean isRequestFromActivityStarted(int flags) { + return (flags & FLAG_ACTIVITY_START) != 0; + } + @GuardedBy("mLock") private void requestAssistStructureLocked(int requestId, int flags) { try { @@ -1501,6 +1511,23 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState this, intentSender, intent)); } + // AutoFillUiCallback + @Override + public void requestShowSoftInput(AutofillId id) { + IAutoFillManagerClient client = getClient(); + if (client != null) { + try { + client.requestShowSoftInput(id); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending input show up notification", e); + } + } + synchronized (Session.this.mLock) { + // stop to show fill dialog + mSessionFlags.mFillDialogDisabled = true; + } + } + private void notifyFillUiHidden(@NonNull AutofillId autofillId) { synchronized (mLock) { try { @@ -2869,6 +2896,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // View is triggering autofill. mCurrentViewId = viewState.id; viewState.update(value, virtualBounds, flags); + if (!isRequestFromActivityStarted(flags)) { + mSessionFlags.mFillDialogDisabled = true; + } requestNewFillResponseLocked(viewState, ViewState.STATE_STARTED_SESSION, flags); break; case ACTION_VALUE_CHANGED: @@ -2958,6 +2988,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } if (isSameViewEntered) { + setFillDialogDisabledAndStartInput(); return; } @@ -2968,6 +2999,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (Objects.equals(mCurrentViewId, viewState.id)) { if (sVerbose) Slog.v(TAG, "Exiting view " + id); mUi.hideFillUi(this); + mUi.hideFillDialog(this); hideAugmentedAutofillLocked(viewState); // We don't send an empty response to IME so that it doesn't cause UI flicker // on the IME side if it arrives before the input view is finished on the IME. @@ -3148,6 +3180,17 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } + if (requestShowFillDialog(response, filledId, filterText)) { + synchronized (mLock) { + final ViewState currentView = mViewStates.get(mCurrentViewId); + currentView.setState(ViewState.STATE_FILL_DIALOG_SHOWN); + mService.logDatasetShown(id, mClientState); + } + return; + } + + setFillDialogDisabled(); + if (response.supportsInlineSuggestions()) { synchronized (mLock) { if (requestShowInlineSuggestionsLocked(response, filterText)) { @@ -3192,6 +3235,81 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + @GuardedBy("mLock") + private void updateFillDialogTriggerIdsLocked() { + final FillResponse response = getLastResponseLocked(null); + + if (response == null) return; + + final AutofillId[] ids = response.getFillDialogTriggerIds(); + notifyClientFillDialogTriggerIds(ids == null ? null : Arrays.asList(ids)); + } + + private void notifyClientFillDialogTriggerIds(List<AutofillId> fieldIds) { + try { + if (sVerbose) { + Slog.v(TAG, "notifyFillDialogTriggerIds(): " + fieldIds); + } + getClient().notifyFillDialogTriggerIds(fieldIds); + } catch (RemoteException e) { + Slog.w(TAG, "Cannot set trigger ids for fill dialog", e); + } + } + + private boolean isFillDialogUiEnabled() { + // TODO read from Settings or somewhere + final boolean isSettingsEnabledFillDialog = true; + synchronized (Session.this.mLock) { + return isSettingsEnabledFillDialog && !mSessionFlags.mFillDialogDisabled; + } + } + + private void setFillDialogDisabled() { + synchronized (mLock) { + mSessionFlags.mFillDialogDisabled = true; + } + notifyClientFillDialogTriggerIds(null); + } + + private void setFillDialogDisabledAndStartInput() { + if (getUiForShowing().isFillDialogShowing()) { + setFillDialogDisabled(); + final AutofillId id; + synchronized (mLock) { + id = mCurrentViewId; + } + requestShowSoftInput(id); + } + } + + private boolean requestShowFillDialog(FillResponse response, + AutofillId filledId, String filterText) { + if (!isFillDialogUiEnabled()) { + // Unsupported fill dialog UI + return false; + } + + final AutofillId[] ids = response.getFillDialogTriggerIds(); + if (ids == null || !ArrayUtils.contains(ids, filledId)) { + return false; + } + + final Drawable serviceIcon = getServiceIcon(); + + getUiForShowing().showFillDialog(filledId, response, filterText, + mService.getServicePackageName(), mComponentName, serviceIcon, this); + return true; + } + + @SuppressWarnings("GuardedBy") // ErrorProne says we need to use mService.mLock, but it's + // actually the same object as mLock. + // TODO: Expose mService.mLock or redesign instead. + private Drawable getServiceIcon() { + synchronized (mLock) { + return mService.getServiceIconLocked(); + } + } + /** * Returns whether we made a request to show inline suggestions. */ @@ -3584,9 +3702,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mService.getRemoteInlineSuggestionRenderServiceLocked(); if (remoteRenderService != null && (mSessionFlags.mAugmentedAutofillOnly - || !mSessionFlags.mInlineSupportedByService - || mSessionFlags.mExpiredResponse) - && isViewFocusedLocked(flags)) { + || !mSessionFlags.mInlineSupportedByService + || mSessionFlags.mExpiredResponse) + && isViewFocusedLocked(flags) + || isFillDialogUiEnabled()) { if (sDebug) Slog.d(TAG, "Create inline request for augmented autofill"); remoteRenderService.getInlineSuggestionsRendererInfo(new RemoteCallback( (extras) -> { @@ -3642,6 +3761,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mClientState = newClientState != null ? newClientState : newResponse.getClientState(); setViewStatesLocked(newResponse, ViewState.STATE_FILLABLE, false); + updateFillDialogTriggerIdsLocked(); updateTrackedIdsLocked(); if (mCurrentViewId == null) { diff --git a/services/autofill/java/com/android/server/autofill/ViewState.java b/services/autofill/java/com/android/server/autofill/ViewState.java index adb1e3e43731..4a14f1420cf2 100644 --- a/services/autofill/java/com/android/server/autofill/ViewState.java +++ b/services/autofill/java/com/android/server/autofill/ViewState.java @@ -82,6 +82,8 @@ final class ViewState { public static final int STATE_INLINE_DISABLED = 0x8000; /** The View is waiting for an inline suggestions request from IME.*/ public static final int STATE_PENDING_CREATE_INLINE_REQUEST = 0x10000; + /** Fill dialog were shown for this View. */ + public static final int STATE_FILL_DIALOG_SHOWN = 0x20000; public final AutofillId id; diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java index 71c3c16a2c06..056ab92fffb2 100644 --- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java @@ -66,6 +66,7 @@ public final class AutoFillUI { private @Nullable FillUi mFillUi; private @Nullable SaveUi mSaveUi; + private @Nullable DialogFillUi mFillDialog; private @Nullable AutoFillUiCallback mCallback; @@ -90,6 +91,7 @@ public final class AutoFillUI { void startIntentSender(IntentSender intentSender, Intent intent); void dispatchUnhandledKey(AutofillId id, KeyEvent keyEvent); void cancelSession(); + void requestShowSoftInput(AutofillId id); } public AutoFillUI(@NonNull Context context) { @@ -155,6 +157,12 @@ public final class AutoFillUI { } /** + * Hides the fill UI. + */ + public void hideFillDialog(@NonNull AutoFillUiCallback callback) { + mHandler.post(() -> hideFillDialogUiThread(callback)); + } + /** * Filters the options in the fill UI. * * @param filterText The filter prefix. @@ -369,6 +377,62 @@ public final class AutoFillUI { } /** + * Shows the UI asking the user to choose for autofill. + */ + public void showFillDialog(@NonNull AutofillId focusedId, @NonNull FillResponse response, + @Nullable String filterText, @Nullable String servicePackageName, + @NonNull ComponentName componentName, @Nullable Drawable serviceIcon, + @NonNull AutoFillUiCallback callback) { + if (sVerbose) { + Slog.v(TAG, "showFillDialog for " + + componentName.toShortString() + ": " + response); + } + + // TODO: enable LogMaker + + mHandler.post(() -> { + if (callback != mCallback) { + return; + } + hideAllUiThread(callback); + mFillDialog = new DialogFillUi(mContext, response, focusedId, filterText, + serviceIcon, servicePackageName, componentName, mOverlayControl, + mUiModeMgr.isNightMode(), new DialogFillUi.UiCallback() { + @Override + public void onResponsePicked(FillResponse response) { + hideFillDialogUiThread(callback); + if (mCallback != null) { + mCallback.authenticate(response.getRequestId(), + AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED, + response.getAuthentication(), response.getClientState(), + /* authenticateInline= */ false); + } + } + + @Override + public void onDatasetPicked(Dataset dataset) { + hideFillDialogUiThread(callback); + if (mCallback != null) { + final int datasetIndex = response.getDatasets().indexOf(dataset); + mCallback.fill(response.getRequestId(), datasetIndex, dataset); + } + } + + @Override + public void onCanceled() { + hideFillDialogUiThread(callback); + callback.requestShowSoftInput(focusedId); + } + + @Override + public void startIntentSender(IntentSender intentSender) { + mCallback.startIntentSenderAndFinishSession(intentSender); + } + }); + }); + } + + /** * Executes an operation in the pending save UI, if any. */ public void onPendingSaveUi(int operation, @NonNull IBinder token) { @@ -400,6 +464,10 @@ public final class AutoFillUI { return mSaveUi == null ? false : mSaveUi.isShowing(); } + public boolean isFillDialogShowing() { + return mFillDialog == null ? false : mFillDialog.isShowing(); + } + public void dump(PrintWriter pw) { pw.println("Autofill UI"); final String prefix = " "; @@ -417,6 +485,12 @@ public final class AutoFillUI { } else { pw.print(prefix); pw.println("showsSaveUi: false"); } + if (mFillDialog != null) { + pw.print(prefix); pw.println("showsFillDialog: true"); + mFillDialog.dump(pw, prefix2); + } else { + pw.print(prefix); pw.println("showsFillDialog: false"); + } } @android.annotation.UiThread @@ -442,6 +516,14 @@ public final class AutoFillUI { } @android.annotation.UiThread + private void hideFillDialogUiThread(@Nullable AutoFillUiCallback callback) { + if (mFillDialog != null && (callback == null || callback == mCallback)) { + mFillDialog.destroy(); + mFillDialog = null; + } + } + + @android.annotation.UiThread private void destroySaveUiUiThread(@Nullable PendingUi pendingSaveUi, boolean notifyClient) { if (mSaveUi == null) { // Calling destroySaveUiUiThread() twice is normal - it usually happens when the @@ -475,12 +557,14 @@ public final class AutoFillUI { private void destroyAllUiThread(@Nullable PendingUi pendingSaveUi, @Nullable AutoFillUiCallback callback, boolean notifyClient) { hideFillUiUiThread(callback, notifyClient); + hideFillDialogUiThread(callback); destroySaveUiUiThread(pendingSaveUi, notifyClient); } @android.annotation.UiThread private void hideAllUiThread(@Nullable AutoFillUiCallback callback) { hideFillUiUiThread(callback, true); + hideFillDialogUiThread(callback); final PendingUi pendingSaveUi = hideSaveUiUiThread(callback); if (pendingSaveUi != null && pendingSaveUi.getState() == PendingUi.STATE_FINISHED) { if (sDebug) { diff --git a/services/autofill/java/com/android/server/autofill/ui/DialogFillUi.java b/services/autofill/java/com/android/server/autofill/ui/DialogFillUi.java new file mode 100644 index 000000000000..e1229939e2ca --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/ui/DialogFillUi.java @@ -0,0 +1,629 @@ +/* + * 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.server.autofill.ui; + +import static com.android.server.autofill.Helper.sDebug; +import static com.android.server.autofill.Helper.sVerbose; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Dialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.IntentSender; +import android.graphics.drawable.Drawable; +import android.service.autofill.Dataset; +import android.service.autofill.FillResponse; +import android.text.TextUtils; +import android.util.PluralsMessageFormatter; +import android.util.Slog; +import android.view.ContextThemeWrapper; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; +import android.view.autofill.AutofillId; +import android.view.autofill.AutofillValue; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.RemoteViews; +import android.widget.TextView; + +import com.android.internal.R; +import com.android.server.autofill.AutofillManagerService; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * A dialog to show Autofill suggestions. + * + * This fill dialog UI shows as a bottom sheet style dialog. This dialog UI + * provides a larger area to display the suggestions, it provides a more + * conspicuous and efficient interface to the user. So it is easy for users + * to pay attention to the datasets and selecting one of them. + */ +final class DialogFillUi { + + private static final String TAG = "DialogFillUi"; + private static final int THEME_ID_LIGHT = + R.style.Theme_DeviceDefault_Light_Autofill_Save; + private static final int THEME_ID_DARK = + R.style.Theme_DeviceDefault_Autofill_Save; + + interface UiCallback { + void onResponsePicked(@NonNull FillResponse response); + void onDatasetPicked(@NonNull Dataset dataset); + void onCanceled(); + void startIntentSender(IntentSender intentSender); + } + + private final @NonNull Dialog mDialog; + private final @NonNull OverlayControl mOverlayControl; + private final String mServicePackageName; + private final ComponentName mComponentName; + private final int mThemeId; + private final @NonNull Context mContext; + private final @NonNull UiCallback mCallback; + private final @NonNull ListView mListView; + private final @Nullable ItemsAdapter mAdapter; + private final int mVisibleDatasetsMaxCount; + + private @Nullable String mFilterText; + private @Nullable AnnounceFilterResult mAnnounceFilterResult; + private boolean mDestroyed; + + DialogFillUi(@NonNull Context context, @NonNull FillResponse response, + @NonNull AutofillId focusedViewId, @Nullable String filterText, + @Nullable Drawable serviceIcon, @Nullable String servicePackageName, + @Nullable ComponentName componentName, @NonNull OverlayControl overlayControl, + boolean nightMode, @NonNull UiCallback callback) { + if (sVerbose) Slog.v(TAG, "nightMode: " + nightMode); + mThemeId = nightMode ? THEME_ID_DARK : THEME_ID_LIGHT; + mCallback = callback; + mOverlayControl = overlayControl; + mServicePackageName = servicePackageName; + mComponentName = componentName; + + mContext = new ContextThemeWrapper(context, mThemeId); + final LayoutInflater inflater = LayoutInflater.from(mContext); + final View decor = inflater.inflate(R.layout.autofill_fill_dialog, null); + + setServiceIcon(decor, serviceIcon); + setHeader(decor, response); + + mVisibleDatasetsMaxCount = getVisibleDatasetsMaxCount(); + + if (response.getAuthentication() != null) { + mListView = null; + mAdapter = null; + try { + initialAuthenticationLayout(decor, response); + } catch (RuntimeException e) { + callback.onCanceled(); + Slog.e(TAG, "Error inflating remote views", e); + mDialog = null; + return; + } + } else { + final List<ViewItem> items = createDatasetItems(response, focusedViewId); + mAdapter = new ItemsAdapter(items); + mListView = decor.findViewById(R.id.autofill_dialog_list); + initialDatasetLayout(decor, filterText); + } + + setDismissButton(decor); + + mDialog = new Dialog(mContext, mThemeId); + mDialog.setContentView(decor); + setDialogParamsAsBottomSheet(); + + show(); + } + + private int getVisibleDatasetsMaxCount() { + if (AutofillManagerService.getVisibleDatasetsMaxCount() > 0) { + final int maxCount = AutofillManagerService.getVisibleDatasetsMaxCount(); + if (sVerbose) { + Slog.v(TAG, "overriding maximum visible datasets to " + maxCount); + } + return maxCount; + } else { + return mContext.getResources() + .getInteger(com.android.internal.R.integer.autofill_max_visible_datasets); + } + } + + private void setDialogParamsAsBottomSheet() { + final Window window = mDialog.getWindow(); + window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); + window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); + window.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS); + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); + window.setGravity(Gravity.BOTTOM | Gravity.CENTER); + window.setCloseOnTouchOutside(true); + final WindowManager.LayoutParams params = window.getAttributes(); + params.width = WindowManager.LayoutParams.MATCH_PARENT; + params.accessibilityTitle = + mContext.getString(R.string.autofill_picker_accessibility_title); + params.windowAnimations = R.style.AutofillSaveAnimation; + } + + private void setServiceIcon(View decor, Drawable serviceIcon) { + if (serviceIcon == null) { + return; + } + + final ImageView iconView = decor.findViewById(R.id.autofill_service_icon); + final int actualWidth = serviceIcon.getMinimumWidth(); + final int actualHeight = serviceIcon.getMinimumHeight(); + if (sDebug) { + Slog.d(TAG, "Adding service icon " + + "(" + actualWidth + "x" + actualHeight + ")"); + } + iconView.setImageDrawable(serviceIcon); + iconView.setVisibility(View.VISIBLE); + } + + private void setHeader(View decor, FillResponse response) { + final RemoteViews presentation = response.getDialogHeader(); + if (presentation == null) { + return; + } + + final ViewGroup container = decor.findViewById(R.id.autofill_dialog_header); + final RemoteViews.InteractionHandler interceptionHandler = (view, pendingIntent, r) -> { + if (pendingIntent != null) { + mCallback.startIntentSender(pendingIntent.getIntentSender()); + } + return true; + }; + + final View content = presentation.applyWithTheme( + mContext, (ViewGroup) decor, interceptionHandler, mThemeId); + container.addView(content); + container.setVisibility(View.VISIBLE); + } + + private void setDismissButton(View decor) { + final TextView noButton = decor.findViewById(R.id.autofill_dialog_no); + noButton.setOnClickListener((v) -> mCallback.onCanceled()); + } + + private void setContinueButton(View decor, View.OnClickListener listener) { + final TextView yesButton = decor.findViewById(R.id.autofill_dialog_yes); + // set "Continue" by default + yesButton.setText(R.string.autofill_continue_yes); + yesButton.setOnClickListener(listener); + } + + private void initialAuthenticationLayout(View decor, FillResponse response) { + RemoteViews presentation = response.getDialogPresentation(); + if (presentation == null) { + presentation = response.getPresentation(); + } + if (presentation == null) { + throw new RuntimeException("No presentation for fill dialog authentication"); + } + + // insert authentication item under autofill_dialog_container + final ViewGroup container = decor.findViewById(R.id.autofill_dialog_container); + final RemoteViews.InteractionHandler interceptionHandler = (view, pendingIntent, r) -> { + if (pendingIntent != null) { + mCallback.startIntentSender(pendingIntent.getIntentSender()); + } + return true; + }; + final View content = presentation.applyWithTheme( + mContext, (ViewGroup) decor, interceptionHandler, mThemeId); + container.addView(content); + container.setVisibility(View.VISIBLE); + container.setFocusable(true); + container.setOnClickListener(v -> mCallback.onResponsePicked(response)); + // just single item, set up continue button + setContinueButton(decor, v -> mCallback.onResponsePicked(response)); + } + + private ArrayList<ViewItem> createDatasetItems(FillResponse response, + AutofillId focusedViewId) { + final int datasetCount = response.getDatasets().size(); + if (sVerbose) { + Slog.v(TAG, "Number datasets: " + datasetCount + " max visible: " + + mVisibleDatasetsMaxCount); + } + + final RemoteViews.InteractionHandler interceptionHandler = (view, pendingIntent, r) -> { + if (pendingIntent != null) { + mCallback.startIntentSender(pendingIntent.getIntentSender()); + } + return true; + }; + + final ArrayList<ViewItem> items = new ArrayList<>(datasetCount); + for (int i = 0; i < datasetCount; i++) { + final Dataset dataset = response.getDatasets().get(i); + final int index = dataset.getFieldIds().indexOf(focusedViewId); + if (index >= 0) { + RemoteViews presentation = dataset.getFieldDialogPresentation(index); + if (presentation == null) { + Slog.w(TAG, "fallback to presentation"); + presentation = dataset.getFieldPresentation(index); + } + if (presentation == null) { + Slog.w(TAG, "not displaying UI on field " + focusedViewId + " because " + + "service didn't provide a presentation for it on " + dataset); + continue; + } + final View view; + try { + if (sVerbose) Slog.v(TAG, "setting remote view for " + focusedViewId); + view = presentation.applyWithTheme( + mContext, null, interceptionHandler, mThemeId); + } catch (RuntimeException e) { + Slog.e(TAG, "Error inflating remote views", e); + continue; + } + // TODO: Extract the shared filtering logic here and in FillUi to a common + // method. + final Dataset.DatasetFieldFilter filter = dataset.getFilter(index); + Pattern filterPattern = null; + String valueText = null; + boolean filterable = true; + if (filter == null) { + final AutofillValue value = dataset.getFieldValues().get(index); + if (value != null && value.isText()) { + valueText = value.getTextValue().toString().toLowerCase(); + } + } else { + filterPattern = filter.pattern; + if (filterPattern == null) { + if (sVerbose) { + Slog.v(TAG, "Explicitly disabling filter at id " + focusedViewId + + " for dataset #" + index); + } + filterable = false; + } + } + + items.add(new ViewItem(dataset, filterPattern, filterable, valueText, view)); + } + } + return items; + } + + private void initialDatasetLayout(View decor, String filterText) { + final AdapterView.OnItemClickListener onItemClickListener = + (adapter, view, position, id) -> { + final ViewItem vi = mAdapter.getItem(position); + mCallback.onDatasetPicked(vi.dataset); + }; + + mListView.setAdapter(mAdapter); + mListView.setVisibility(View.VISIBLE); + mListView.setOnItemClickListener(onItemClickListener); + + if (mAdapter.getCount() == 1) { + // just single item, set up continue button + setContinueButton(decor, (v) -> + onItemClickListener.onItemClick(null, null, 0, 0)); + } + + if (filterText == null) { + mFilterText = null; + } else { + mFilterText = filterText.toLowerCase(); + } + + final int oldCount = mAdapter.getCount(); + mAdapter.getFilter().filter(mFilterText, (count) -> { + if (mDestroyed) { + return; + } + if (count <= 0) { + if (sDebug) { + final int size = mFilterText == null ? 0 : mFilterText.length(); + Slog.d(TAG, "No dataset matches filter with " + size + " chars"); + } + mCallback.onCanceled(); + } else { + + if (mAdapter.getCount() > mVisibleDatasetsMaxCount) { + mListView.setVerticalScrollBarEnabled(true); + mListView.onVisibilityAggregated(true); + } else { + mListView.setVerticalScrollBarEnabled(false); + } + if (mAdapter.getCount() != oldCount) { + mListView.requestLayout(); + } + } + }); + } + + private void show() { + Slog.i(TAG, "Showing fill dialog"); + mDialog.show(); + mOverlayControl.hideOverlays(); + } + + boolean isShowing() { + return mDialog.isShowing(); + } + + void hide() { + if (sVerbose) Slog.v(TAG, "Hiding fill dialog."); + try { + mDialog.hide(); + } finally { + mOverlayControl.showOverlays(); + } + } + + void destroy() { + try { + if (sDebug) Slog.d(TAG, "destroy()"); + throwIfDestroyed(); + + mDialog.dismiss(); + mDestroyed = true; + } finally { + mOverlayControl.showOverlays(); + } + } + + private void throwIfDestroyed() { + if (mDestroyed) { + throw new IllegalStateException("cannot interact with a destroyed instance"); + } + } + + @Override + public String toString() { + // TODO toString + return "NO TITLE"; + } + + void dump(PrintWriter pw, String prefix) { + + pw.print(prefix); pw.print("service: "); pw.println(mServicePackageName); + pw.print(prefix); pw.print("app: "); pw.println(mComponentName.toShortString()); + pw.print(prefix); pw.print("theme id: "); pw.print(mThemeId); + switch (mThemeId) { + case THEME_ID_DARK: + pw.println(" (dark)"); + break; + case THEME_ID_LIGHT: + pw.println(" (light)"); + break; + default: + pw.println("(UNKNOWN_MODE)"); + break; + } + final View view = mDialog.getWindow().getDecorView(); + final int[] loc = view.getLocationOnScreen(); + pw.print(prefix); pw.print("coordinates: "); + pw.print('('); pw.print(loc[0]); pw.print(','); pw.print(loc[1]); pw.print(')'); + pw.print('('); + pw.print(loc[0] + view.getWidth()); pw.print(','); + pw.print(loc[1] + view.getHeight()); pw.println(')'); + pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed); + } + + private void announceSearchResultIfNeeded() { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + if (mAnnounceFilterResult == null) { + mAnnounceFilterResult = new AnnounceFilterResult(); + } + mAnnounceFilterResult.post(); + } + } + + // TODO: Below code copied from FullUi, Extract the shared filtering logic here + // and in FillUi to a common method. + private final class AnnounceFilterResult implements Runnable { + private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec + + public void post() { + remove(); + mListView.postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY); + } + + public void remove() { + mListView.removeCallbacks(this); + } + + @Override + public void run() { + final int count = mListView.getAdapter().getCount(); + final String text; + if (count <= 0) { + text = mContext.getString(R.string.autofill_picker_no_suggestions); + } else { + Map<String, Object> arguments = new HashMap<>(); + arguments.put("count", count); + text = PluralsMessageFormatter.format(mContext.getResources(), + arguments, + R.string.autofill_picker_some_suggestions); + } + mListView.announceForAccessibility(text); + } + } + + private final class ItemsAdapter extends BaseAdapter implements Filterable { + private @NonNull final List<ViewItem> mAllItems; + + private @NonNull final List<ViewItem> mFilteredItems = new ArrayList<>(); + + ItemsAdapter(@NonNull List<ViewItem> items) { + mAllItems = Collections.unmodifiableList(new ArrayList<>(items)); + mFilteredItems.addAll(items); + } + + @Override + public Filter getFilter() { + return new Filter() { + @Override + protected FilterResults performFiltering(CharSequence filterText) { + // No locking needed as mAllItems is final an immutable + final List<ViewItem> filtered = mAllItems.stream() + .filter((item) -> item.matches(filterText)) + .collect(Collectors.toList()); + final FilterResults results = new FilterResults(); + results.values = filtered; + results.count = filtered.size(); + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + final boolean resultCountChanged; + final int oldItemCount = mFilteredItems.size(); + mFilteredItems.clear(); + if (results.count > 0) { + @SuppressWarnings("unchecked") final List<ViewItem> items = + (List<ViewItem>) results.values; + mFilteredItems.addAll(items); + } + resultCountChanged = (oldItemCount != mFilteredItems.size()); + if (resultCountChanged) { + announceSearchResultIfNeeded(); + } + notifyDataSetChanged(); + } + }; + } + + @Override + public int getCount() { + return mFilteredItems.size(); + } + + @Override + public ViewItem getItem(int position) { + return mFilteredItems.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return getItem(position).view; + } + + @Override + public String toString() { + return "ItemsAdapter: [all=" + mAllItems + ", filtered=" + mFilteredItems + "]"; + } + } + + + /** + * An item for the list view - either a (clickable) dataset or a (read-only) header / footer. + */ + private static class ViewItem { + public final @Nullable String value; + public final @Nullable Dataset dataset; + public final @NonNull View view; + public final @Nullable Pattern filter; + public final boolean filterable; + + /** + * Default constructor. + * + * @param dataset dataset associated with the item + * @param filter optional filter set by the service to determine how the item should be + * filtered + * @param filterable optional flag set by the service to indicate this item should not be + * filtered (typically used when the dataset has value but it's sensitive, like a password) + * @param value dataset value + * @param view dataset presentation. + */ + ViewItem(@NonNull Dataset dataset, @Nullable Pattern filter, boolean filterable, + @Nullable String value, @NonNull View view) { + this.dataset = dataset; + this.value = value; + this.view = view; + this.filter = filter; + this.filterable = filterable; + } + + /** + * Returns whether this item matches the value input by the user so it can be included + * in the filtered datasets. + */ + public boolean matches(CharSequence filterText) { + if (TextUtils.isEmpty(filterText)) { + // Always show item when the user input is empty + return true; + } + if (!filterable) { + // Service explicitly disabled filtering using a null Pattern. + return false; + } + final String constraintLowerCase = filterText.toString().toLowerCase(); + if (filter != null) { + // Uses pattern provided by service + return filter.matcher(constraintLowerCase).matches(); + } else { + // Compares it with dataset value with dataset + return (value == null) + ? (dataset.getAuthentication() == null) + : value.toLowerCase().startsWith(constraintLowerCase); + } + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder("ViewItem:[view=") + .append(view.getAutofillId()); + final String datasetId = dataset == null ? null : dataset.getId(); + if (datasetId != null) { + builder.append(", dataset=").append(datasetId); + } + if (value != null) { + // Cannot print value because it could contain PII + builder.append(", value=").append(value.length()).append("_chars"); + } + if (filterable) { + builder.append(", filterable"); + } + if (filter != null) { + // Filter should not have PII, but it could be a huge regexp + builder.append(", filter=").append(filter.pattern().length()).append("_chars"); + } + return builder.append(']').toString(); + } + } +} |