/* * Copyright (C) 2017 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 static android.service.autofill.AutofillServiceHelper.assertValid; import static android.view.autofill.Helper.sDebug; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.content.IntentSender; import android.os.Parcel; import android.os.Parcelable; import android.util.ArrayMap; import android.util.ArraySet; import android.util.DebugUtils; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Objects; /** * Information used to indicate that an {@link AutofillService} is interested on saving the * user-inputed data for future use, through a * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} * call. * *

A {@link SaveInfo} is always associated with a {@link FillResponse}, and it contains at least * two pieces of information: * *

    *
  1. The type(s) of user data (like password or credit card info) that would be saved. *
  2. The minimum set of views (represented by their {@link AutofillId}) that need to be changed * to trigger a save request. *
* *

Typically, the {@link SaveInfo} contains the same {@code id}s as the {@link Dataset}: * *

 *   new FillResponse.Builder()
 *       .addDataset(new Dataset.Builder()
 *           .setValue(id1, AutofillValue.forText("homer"), createPresentation("homer")) // username
 *           .setValue(id2, AutofillValue.forText("D'OH!"), createPresentation("password for homer")) // password
 *           .build())
 *       .setSaveInfo(new SaveInfo.Builder(
 *           SaveInfo.SAVE_DATA_TYPE_USERNAME | SaveInfo.SAVE_DATA_TYPE_PASSWORD,
 *           new AutofillId[] { id1, id2 }).build())
 *       .build();
 * 
* *

The save type flags are used to display the appropriate strings in the autofill save UI. * You can pass multiple values, but try to keep it short if possible. In the above example, just * {@code SaveInfo.SAVE_DATA_TYPE_PASSWORD} would be enough. * *

There might be cases where the {@link AutofillService} knows how to fill the screen, * but the user has no data for it. In that case, the {@link FillResponse} should contain just the * {@link SaveInfo}, but no {@link Dataset Datasets}: * *

 *   new FillResponse.Builder()
 *       .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_PASSWORD,
 *           new AutofillId[] { id1, id2 }).build())
 *       .build();
 * 
* *

There might be cases where the user data in the {@link AutofillService} is enough * to populate some fields but not all, and the service would still be interested on saving the * other fields. In that case, the service could set the * {@link SaveInfo.Builder#setOptionalIds(AutofillId[])} as well: * *

 *   new FillResponse.Builder()
 *       .addDataset(new Dataset.Builder()
 *           .setValue(id1, AutofillValue.forText("742 Evergreen Terrace"),
 *               createPresentation("742 Evergreen Terrace")) // street
 *           .setValue(id2, AutofillValue.forText("Springfield"),
 *               createPresentation("Springfield")) // city
 *           .build())
 *       .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_ADDRESS,
 *           new AutofillId[] { id1, id2 }) // street and  city
 *           .setOptionalIds(new AutofillId[] { id3, id4 }) // state and zipcode
 *           .build())
 *       .build();
 * 
* * *

Triggering a save request

* *

The {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} can be triggered after * any of the following events: *

* *

But it is only triggered when all conditions below are met: *

* * *

Customizing the autofill save UI

* *

The service can also customize some aspects of the autofill save UI: *

*/ public final class SaveInfo implements Parcelable { /** * Type used when the service can save the contents of a screen, but cannot describe what * the content is for. */ public static final int SAVE_DATA_TYPE_GENERIC = 0x0; /** * Type used when the {@link FillResponse} represents user credentials that have a password. */ public static final int SAVE_DATA_TYPE_PASSWORD = 0x01; /** * Type used on when the {@link FillResponse} represents a physical address (such as street, * city, state, etc). */ public static final int SAVE_DATA_TYPE_ADDRESS = 0x02; /** * Type used when the {@link FillResponse} represents a credit card. */ public static final int SAVE_DATA_TYPE_CREDIT_CARD = 0x04; /** * Type used when the {@link FillResponse} represents just an username, without a password. */ public static final int SAVE_DATA_TYPE_USERNAME = 0x08; /** * Type used when the {@link FillResponse} represents just an email address, without a password. */ public static final int SAVE_DATA_TYPE_EMAIL_ADDRESS = 0x10; /** * Type used when the {@link FillResponse} represents a debit card. */ public static final int SAVE_DATA_TYPE_DEBIT_CARD = 0x20; /** * Type used when the {@link FillResponse} represents a payment card except for credit and * debit cards. */ public static final int SAVE_DATA_TYPE_PAYMENT_CARD = 0x40; /** * Type used when the {@link FillResponse} represents a card that does not a specified card or * cannot identify what the card is for. */ public static final int SAVE_DATA_TYPE_GENERIC_CARD = 0x80; /** * Style for the negative button of the save UI to cancel the * save operation. In this case, the user tapping the negative * button signals that they would prefer to not save the filled * content. */ public static final int NEGATIVE_BUTTON_STYLE_CANCEL = 0; /** * Style for the negative button of the save UI to reject the * save operation. This could be useful if the user needs to * opt-in your service and the save prompt is an advertisement * of the potential value you can add to the user. In this * case, the user tapping the negative button sends a strong * signal that the feature may not be useful and you may * consider some backoff strategy. */ public static final int NEGATIVE_BUTTON_STYLE_REJECT = 1; /** * Style for the negative button of the save UI to never do the * save operation. This means that the user does not need to save * any data on this activity or application. Once the user tapping * the negative button, the service should never trigger the save * UI again. In addition to this, must consider providing restore * options for the user. */ public static final int NEGATIVE_BUTTON_STYLE_NEVER = 2; /** @hide */ @IntDef(prefix = { "NEGATIVE_BUTTON_STYLE_" }, value = { NEGATIVE_BUTTON_STYLE_CANCEL, NEGATIVE_BUTTON_STYLE_REJECT, NEGATIVE_BUTTON_STYLE_NEVER }) @Retention(RetentionPolicy.SOURCE) @interface NegativeButtonStyle{} /** * Style for the positive button of save UI to request the save operation. * In this case, the user tapping the positive button signals that they * agrees to save the filled content. */ public static final int POSITIVE_BUTTON_STYLE_SAVE = 0; /** * Style for the positive button of save UI to have next action before the save operation. * This could be useful if the filled content contains sensitive personally identifiable * information and then requires user confirmation or verification. In this case, the user * tapping the positive button signals that they would complete the next required action * to save the filled content. */ public static final int POSITIVE_BUTTON_STYLE_CONTINUE = 1; /** @hide */ @IntDef(prefix = { "POSITIVE_BUTTON_STYLE_" }, value = { POSITIVE_BUTTON_STYLE_SAVE, POSITIVE_BUTTON_STYLE_CONTINUE }) @Retention(RetentionPolicy.SOURCE) @interface PositiveButtonStyle{} /** @hide */ @IntDef(flag = true, prefix = { "SAVE_DATA_TYPE_" }, value = { SAVE_DATA_TYPE_GENERIC, SAVE_DATA_TYPE_PASSWORD, SAVE_DATA_TYPE_ADDRESS, SAVE_DATA_TYPE_CREDIT_CARD, SAVE_DATA_TYPE_USERNAME, SAVE_DATA_TYPE_EMAIL_ADDRESS, SAVE_DATA_TYPE_DEBIT_CARD, SAVE_DATA_TYPE_PAYMENT_CARD, SAVE_DATA_TYPE_GENERIC_CARD }) @Retention(RetentionPolicy.SOURCE) @interface SaveDataType{} /** * Usually, a save request is only automatically triggered * once the {@link Activity} finishes. If this flag is set, it is triggered once all saved views * become invisible. */ public static final int FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE = 0x1; /** * By default, a save request is automatically triggered * once the {@link Activity} finishes. If this flag is set, finishing the activity doesn't * trigger a save request. * *

This flag is typically used in conjunction with {@link Builder#setTriggerId(AutofillId)}. */ public static final int FLAG_DONT_SAVE_ON_FINISH = 0x2; /** * Postpone the autofill save UI. * *

If flag is set, the autofill save UI is not triggered when the * autofill context associated with the response associated with this {@link SaveInfo} is * committed (with {@link AutofillManager#commit()}). Instead, the {@link FillContext} * is delivered in future fill requests (with {@link * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}) * and save request (with {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}) * of an activity belonging to the same task. * *

This flag should be used when the service detects that the application uses * multiple screens to implement an autofillable workflow (for example, one screen for the * username field, another for password). */ // TODO(b/113281366): improve documentation: add example, document relationship with other // flags, etc... public static final int FLAG_DELAY_SAVE = 0x4; /** @hide */ @IntDef(flag = true, prefix = { "FLAG_" }, value = { FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, FLAG_DONT_SAVE_ON_FINISH, FLAG_DELAY_SAVE }) @Retention(RetentionPolicy.SOURCE) @interface SaveInfoFlags{} private final @SaveDataType int mType; private final @NegativeButtonStyle int mNegativeButtonStyle; private final @PositiveButtonStyle int mPositiveButtonStyle; private final IntentSender mNegativeActionListener; private final AutofillId[] mRequiredIds; private final AutofillId[] mOptionalIds; private final CharSequence mDescription; private final int mFlags; private final CustomDescription mCustomDescription; private final InternalValidator mValidator; private final InternalSanitizer[] mSanitizerKeys; private final AutofillId[][] mSanitizerValues; private final AutofillId mTriggerId; /** * Creates a copy of the provided SaveInfo. * * @hide */ public static SaveInfo copy(SaveInfo s, AutofillId[] optionalIds) { return new SaveInfo(s.mType, s.mNegativeButtonStyle, s.mPositiveButtonStyle, s.mNegativeActionListener, s.mRequiredIds, assertValid(optionalIds), s.mDescription, s.mFlags, s.mCustomDescription, s.mValidator, s.mSanitizerKeys, s.mSanitizerValues, s.mTriggerId); } private SaveInfo(@SaveDataType int type, @NegativeButtonStyle int negativeButtonStyle, @PositiveButtonStyle int positiveButtonStyle, IntentSender negativeActionListener, AutofillId[] requiredIds, AutofillId[] optionalIds, CharSequence description, int flags, CustomDescription customDescription, InternalValidator validator, InternalSanitizer[] sanitizerKeys, AutofillId[][] sanitizerValues, AutofillId triggerId) { mType = type; mNegativeButtonStyle = negativeButtonStyle; mNegativeActionListener = negativeActionListener; mPositiveButtonStyle = positiveButtonStyle; mRequiredIds = requiredIds; mOptionalIds = optionalIds; mDescription = description; mFlags = flags; mCustomDescription = customDescription; mValidator = validator; mSanitizerKeys = sanitizerKeys; mSanitizerValues = sanitizerValues; mTriggerId = triggerId; } private SaveInfo(Builder builder) { mType = builder.mType; mNegativeButtonStyle = builder.mNegativeButtonStyle; mNegativeActionListener = builder.mNegativeActionListener; mPositiveButtonStyle = builder.mPositiveButtonStyle; mRequiredIds = builder.mRequiredIds; mOptionalIds = builder.mOptionalIds; mDescription = builder.mDescription; mFlags = builder.mFlags; mCustomDescription = builder.mCustomDescription; mValidator = builder.mValidator; if (builder.mSanitizers == null) { mSanitizerKeys = null; mSanitizerValues = null; } else { final int size = builder.mSanitizers.size(); mSanitizerKeys = new InternalSanitizer[size]; mSanitizerValues = new AutofillId[size][]; for (int i = 0; i < size; i++) { mSanitizerKeys[i] = builder.mSanitizers.keyAt(i); mSanitizerValues[i] = builder.mSanitizers.valueAt(i); } } mTriggerId = builder.mTriggerId; } /** @hide */ public @NegativeButtonStyle int getNegativeActionStyle() { return mNegativeButtonStyle; } /** @hide */ public @Nullable IntentSender getNegativeActionListener() { return mNegativeActionListener; } /** @hide */ public @PositiveButtonStyle int getPositiveActionStyle() { return mPositiveButtonStyle; } /** @hide */ public @Nullable AutofillId[] getRequiredIds() { return mRequiredIds; } /** @hide */ public @Nullable AutofillId[] getOptionalIds() { return mOptionalIds; } /** @hide */ public @SaveDataType int getType() { return mType; } /** @hide */ public @SaveInfoFlags int getFlags() { return mFlags; } /** @hide */ public CharSequence getDescription() { return mDescription; } /** @hide */ @Nullable public CustomDescription getCustomDescription() { return mCustomDescription; } /** @hide */ @Nullable public InternalValidator getValidator() { return mValidator; } /** @hide */ @Nullable public InternalSanitizer[] getSanitizerKeys() { return mSanitizerKeys; } /** @hide */ @Nullable public AutofillId[][] getSanitizerValues() { return mSanitizerValues; } /** @hide */ @Nullable public AutofillId getTriggerId() { return mTriggerId; } /** * A builder for {@link SaveInfo} objects. */ public static final class Builder { private final @SaveDataType int mType; private @NegativeButtonStyle int mNegativeButtonStyle = NEGATIVE_BUTTON_STYLE_CANCEL; private @PositiveButtonStyle int mPositiveButtonStyle = POSITIVE_BUTTON_STYLE_SAVE; private IntentSender mNegativeActionListener; private final AutofillId[] mRequiredIds; private AutofillId[] mOptionalIds; private CharSequence mDescription; private boolean mDestroyed; private int mFlags; private CustomDescription mCustomDescription; private InternalValidator mValidator; private ArrayMap mSanitizers; // Set used to validate against duplicate ids. private ArraySet mSanitizerIds; private AutofillId mTriggerId; /** * Creates a new builder. * * @param type the type of information the associated {@link FillResponse} represents. It * can be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, * {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD}, * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD}, * {@link SaveInfo#SAVE_DATA_TYPE_DEBIT_CARD}, {@link SaveInfo#SAVE_DATA_TYPE_PAYMENT_CARD}, * {@link SaveInfo#SAVE_DATA_TYPE_GENERIC_CARD}, {@link SaveInfo#SAVE_DATA_TYPE_USERNAME}, * or {@link SaveInfo#SAVE_DATA_TYPE_EMAIL_ADDRESS}. * @param requiredIds ids of all required views that will trigger a save request. * *

See {@link SaveInfo} for more info. * * @throws IllegalArgumentException if {@code requiredIds} is {@code null} or empty, or if * it contains any {@code null} entry. */ public Builder(@SaveDataType int type, @NonNull AutofillId[] requiredIds) { mType = type; mRequiredIds = assertValid(requiredIds); } /** * Creates a new builder when no id is required. * *

When using this builder, caller must call {@link #setOptionalIds(AutofillId[])} before * calling {@link #build()}. * * @param type the type of information the associated {@link FillResponse} represents. It * can be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, * {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD}, * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD}, * {@link SaveInfo#SAVE_DATA_TYPE_DEBIT_CARD}, {@link SaveInfo#SAVE_DATA_TYPE_PAYMENT_CARD}, * {@link SaveInfo#SAVE_DATA_TYPE_GENERIC_CARD}, {@link SaveInfo#SAVE_DATA_TYPE_USERNAME}, * or {@link SaveInfo#SAVE_DATA_TYPE_EMAIL_ADDRESS}. * *

See {@link SaveInfo} for more info. */ public Builder(@SaveDataType int type) { mType = type; mRequiredIds = null; } /** * Sets flags changing the save behavior. * * @param flags {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE}, * {@link #FLAG_DONT_SAVE_ON_FINISH}, {@link #FLAG_DELAY_SAVE}, or {@code 0}. * @return This builder. */ public @NonNull Builder setFlags(@SaveInfoFlags int flags) { throwIfDestroyed(); mFlags = Preconditions.checkFlagsArgument(flags, FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE | FLAG_DONT_SAVE_ON_FINISH | FLAG_DELAY_SAVE); return this; } /** * Sets the ids of additional, optional views the service would be interested to save. * *

See {@link SaveInfo} for more info. * * @param ids The ids of the optional views. * @return This builder. * * @throws IllegalArgumentException if {@code ids} is {@code null} or empty, or if * it contains any {@code null} entry. */ public @NonNull Builder setOptionalIds(@NonNull AutofillId[] ids) { throwIfDestroyed(); mOptionalIds = assertValid(ids); return this; } /** * Sets an optional description to be shown in the UI when the user is asked to save. * *

Typically, it describes how the data will be stored by the service, so it can help * users to decide whether they can trust the service to save their data. * * @param description a succint description. * @return This Builder. * * @throws IllegalStateException if this call was made after calling * {@link #setCustomDescription(CustomDescription)}. */ public @NonNull Builder setDescription(@Nullable CharSequence description) { throwIfDestroyed(); Preconditions.checkState(mCustomDescription == null, "Can call setDescription() or setCustomDescription(), but not both"); mDescription = description; return this; } /** * Sets a custom description to be shown in the UI when the user is asked to save. * *

Typically used when the service must show more info about the object being saved, * like a credit card logo, masked number, and expiration date. * * @param customDescription the custom description. * @return This Builder. * * @throws IllegalStateException if this call was made after calling * {@link #setDescription(CharSequence)}. */ public @NonNull Builder setCustomDescription(@NonNull CustomDescription customDescription) { throwIfDestroyed(); Preconditions.checkState(mDescription == null, "Can call setDescription() or setCustomDescription(), but not both"); mCustomDescription = customDescription; return this; } /** * Sets the style and listener for the negative save action. * *

This allows an autofill service to customize the style and be * notified when the user selects the negative action in the save * UI. Note that selecting the negative action regardless of its style * and listener being customized would dismiss the save UI and if a * custom listener intent is provided then this intent is * started. The default style is {@link #NEGATIVE_BUTTON_STYLE_CANCEL}

* * @param style The action style. * @param listener The action listener. * @return This builder. * * @see #NEGATIVE_BUTTON_STYLE_CANCEL * @see #NEGATIVE_BUTTON_STYLE_REJECT * @see #NEGATIVE_BUTTON_STYLE_NEVER * * @throws IllegalArgumentException If the style is invalid */ public @NonNull Builder setNegativeAction(@NegativeButtonStyle int style, @Nullable IntentSender listener) { throwIfDestroyed(); Preconditions.checkArgumentInRange(style, NEGATIVE_BUTTON_STYLE_CANCEL, NEGATIVE_BUTTON_STYLE_NEVER, "style"); mNegativeButtonStyle = style; mNegativeActionListener = listener; return this; } /** * Sets the style for the positive save action. * *

This allows an autofill service to customize the style of the * positive action in the save UI. Note that selecting the positive * action regardless of its style would dismiss the save UI and calling * into the {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback) save request}. * The service should take the next action if selecting style * {@link #POSITIVE_BUTTON_STYLE_CONTINUE}. The default style is * {@link #POSITIVE_BUTTON_STYLE_SAVE} * * @param style The action style. * @return This builder. * * @see #POSITIVE_BUTTON_STYLE_SAVE * @see #POSITIVE_BUTTON_STYLE_CONTINUE * * @throws IllegalArgumentException If the style is invalid */ public @NonNull Builder setPositiveAction(@PositiveButtonStyle int style) { throwIfDestroyed(); Preconditions.checkArgumentInRange(style, POSITIVE_BUTTON_STYLE_SAVE, POSITIVE_BUTTON_STYLE_CONTINUE, "style"); mPositiveButtonStyle = style; return this; } /** * Sets an object used to validate the user input - if the input is not valid, the * autofill save UI is not shown. * *

Typically used to validate credit card numbers. Examples: * *

Validator for a credit number that must have exactly 16 digits: * *

         * Validator validator = new RegexValidator(ccNumberId, Pattern.compile(""^\\d{16}$"))
         * 
* *

Validator for a credit number that must pass a Luhn checksum and either have * 16 digits, or 15 digits starting with 108: * *

         * import static android.service.autofill.Validators.and;
         * import static android.service.autofill.Validators.or;
         *
         * Validator validator =
         *   and(
         *     new LuhnChecksumValidator(ccNumberId),
         *     or(
         *       new RegexValidator(ccNumberId, Pattern.compile("^\\d{16}$")),
         *       new RegexValidator(ccNumberId, Pattern.compile("^108\\d{12}$"))
         *     )
         *   );
         * 
* *

Note: the example above is just for illustrative purposes; the same validator * could be created using a single regex for the {@code OR} part: * *

         * Validator validator =
         *   and(
         *     new LuhnChecksumValidator(ccNumberId),
         *     new RegexValidator(ccNumberId, Pattern.compile(""^(\\d{16}|108\\d{12})$"))
         *   );
         * 
* *

Validator for a credit number contained in just 4 fields and that must have exactly * 4 digits on each field: * *

         * import static android.service.autofill.Validators.and;
         *
         * Validator validator =
         *   and(
         *     new RegexValidator(ccNumberId1, Pattern.compile("^\\d{4}$")),
         *     new RegexValidator(ccNumberId2, Pattern.compile("^\\d{4}$")),
         *     new RegexValidator(ccNumberId3, Pattern.compile("^\\d{4}$")),
         *     new RegexValidator(ccNumberId4, Pattern.compile("^\\d{4}$"))
         *   );
         * 
* * @param validator an implementation provided by the Android System. * @return this builder. * * @throws IllegalArgumentException if {@code validator} is not a class provided * by the Android System. */ public @NonNull Builder setValidator(@NonNull Validator validator) { throwIfDestroyed(); Preconditions.checkArgument((validator instanceof InternalValidator), "not provided by Android System: %s", validator); mValidator = (InternalValidator) validator; return this; } /** * Adds a sanitizer for one or more field. * *

When a sanitizer is set for a field, the {@link AutofillValue} is sent to the * sanitizer before a save request is triggered. * *

Typically used to avoid displaying the save UI for values that are autofilled but * reformattedby the app. For example, to remove spaces between every 4 digits of a * credit card number: * *

         * builder.addSanitizer(new TextValueSanitizer(
         *     Pattern.compile("^(\\d{4})\\s?(\\d{4})\\s?(\\d{4})\\s?(\\d{4})$", "$1$2$3$4")),
         *     ccNumberId);
         * 
* *

The same sanitizer can be reused to sanitize multiple fields. For example, to trim * both the username and password fields: * *

         * builder.addSanitizer(
         *     new TextValueSanitizer(Pattern.compile("^\\s*(.*)\\s*$"), "$1"),
         *         usernameId, passwordId);
         * 
* *

The sanitizer can also be used as an alternative for a * {@link #setValidator(Validator) validator}. If any of the {@code ids} is a * {@link #Builder(int, AutofillId[]) required id} and the {@code sanitizer} fails * because of it, then the save UI is not shown. * * @param sanitizer an implementation provided by the Android System. * @param ids id of fields whose value will be sanitized. * @return this builder. * * @throws IllegalArgumentException if a sanitizer for any of the {@code ids} has already * been added or if {@code ids} is empty. */ public @NonNull Builder addSanitizer(@NonNull Sanitizer sanitizer, @NonNull AutofillId... ids) { throwIfDestroyed(); Preconditions.checkArgument(!ArrayUtils.isEmpty(ids), "ids cannot be empty or null"); Preconditions.checkArgument((sanitizer instanceof InternalSanitizer), "not provided by Android System: %s", sanitizer); if (mSanitizers == null) { mSanitizers = new ArrayMap<>(); mSanitizerIds = new ArraySet<>(ids.length); } // Check for duplicates first. for (AutofillId id : ids) { Preconditions.checkArgument(!mSanitizerIds.contains(id), "already added %s", id); mSanitizerIds.add(id); } mSanitizers.put((InternalSanitizer) sanitizer, ids); return this; } /** * Explicitly defines the view that should commit the autofill context when clicked. * *

Usually, the save request is only automatically * triggered after the activity is * finished or all relevant views become invisible, but there are scenarios where the * autofill context is automatically commited too late * —for example, when the activity manually clears the autofillable views when a * button is tapped. This method can be used to trigger the autofill save UI earlier in * these scenarios. * *

Note: This method should only be used in scenarios where the automatic workflow * is not enough, otherwise it could trigger the autofill save UI when it should not— * for example, when the user entered invalid credentials for the autofillable views. */ public @NonNull Builder setTriggerId(@NonNull AutofillId id) { throwIfDestroyed(); mTriggerId = Objects.requireNonNull(id); return this; } /** * Builds a new {@link SaveInfo} instance. * * If no {@link #Builder(int, AutofillId[]) required ids}, * or {@link #setOptionalIds(AutofillId[]) optional ids}, or {@link #FLAG_DELAY_SAVE} * were set, Save Dialog will only be triggered if platform detection is enabled, which * is indicated when {@link FillRequest#getHints()} is not empty. */ public SaveInfo build() { throwIfDestroyed(); mDestroyed = true; return new SaveInfo(this); } private void throwIfDestroyed() { if (mDestroyed) { throw new IllegalStateException("Already called #build()"); } } } ///////////////////////////////////// // Object "contract" methods. // ///////////////////////////////////// @Override public String toString() { if (!sDebug) return super.toString(); final StringBuilder builder = new StringBuilder("SaveInfo: [type=") .append(DebugUtils.flagsToString(SaveInfo.class, "SAVE_DATA_TYPE_", mType)) .append(", requiredIds=").append(Arrays.toString(mRequiredIds)) .append(", negative style=").append(DebugUtils.flagsToString(SaveInfo.class, "NEGATIVE_BUTTON_STYLE_", mNegativeButtonStyle)) .append(", positive style=").append(DebugUtils.flagsToString(SaveInfo.class, "POSITIVE_BUTTON_STYLE_", mPositiveButtonStyle)); if (mOptionalIds != null) { builder.append(", optionalIds=").append(Arrays.toString(mOptionalIds)); } if (mDescription != null) { builder.append(", description=").append(mDescription); } if (mFlags != 0) { builder.append(", flags=").append(mFlags); } if (mCustomDescription != null) { builder.append(", customDescription=").append(mCustomDescription); } if (mValidator != null) { builder.append(", validator=").append(mValidator); } if (mSanitizerKeys != null) { builder.append(", sanitizerKeys=").append(mSanitizerKeys.length); } if (mSanitizerValues != null) { builder.append(", sanitizerValues=").append(mSanitizerValues.length); } if (mTriggerId != null) { builder.append(", triggerId=").append(mTriggerId); } return builder.append("]").toString(); } ///////////////////////////////////// // Parcelable "contract" methods. // ///////////////////////////////////// @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(mType); parcel.writeParcelableArray(mRequiredIds, flags); parcel.writeParcelableArray(mOptionalIds, flags); parcel.writeInt(mNegativeButtonStyle); parcel.writeParcelable(mNegativeActionListener, flags); parcel.writeInt(mPositiveButtonStyle); parcel.writeCharSequence(mDescription); parcel.writeParcelable(mCustomDescription, flags); parcel.writeParcelable(mValidator, flags); parcel.writeParcelableArray(mSanitizerKeys, flags); if (mSanitizerKeys != null) { for (int i = 0; i < mSanitizerValues.length; i++) { parcel.writeParcelableArray(mSanitizerValues[i], flags); } } parcel.writeParcelable(mTriggerId, flags); parcel.writeInt(mFlags); } public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public SaveInfo createFromParcel(Parcel parcel) { // 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 int type = parcel.readInt(); final AutofillId[] requiredIds = parcel.readParcelableArray(null, AutofillId.class); final Builder builder = requiredIds != null ? new Builder(type, requiredIds) : new Builder(type); final AutofillId[] optionalIds = parcel.readParcelableArray(null, AutofillId.class); if (optionalIds != null) { builder.setOptionalIds(optionalIds); } builder.setNegativeAction(parcel.readInt(), parcel.readParcelable(null, android.content.IntentSender.class)); builder.setPositiveAction(parcel.readInt()); builder.setDescription(parcel.readCharSequence()); final CustomDescription customDescripton = parcel.readParcelable(null, android.service.autofill.CustomDescription.class); if (customDescripton != null) { builder.setCustomDescription(customDescripton); } final InternalValidator validator = parcel.readParcelable(null, android.service.autofill.InternalValidator.class); if (validator != null) { builder.setValidator(validator); } final InternalSanitizer[] sanitizers = parcel.readParcelableArray(null, InternalSanitizer.class); if (sanitizers != null) { final int size = sanitizers.length; for (int i = 0; i < size; i++) { final AutofillId[] autofillIds = parcel.readParcelableArray(null, AutofillId.class); builder.addSanitizer(sanitizers[i], autofillIds); } } final AutofillId triggerId = parcel.readParcelable(null, android.view.autofill.AutofillId.class); if (triggerId != null) { builder.setTriggerId(triggerId); } builder.setFlags(parcel.readInt()); return builder.build(); } @Override public SaveInfo[] newArray(int size) { return new SaveInfo[size]; } }; }