| /* |
| * 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; |
| |
| /** |
| * 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. |
| * |
| * <p>A {@link SaveInfo} is always associated with a {@link FillResponse}, and it contains at least |
| * two pieces of information: |
| * |
| * <ol> |
| * <li>The type(s) of user data (like password or credit card info) that would be saved. |
| * <li>The minimum set of views (represented by their {@link AutofillId}) that need to be changed |
| * to trigger a save request. |
| * </ol> |
| * |
| * <p>Typically, the {@link SaveInfo} contains the same {@code id}s as the {@link Dataset}: |
| * |
| * <pre class="prettyprint"> |
| * 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(); |
| * </pre> |
| * |
| * <p>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. |
| * |
| * <p>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}: |
| * |
| * <pre class="prettyprint"> |
| * new FillResponse.Builder() |
| * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_PASSWORD, |
| * new AutofillId[] { id1, id2 }).build()) |
| * .build(); |
| * </pre> |
| * |
| * <p>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: |
| * |
| * <pre class="prettyprint"> |
| * 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(); |
| * </pre> |
| * |
| * <a name="TriggeringSaveRequest"></a> |
| * <h3>Triggering a save request</h3> |
| * |
| * <p>The {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} can be triggered after |
| * any of the following events: |
| * <ul> |
| * <li>The {@link Activity} finishes. |
| * <li>The app explicitly calls {@link AutofillManager#commit()}. |
| * <li>All required views become invisible (if the {@link SaveInfo} was created with the |
| * {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} flag). |
| * <li>The user clicks a specific view (defined by {@link Builder#setTriggerId(AutofillId)}. |
| * </ul> |
| * |
| * <p>But it is only triggered when all conditions below are met: |
| * <ul> |
| * <li>The {@link SaveInfo} associated with the {@link FillResponse} is not {@code null} neither |
| * has the {@link #FLAG_DELAY_SAVE} flag. |
| * <li>The {@link AutofillValue}s of all required views (as set by the {@code requiredIds} passed |
| * to the {@link SaveInfo.Builder} constructor are not empty. |
| * <li>The {@link AutofillValue} of at least one view (be it required or optional) has changed |
| * (i.e., it's neither the same value passed in a {@link Dataset}, nor the initial value |
| * presented in the view). |
| * <li>There is no {@link Dataset} in the last {@link FillResponse} that completely matches the |
| * screen state (i.e., all required and optional fields in the dataset have the same value as |
| * the fields in the screen). |
| * <li>The user explicitly tapped the autofill save UI asking to save data for autofill. |
| * </ul> |
| * |
| * <a name="CustomizingSaveUI"></a> |
| * <h3>Customizing the autofill save UI</h3> |
| * |
| * <p>The service can also customize some aspects of the autofill save UI: |
| * <ul> |
| * <li>Add a simple subtitle by calling {@link Builder#setDescription(CharSequence)}. |
| * <li>Add a customized subtitle by calling |
| * {@link Builder#setCustomDescription(CustomDescription)}. |
| * <li>Customize the button used to reject the save request by calling |
| * {@link Builder#setNegativeAction(int, IntentSender)}. |
| * <li>Decide whether the UI should be shown based on the user input validation by calling |
| * {@link Builder#setValidator(Validator)}. |
| * </ul> |
| */ |
| 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; |
| |
| /** |
| * 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; |
| |
| /** @hide */ |
| @IntDef(prefix = { "NEGATIVE_BUTTON_STYLE_" }, value = { |
| NEGATIVE_BUTTON_STYLE_CANCEL, |
| NEGATIVE_BUTTON_STYLE_REJECT |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| @interface NegativeButtonStyle{} |
| |
| /** @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 |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| @interface SaveDataType{} |
| |
| /** |
| * Usually, a save request is only automatically <a href="#TriggeringSaveRequest">triggered</a> |
| * 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 <a href="#TriggeringSaveRequest">triggered</a> |
| * once the {@link Activity} finishes. If this flag is set, finishing the activity doesn't |
| * trigger a save request. |
| * |
| * <p>This flag is typically used in conjunction with {@link Builder#setTriggerId(AutofillId)}. |
| */ |
| public static final int FLAG_DONT_SAVE_ON_FINISH = 0x2; |
| |
| /** |
| * Don't trigger the autofill save UI when the autofill context associated with the response |
| * associated with this {@link SaveInfo} is {@link AutofillManager#commit() committed}, |
| * but keep its {@link FillContext} so it's delivered in a future |
| * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback) save request} of an |
| * activity belonging to the same task. |
| * |
| * <p>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 |
| // flagss, 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 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; |
| |
| private SaveInfo(Builder builder) { |
| mType = builder.mType; |
| mNegativeButtonStyle = builder.mNegativeButtonStyle; |
| mNegativeActionListener = builder.mNegativeActionListener; |
| 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 @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 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<InternalSanitizer, AutofillId[]> mSanitizers; |
| // Set used to validate against duplicate ids. |
| private ArraySet<AutofillId> 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_USERNAME}, or |
| * {@link SaveInfo#SAVE_DATA_TYPE_EMAIL_ADDRESS}. |
| * @param requiredIds ids of all required views that will trigger a save request. |
| * |
| * <p>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. |
| * |
| * <p>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_USERNAME}, or |
| * {@link SaveInfo#SAVE_DATA_TYPE_EMAIL_ADDRESS}. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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}</p> |
| * |
| * @param style The action style. |
| * @param listener The action listener. |
| * @return This builder. |
| * |
| * @see #NEGATIVE_BUTTON_STYLE_CANCEL |
| * @see #NEGATIVE_BUTTON_STYLE_REJECT |
| * |
| * @throws IllegalArgumentException If the style is invalid |
| */ |
| public @NonNull Builder setNegativeAction(@NegativeButtonStyle int style, |
| @Nullable IntentSender listener) { |
| throwIfDestroyed(); |
| if (style != NEGATIVE_BUTTON_STYLE_CANCEL |
| && style != NEGATIVE_BUTTON_STYLE_REJECT) { |
| throw new IllegalArgumentException("Invalid style: " + style); |
| } |
| mNegativeButtonStyle = style; |
| mNegativeActionListener = listener; |
| return this; |
| } |
| |
| /** |
| * Sets an object used to validate the user input - if the input is not valid, the |
| * autofill save UI is not shown. |
| * |
| * <p>Typically used to validate credit card numbers. Examples: |
| * |
| * <p>Validator for a credit number that must have exactly 16 digits: |
| * |
| * <pre class="prettyprint"> |
| * Validator validator = new RegexValidator(ccNumberId, Pattern.compile(""^\\d{16}$")) |
| * </pre> |
| * |
| * <p>Validator for a credit number that must pass a Luhn checksum and either have |
| * 16 digits, or 15 digits starting with 108: |
| * |
| * <pre class="prettyprint"> |
| * 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}$")) |
| * ) |
| * ); |
| * </pre> |
| * |
| * <p><b>Note:</b> the example above is just for illustrative purposes; the same validator |
| * could be created using a single regex for the {@code OR} part: |
| * |
| * <pre class="prettyprint"> |
| * Validator validator = |
| * and( |
| * new LuhnChecksumValidator(ccNumberId), |
| * new RegexValidator(ccNumberId, Pattern.compile(""^(\\d{16}|108\\d{12})$")) |
| * ); |
| * </pre> |
| * |
| * <p>Validator for a credit number contained in just 4 fields and that must have exactly |
| * 4 digits on each field: |
| * |
| * <pre class="prettyprint"> |
| * 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}$")) |
| * ); |
| * </pre> |
| * |
| * @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: " + validator); |
| mValidator = (InternalValidator) validator; |
| return this; |
| } |
| |
| /** |
| * Adds a sanitizer for one or more field. |
| * |
| * <p>When a sanitizer is set for a field, the {@link AutofillValue} is sent to the |
| * sanitizer before a save request is <a href="#TriggeringSaveRequest">triggered</a>. |
| * |
| * <p>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: |
| * |
| * <pre class="prettyprint"> |
| * builder.addSanitizer(new TextValueSanitizer( |
| * Pattern.compile("^(\\d{4})\\s?(\\d{4})\\s?(\\d{4})\\s?(\\d{4})$", "$1$2$3$4")), |
| * ccNumberId); |
| * </pre> |
| * |
| * <p>The same sanitizer can be reused to sanitize multiple fields. For example, to trim |
| * both the username and password fields: |
| * |
| * <pre class="prettyprint"> |
| * builder.addSanitizer( |
| * new TextValueSanitizer(Pattern.compile("^\\s*(.*)\\s*$"), "$1"), |
| * usernameId, passwordId); |
| * </pre> |
| * |
| * <p>The sanitizer can also be used as an alternative for a |
| * {@link #setValidator(Validator) validator}. If any of the {@code ids} is a |
| * {@link #SaveInfo.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: " + 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. |
| * |
| * <p>Usually, the save request is only automatically |
| * <a href="#TriggeringSaveRequest">triggered</a> 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. |
| * |
| * <p><b>Note:</b> 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 = Preconditions.checkNotNull(id); |
| return this; |
| } |
| |
| /** |
| * Builds a new {@link SaveInfo} instance. |
| * |
| * @throws IllegalStateException if no |
| * {@link #SaveInfo.Builder(int, AutofillId[]) required ids}, |
| * or {@link #setOptionalIds(AutofillId[]) optional ids}, or {@link #FLAG_DELAY_SAVE} |
| * were set |
| */ |
| public SaveInfo build() { |
| throwIfDestroyed(); |
| Preconditions.checkState( |
| !ArrayUtils.isEmpty(mRequiredIds) || !ArrayUtils.isEmpty(mOptionalIds) |
| || (mFlags & FLAG_DELAY_SAVE) != 0, |
| "must have at least one required or optional id or FLAG_DELAYED_SAVE"); |
| 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(", style=").append(DebugUtils.flagsToString(SaveInfo.class, |
| "NEGATIVE_BUTTON_STYLE_", mNegativeButtonStyle)); |
| 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.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 Parcelable.Creator<SaveInfo> CREATOR = new Parcelable.Creator<SaveInfo>() { |
| @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)); |
| builder.setDescription(parcel.readCharSequence()); |
| final CustomDescription customDescripton = parcel.readParcelable(null); |
| if (customDescripton != null) { |
| builder.setCustomDescription(customDescripton); |
| } |
| final InternalValidator validator = parcel.readParcelable(null); |
| 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); |
| if (triggerId != null) { |
| builder.setTriggerId(triggerId); |
| } |
| builder.setFlags(parcel.readInt()); |
| return builder.build(); |
| } |
| |
| @Override |
| public SaveInfo[] newArray(int size) { |
| return new SaveInfo[size]; |
| } |
| }; |
| } |