Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 678f0b12 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "New autofill API: let service explicitly set dataset filter."

parents 58055ea4 27a026ac
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -37137,6 +37137,8 @@ package android.service.autofill {
    method public android.service.autofill.Dataset.Builder setId(java.lang.String);
    method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue);
    method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue, android.widget.RemoteViews);
    method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue, java.util.regex.Pattern);
    method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue, java.util.regex.Pattern, android.widget.RemoteViews);
  }
  public final class FillCallback {
+2 −0
Original line number Diff line number Diff line
@@ -40232,6 +40232,8 @@ package android.service.autofill {
    method public android.service.autofill.Dataset.Builder setId(java.lang.String);
    method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue);
    method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue, android.widget.RemoteViews);
    method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue, java.util.regex.Pattern);
    method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue, java.util.regex.Pattern, android.widget.RemoteViews);
  }
  public final class FillCallback {
+2 −0
Original line number Diff line number Diff line
@@ -37426,6 +37426,8 @@ package android.service.autofill {
    method public android.service.autofill.Dataset.Builder setId(java.lang.String);
    method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue);
    method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue, android.widget.RemoteViews);
    method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue, java.util.regex.Pattern);
    method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue, java.util.regex.Pattern, android.widget.RemoteViews);
  }
  public final class FillCallback {
+153 −34
Original line number Diff line number Diff line
@@ -29,32 +29,77 @@ import android.widget.RemoteViews;

import com.android.internal.util.Preconditions;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.regex.Pattern;

/**
 * A dataset object represents a group of key/value pairs used to autofill parts of a screen.
 * A dataset object represents a group of fields (key / value pairs) used to autofill parts of a
 * screen.
 *
 * <p>In its simplest form, a dataset contains one or more key / value pairs (comprised of
 * {@link AutofillId} and {@link AutofillValue} respectively); and one or more
 * {@link RemoteViews presentation} for these pairs (a pair could have its own
 * {@link RemoteViews presentation}, or use the default {@link RemoteViews presentation} associated
 * with the whole dataset). When an autofill service returns datasets in a {@link FillResponse}
 * <a name="BasicUsage"></a>
 * <h3>Basic usage</h3>
 *
 * <p>In its simplest form, a dataset contains one or more fields (comprised of
 * an {@link AutofillId id}, a {@link AutofillValue value}, and an optional filter
 * {@link Pattern regex}); and one or more {@link RemoteViews presentations} for these fields
 * (each field could have its own {@link RemoteViews presentation}, or use the default
 * {@link RemoteViews presentation} associated with the whole dataset).
 *
 * <p>When an autofill service returns datasets in a {@link FillResponse}
 * and the screen input is focused in a view that is present in at least one of these datasets,
 * the Android System displays a UI affordance containing the {@link RemoteViews presentation} of
 * the Android System displays a UI containing the {@link RemoteViews presentation} of
 * all datasets pairs that have that view's {@link AutofillId}. Then, when the user selects a
 * dataset from the affordance, all views in that dataset are autofilled.
 * dataset from the UI, all views in that dataset are autofilled.
 *
 * <a name="Authentication"></a>
 * <h3>Dataset authentication</h3>
 *
 * <p>In a more sophisticated form, the dataset values can be protected until the user authenticates
 * the dataset&mdash;in that case, when a dataset is selected by the user, the Android System
 * launches an intent set by the service to "unlock" the dataset.
 *
 * <p>For example, when a data set contains credit card information (such as number,
 * expiration date, and verification code), you could provide a dataset presentation saying
 * "Tap to authenticate". Then when the user taps that option, you would launch an activity asking
 * the user to enter the credit card code, and if the user enters a valid code, you could then
 * "unlock" the dataset.
 *
 * <p>You can also use authenticated datasets to offer an interactive UI for the user. For example,
 * if the activity being autofilled is an account creation screen, you could use an authenticated
 * dataset to automatically generate a random password for the user.
 *
 * <p>In a more sophisticated form, the dataset value can be protected until the user authenticates
 * the dataset - see {@link Dataset.Builder#setAuthentication(IntentSender)}.
 * <p>See {@link Dataset.Builder#setAuthentication(IntentSender)} for more details about the dataset
 * authentication mechanism.
 *
 * @see android.service.autofill.AutofillService for more information and examples about the
 * role of datasets in the autofill workflow.
 * <a name="Filtering"></a>
 * <h3>Filtering</h3>
 * <p>The autofill UI automatically changes which values are shown based on value of the view
 * anchoring it, following the rules below:
 * <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 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.
 *   <li>All other datasets are hidden.
 * </ol>
 *
 * <a name="MoreInfo"></a>
 * <h3>More information</h3>
 * <p>See {@link android.service.autofill.AutofillService} for more information and examples about
 * the role of datasets in the autofill workflow.
 */
public final class Dataset implements Parcelable {

    private final ArrayList<AutofillId> mFieldIds;
    private final ArrayList<AutofillValue> mFieldValues;
    private final ArrayList<RemoteViews> mFieldPresentations;
    private final ArrayList<Pattern> mFieldFilters;
    private final RemoteViews mPresentation;
    private final IntentSender mAuthentication;
    @Nullable String mId;
@@ -63,6 +108,7 @@ public final class Dataset implements Parcelable {
        mFieldIds = builder.mFieldIds;
        mFieldValues = builder.mFieldValues;
        mFieldPresentations = builder.mFieldPresentations;
        mFieldFilters = builder.mFieldFilters;
        mPresentation = builder.mPresentation;
        mAuthentication = builder.mAuthentication;
        mId = builder.mId;
@@ -84,6 +130,12 @@ public final class Dataset implements Parcelable {
        return customPresentation != null ? customPresentation : mPresentation;
    }

    /** @hide */
    @Nullable
    public Pattern getFilter(int index) {
        return mFieldFilters.get(index);
    }

    /** @hide */
    public @Nullable IntentSender getAuthentication() {
        return mAuthentication;
@@ -103,6 +155,8 @@ public final class Dataset implements Parcelable {
                .append(", fieldValues=").append(mFieldValues)
                .append(", fieldPresentations=")
                .append(mFieldPresentations == null ? 0 : mFieldPresentations.size())
                .append(", fieldFilters=")
                .append(mFieldFilters == null ? 0 : mFieldFilters.size())
                .append(", hasPresentation=").append(mPresentation != null)
                .append(", hasAuthentication=").append(mAuthentication != null)
                .append(']').toString();
@@ -127,6 +181,7 @@ public final class Dataset implements Parcelable {
        private ArrayList<AutofillId> mFieldIds;
        private ArrayList<AutofillValue> mFieldValues;
        private ArrayList<RemoteViews> mFieldPresentations;
        private ArrayList<Pattern> mFieldFilters;
        private RemoteViews mPresentation;
        private IntentSender mAuthentication;
        private boolean mDestroyed;
@@ -182,12 +237,12 @@ public final class Dataset implements Parcelable {
         * credit card information without the CVV for the data set in the {@link FillResponse
         * response} then the returned data set should contain the CVV entry.
         *
         * <p><b>NOTE:</b> Do not make the provided pending intent
         * <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.
         *
         * @param authentication Intent to an activity with your authentication flow.
         * @return This builder.
         * @return this builder.
         *
         * @see android.app.PendingIntent
         */
@@ -214,11 +269,10 @@ public final class Dataset implements Parcelable {
         *
         * @param id id for this dataset or {@code null} to unset.
         *
         * @return This builder.
         * @return this builder.
         */
        public @NonNull Builder setId(@Nullable String id) {
            throwIfDestroyed();

            mId = id;
            return this;
        }
@@ -230,17 +284,16 @@ public final class Dataset implements Parcelable {
         *         android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
         * @param value 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 an authentication and you have no access to the value.
         * @return This builder.
         *        the dataset needs authentication and you have no access to the value.
         * @return this builder.
         * @throws IllegalStateException if the builder was constructed without a
         * {@link RemoteViews presentation}.
         */
        public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value) {
            throwIfDestroyed();
            if (mPresentation == null) {
                throw new IllegalStateException("Dataset presentation not set on constructor");
            }
            setValueAndPresentation(id, value, null);
            Preconditions.checkState(mPresentation != null,
                    "Dataset presentation not set on constructor");
            setLifeTheUniverseAndEverything(id, value, null, null);
            return this;
        }

@@ -250,23 +303,81 @@ public final class Dataset implements Parcelable {
         *
         * @param id id returned by {@link
         *         android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
         * @param value value to be auto filled. Pass {@code null} if you do not have the value
         * @param value 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 an authentication and you have no access to the value.
         *        Filtering matches any user typed string to {@code null} values.
         * @param presentation The presentation used to visualize this field.
         * @return This builder.
         *        the dataset needs authentication and you have no access to the value.
         * @param presentation the presentation used to visualize this field.
         * @return this builder.
         *
         */
        public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
                @NonNull RemoteViews presentation) {
            throwIfDestroyed();
            Preconditions.checkNotNull(presentation, "presentation cannot be null");
            setValueAndPresentation(id, value, presentation);
            setLifeTheUniverseAndEverything(id, value, presentation, null);
            return this;
        }

        /**
         * Sets the value of a field using an <a href="#Filtering">explicit filter</a>.
         *
         * <p>This method is typically used when the dataset is not authenticated and the field
         * value is not {@link AutofillValue#isText() text} but the service still wants to allow
         * the user to filter it out.
         *
         * @param id id returned by {@link
         *         android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
         * @param value 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.
         * @param filter regex used to determine if the dataset should be shown in the autofill UI.
         *
         * @return this builder.
         * @throws IllegalStateException if the builder was constructed without a
         * {@link RemoteViews presentation}.
         */
        public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
                @NonNull Pattern filter) {
            throwIfDestroyed();
            Preconditions.checkNotNull(filter, "filter cannot be null");
            Preconditions.checkState(mPresentation != null,
                    "Dataset presentation not set on constructor");
            setLifeTheUniverseAndEverything(id, value, null, filter);
            return this;
        }

        /**
         * Sets the value of a field, using a custom {@link RemoteViews presentation} to
         * visualize it and a <a href="#Filtering">explicit filter</a>.
         *
         * <p>Typically used to allow filtering on
         * {@link Dataset.Builder#setAuthentication(IntentSender) authenticated datasets}. For
         * example, if the dataset represents a credit card number and the service does not want to
         * show the "Tap to authenticate" message until the user tapped 4 digits, in which case
         * the filter would be {@code Pattern.compile("\\d.{4,}")}.
         *
         * @param id id returned by {@link
         *         android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
         * @param value 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.
         * @param presentation the presentation used to visualize this field.
         * @param filter regex used to determine if the dataset should be shown in the autofill UI.
         *
         * @return this builder.
         */
        public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
                @NonNull Pattern filter, @NonNull RemoteViews presentation) {
            throwIfDestroyed();
            Preconditions.checkNotNull(filter, "filter cannot be null");
            Preconditions.checkNotNull(presentation, "presentation cannot be null");
            setLifeTheUniverseAndEverything(id, value, presentation, filter);
            return this;
        }

        private void setValueAndPresentation(AutofillId id, AutofillValue value,
                RemoteViews presentation) {
        private void setLifeTheUniverseAndEverything(@NonNull AutofillId id,
                @Nullable AutofillValue value, @Nullable RemoteViews presentation,
                @Nullable Pattern filter) {
            Preconditions.checkNotNull(id, "id cannot be null");
            if (mFieldIds != null) {
                final int existingIdx = mFieldIds.indexOf(id);
@@ -279,10 +390,12 @@ public final class Dataset implements Parcelable {
                mFieldIds = new ArrayList<>();
                mFieldValues = new ArrayList<>();
                mFieldPresentations = new ArrayList<>();
                mFieldFilters = new ArrayList<>();
            }
            mFieldIds.add(id);
            mFieldValues.add(value);
            mFieldPresentations.add(presentation);
            mFieldFilters.add(filter);
        }

        /**
@@ -290,8 +403,9 @@ public final class Dataset implements Parcelable {
         *
         * <p>You should not interact with this builder once this method is called.
         *
         * <p>It is required that you specify at least one field before calling this method. It's
         * also mandatory to provide a presentation view to visualize the data set in the UI.
         * @throws IllegalStateException if no field was set (through
         * {@link #setValue(AutofillId, AutofillValue)} or
         * {@link #setValue(AutofillId, AutofillValue, RemoteViews)}).
         *
         * @return The built dataset.
         */
@@ -299,7 +413,7 @@ public final class Dataset implements Parcelable {
            throwIfDestroyed();
            mDestroyed = true;
            if (mFieldIds == null) {
                throw new IllegalArgumentException("at least one value must be set");
                throw new IllegalStateException("at least one value must be set");
            }
            return new Dataset(this);
        }
@@ -326,6 +440,7 @@ public final class Dataset implements Parcelable {
        parcel.writeTypedList(mFieldIds, flags);
        parcel.writeTypedList(mFieldValues, flags);
        parcel.writeParcelableList(mFieldPresentations, flags);
        parcel.writeSerializable(mFieldFilters);
        parcel.writeParcelable(mAuthentication, flags);
        parcel.writeString(mId);
    }
@@ -345,6 +460,9 @@ public final class Dataset implements Parcelable {
                    parcel.createTypedArrayList(AutofillValue.CREATOR);
            final ArrayList<RemoteViews> presentations = new ArrayList<>();
            parcel.readParcelableList(presentations, null);
            @SuppressWarnings("unchecked")
            final ArrayList<Serializable> filters =
                    (ArrayList<Serializable>) parcel.readSerializable();
            final int idCount = (ids != null) ? ids.size() : 0;
            final int valueCount = (values != null) ? values.size() : 0;
            for (int i = 0; i < idCount; i++) {
@@ -352,7 +470,8 @@ public final class Dataset implements Parcelable {
                final AutofillValue value = (valueCount > i) ? values.get(i) : null;
                final RemoteViews fieldPresentation = presentations.isEmpty() ? null
                        : presentations.get(i);
                builder.setValueAndPresentation(id, value, fieldPresentation);
                final Pattern filter = (Pattern) filters.get(i);
                builder.setLifeTheUniverseAndEverything(id, value, fieldPresentation, filter);
            }
            builder.setAuthentication(parcel.readParcelable(null));
            builder.setId(parcel.readString());
+27 −17
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

final class FillUi {
    private static final String TAG = "FillUi";
@@ -164,15 +165,18 @@ final class FillUi {
                        Slog.e(TAG, "Error inflating remote views", e);
                        continue;
                    }
                    final AutofillValue value = dataset.getFieldValues().get(index);
                    final Pattern filter = dataset.getFilter(index);
                    String valueText = null;
                    if (filter == null) {
                        final AutofillValue value = dataset.getFieldValues().get(index);
                        // If the dataset needs auth - don't add its text to allow guessing
                        // its content based on how filtering behaves.
                        if (value != null && value.isText() && dataset.getAuthentication() == null) {
                            valueText = value.getTextValue().toString().toLowerCase();
                        }
                    }

                    items.add(new ViewItem(dataset, valueText, view));
                    items.add(new ViewItem(dataset, filter, valueText, view));
                }
            }

@@ -331,11 +335,17 @@ final class FillUi {
        private final String mValue;
        private final Dataset mDataset;
        private final View mView;
        private final Pattern mFilter;

        ViewItem(Dataset dataset, String value, View view) {
        ViewItem(Dataset dataset, Pattern filter, String value, View view) {
            mDataset = dataset;
            mValue = value;
            mView = view;
            mFilter = filter;
        }

        public Pattern getFilter() {
            return mFilter;
        }

        public View getView() {
@@ -349,12 +359,6 @@ final class FillUi {
        public String getValue() {
            return mValue;
        }

        @Override
        public String toString() {
            // Used for filtering in the adapter
            return mValue;
        }
    }

    private final class AutofillWindowPresenter extends IAutofillWindowPresenter.Stub {
@@ -516,10 +520,16 @@ final class FillUi {
                    for (int i = 0; i < itemCount; i++) {
                        final ViewItem item = mAllItems.get(i);
                        final String value = item.getValue();
                        // No value, i.e. null, matches any filter
                        if ((value == null && item.mDataset.getAuthentication() == null)
                                || (value != null
                                        && value.toLowerCase().startsWith(constraintLowerCase))) {
                        final Pattern filter = item.getFilter();
                        final boolean matches;
                        if (filter != null) {
                            matches = filter.matcher(constraintLowerCase).matches();
                        } else {
                            matches = (value == null)
                                    ? (item.mDataset.getAuthentication() == null)
                                    : value.toLowerCase().startsWith(constraintLowerCase);
                        }
                        if (matches) {
                            filteredItems.add(item);
                        }
                    }