diff options
| author | 2022-01-28 06:01:03 +0000 | |
|---|---|---|
| committer | 2022-01-28 06:01:03 +0000 | |
| commit | 00e99e742a9d9aedf4012924977d0e9fcafe674f (patch) | |
| tree | a73503e50e8fd112f31210d1fcba418d54fe163c | |
| parent | 0891d4b39060982a8631c1b8983e089819099f3b (diff) | |
| parent | 33571fb894c33033594bdf4f47d57f81b22b34ca (diff) | |
Merge changes from topic "aff_bottom_sheet"
* changes:
  Add bottom sheet fill UI for Autofill
  Implement actions on client and service for bottom sheet
  Notify Autofill while activity started
  Adds APIs for Autofill bottom sheet UI
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(); +        } +    } +}  |