diff options
Diffstat (limited to 'android/service/autofill/Dataset.java')
-rw-r--r-- | android/service/autofill/Dataset.java | 196 |
1 files changed, 158 insertions, 38 deletions
diff --git a/android/service/autofill/Dataset.java b/android/service/autofill/Dataset.java index cb341b1d..ef9598aa 100644 --- a/android/service/autofill/Dataset.java +++ b/android/service/autofill/Dataset.java @@ -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—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; @@ -85,6 +131,12 @@ public final class Dataset implements Parcelable { } /** @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); } @@ -323,9 +437,10 @@ public final class Dataset implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeParcelable(mPresentation, flags); - parcel.writeTypedArrayList(mFieldIds, flags); - parcel.writeTypedArrayList(mFieldValues, flags); + parcel.writeTypedList(mFieldIds, flags); + parcel.writeTypedList(mFieldValues, flags); parcel.writeParcelableList(mFieldPresentations, flags); + parcel.writeSerializable(mFieldFilters); parcel.writeParcelable(mAuthentication, flags); parcel.writeString(mId); } @@ -340,10 +455,14 @@ public final class Dataset implements Parcelable { final Builder builder = (presentation == null) ? new Builder() : new Builder(presentation); - final ArrayList<AutofillId> ids = parcel.readTypedArrayList(null); - final ArrayList<AutofillValue> values = parcel.readTypedArrayList(null); + final ArrayList<AutofillId> ids = parcel.createTypedArrayList(AutofillId.CREATOR); + final ArrayList<AutofillValue> values = + 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++) { @@ -351,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()); |