/* * 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: * *
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(); ** * *
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: *
The service can also customize some aspects of the autofill save UI: *
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 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} 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 for a credit number that must pass a Luhn checksum and either have
* 16 digits, or 15 digits starting with 108:
*
* 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 for a credit number contained in just 4 fields and that must have exactly
* 4 digits on each 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:
*
* The same sanitizer can be reused to sanitize multiple fields. For example, to trim
* both the username and password fields:
*
* 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
* Validator validator = new RegexValidator(ccNumberId, Pattern.compile(""^\\d{16}$"))
*
*
*
* 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}$"))
* )
* );
*
*
*
* Validator validator =
* and(
* new LuhnChecksumValidator(ccNumberId),
* new RegexValidator(ccNumberId, Pattern.compile(""^(\\d{16}|108\\d{12})$"))
* );
*
*
*
* 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.
*
*
* builder.addSanitizer(new TextValueSanitizer(
* Pattern.compile("^(\\d{4})\\s?(\\d{4})\\s?(\\d{4})\\s?(\\d{4})$", "$1$2$3$4")),
* ccNumberId);
*
*
*
* builder.addSanitizer(
* new TextValueSanitizer(Pattern.compile("^\\s*(.*)\\s*$"), "$1"),
* usernameId, passwordId);
*
*
*