blob: 33aa025ecb4fc634ff0f8f58431c3cf59ac8e4f8 [file] [log] [blame]
/*
* 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.autofillservice.cts;
import static android.autofillservice.cts.Helper.getAutofillIds;
import static com.google.common.truth.Truth.assertWithMessage;
import android.app.assist.AssistStructure;
import android.app.assist.AssistStructure.ViewNode;
import android.content.IntentSender;
import android.os.Bundle;
import android.service.autofill.CustomDescription;
import android.service.autofill.Dataset;
import android.service.autofill.FillCallback;
import android.service.autofill.FillResponse;
import android.service.autofill.Sanitizer;
import android.service.autofill.SaveInfo;
import android.service.autofill.UserData;
import android.service.autofill.Validator;
import android.util.Pair;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.widget.RemoteViews;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
/**
* Helper class used to produce a {@link FillResponse} based on expected fields that should be
* present in the {@link AssistStructure}.
*
* <p>Typical usage:
*
* <pre class="prettyprint">
* InstrumentedAutoFillService.setFillResponse(new CannedFillResponse.Builder()
* .addDataset(new CannedDataset.Builder("dataset_name")
* .setField("resource_id1", AutofillValue.forText("value1"))
* .setField("resource_id2", AutofillValue.forText("value2"))
* .build())
* .build());
* </pre class="prettyprint">
*/
final class CannedFillResponse {
private final ResponseType mResponseType;
private final List<CannedDataset> mDatasets;
private final ArrayList<Pair<Sanitizer, AutofillId[]>> mSanitizers;
private final String mFailureMessage;
private final int mSaveType;
private final Validator mValidator;
private final String[] mRequiredSavableIds;
private final String[] mOptionalSavableIds;
private final AutofillId[] mRequiredSavableAutofillIds;
private final String mSaveDescription;
private final CustomDescription mCustomDescription;
private final Bundle mExtras;
private final RemoteViews mPresentation;
private final RemoteViews mHeader;
private final RemoteViews mFooter;
private final IntentSender mAuthentication;
private final String[] mAuthenticationIds;
private final String[] mIgnoredIds;
private final int mNegativeActionStyle;
private final IntentSender mNegativeActionListener;
private final int mSaveInfoFlags;
private final int mFillResponseFlags;
private final AutofillId mSaveTriggerId;
private final long mDisableDuration;
private final AutofillId[] mFieldClassificationIds;
private final boolean mFieldClassificationIdsOverflow;
private CannedFillResponse(Builder builder) {
mResponseType = builder.mResponseType;
mDatasets = builder.mDatasets;
mFailureMessage = builder.mFailureMessage;
mValidator = builder.mValidator;
mRequiredSavableIds = builder.mRequiredSavableIds;
mRequiredSavableAutofillIds = builder.mRequiredSavableAutofillIds;
mOptionalSavableIds = builder.mOptionalSavableIds;
mSaveDescription = builder.mSaveDescription;
mCustomDescription = builder.mCustomDescription;
mSaveType = builder.mSaveType;
mExtras = builder.mExtras;
mPresentation = builder.mPresentation;
mHeader = builder.mHeader;
mFooter = builder.mFooter;
mAuthentication = builder.mAuthentication;
mAuthenticationIds = builder.mAuthenticationIds;
mIgnoredIds = builder.mIgnoredIds;
mNegativeActionStyle = builder.mNegativeActionStyle;
mNegativeActionListener = builder.mNegativeActionListener;
mSanitizers = builder.mSanitizers;
mSaveInfoFlags = builder.mSaveInfoFlags;
mFillResponseFlags = builder.mFillResponseFlags;
mSaveTriggerId = builder.mSaveTriggerId;
mDisableDuration = builder.mDisableDuration;
mFieldClassificationIds = builder.mFieldClassificationIds;
mFieldClassificationIdsOverflow = builder.mFieldClassificationIdsOverflow;
}
/**
* Constant used to pass a {@code null} response to the
* {@link FillCallback#onSuccess(FillResponse)} method.
*/
static final CannedFillResponse NO_RESPONSE =
new Builder(ResponseType.NULL).build();
/**
* Constant used to emulate a timeout by not calling any method on {@link FillCallback}.
*/
static final CannedFillResponse DO_NOT_REPLY_RESPONSE =
new Builder(ResponseType.TIMEOUT).build();
String getFailureMessage() {
return mFailureMessage;
}
ResponseType getResponseType() {
return mResponseType;
}
/**
* Creates a new response, replacing the dataset field ids by the real ids from the assist
* structure.
*/
FillResponse asFillResponse(Function<String, ViewNode> nodeResolver) {
final FillResponse.Builder builder = new FillResponse.Builder()
.setFlags(mFillResponseFlags);
if (mDatasets != null) {
for (CannedDataset cannedDataset : mDatasets) {
final Dataset dataset = cannedDataset.asDataset(nodeResolver);
assertWithMessage("Cannot create datase").that(dataset).isNotNull();
builder.addDataset(dataset);
}
}
if (mRequiredSavableIds != null || mRequiredSavableAutofillIds != null) {
final SaveInfo.Builder saveInfo;
if (mRequiredSavableAutofillIds != null) {
saveInfo = new SaveInfo.Builder(mSaveType, mRequiredSavableAutofillIds);
} else {
saveInfo = mRequiredSavableIds == null || mRequiredSavableIds.length == 0
? new SaveInfo.Builder(mSaveType)
: new SaveInfo.Builder(mSaveType,
getAutofillIds(nodeResolver, mRequiredSavableIds));
}
saveInfo.setFlags(mSaveInfoFlags);
if (mValidator != null) {
saveInfo.setValidator(mValidator);
}
if (mOptionalSavableIds != null) {
saveInfo.setOptionalIds(getAutofillIds(nodeResolver, mOptionalSavableIds));
}
if (mSaveDescription != null) {
saveInfo.setDescription(mSaveDescription);
}
saveInfo.setNegativeAction(mNegativeActionStyle, mNegativeActionListener);
if (mCustomDescription != null) {
saveInfo.setCustomDescription(mCustomDescription);
}
for (Pair<Sanitizer, AutofillId[]> sanitizer : mSanitizers) {
saveInfo.addSanitizer(sanitizer.first, sanitizer.second);
}
if (mSaveTriggerId != null) {
saveInfo.setTriggerId(mSaveTriggerId);
}
builder.setSaveInfo(saveInfo.build());
}
if (mIgnoredIds != null) {
builder.setIgnoredIds(getAutofillIds(nodeResolver, mIgnoredIds));
}
if (mAuthenticationIds != null) {
builder.setAuthentication(getAutofillIds(nodeResolver, mAuthenticationIds),
mAuthentication, mPresentation);
}
if (mDisableDuration > 0) {
builder.disableAutofill(mDisableDuration);
}
if (mFieldClassificationIdsOverflow) {
final int length = UserData.getMaxFieldClassificationIdsSize() + 1;
final AutofillId[] fieldIds = new AutofillId[length];
for (int i = 0; i < length; i++) {
fieldIds[i] = new AutofillId(i);
}
builder.setFieldClassificationIds(fieldIds);
} else if (mFieldClassificationIds != null) {
builder.setFieldClassificationIds(mFieldClassificationIds);
}
if (mExtras != null) {
builder.setClientState(mExtras);
}
if (mHeader != null) {
builder.setHeader(mHeader);
}
if (mFooter != null) {
builder.setFooter(mFooter);
}
return builder.build();
}
@Override
public String toString() {
return "CannedFillResponse: [type=" + mResponseType
+ ",datasets=" + mDatasets
+ ", requiredSavableIds=" + Arrays.toString(mRequiredSavableIds)
+ ", optionalSavableIds=" + Arrays.toString(mOptionalSavableIds)
+ ", requiredSavableAutofillIds=" + Arrays.toString(mRequiredSavableAutofillIds)
+ ", saveInfoFlags=" + mSaveInfoFlags
+ ", fillResponseFlags=" + mFillResponseFlags
+ ", failureMessage=" + mFailureMessage
+ ", saveDescription=" + mSaveDescription
+ ", mCustomDescription=" + mCustomDescription
+ ", hasPresentation=" + (mPresentation != null)
+ ", hasHeader=" + (mHeader != null)
+ ", hasFooter=" + (mFooter != null)
+ ", hasAuthentication=" + (mAuthentication != null)
+ ", authenticationIds=" + Arrays.toString(mAuthenticationIds)
+ ", ignoredIds=" + Arrays.toString(mIgnoredIds)
+ ", sanitizers =" + mSanitizers
+ ", saveTriggerId=" + mSaveTriggerId
+ ", disableDuration=" + mDisableDuration
+ ", fieldClassificationIds=" + Arrays.toString(mFieldClassificationIds)
+ ", fieldClassificationIdsOverflow=" + mFieldClassificationIdsOverflow
+ "]";
}
enum ResponseType {
NORMAL,
NULL,
TIMEOUT
}
static class Builder {
private final List<CannedDataset> mDatasets = new ArrayList<>();
private final ArrayList<Pair<Sanitizer, AutofillId[]>> mSanitizers = new ArrayList<>();
private final ResponseType mResponseType;
private String mFailureMessage;
private Validator mValidator;
private String[] mRequiredSavableIds;
private String[] mOptionalSavableIds;
private AutofillId[] mRequiredSavableAutofillIds;
private String mSaveDescription;
public CustomDescription mCustomDescription;
public int mSaveType = -1;
private Bundle mExtras;
private RemoteViews mPresentation;
private RemoteViews mFooter;
private RemoteViews mHeader;
private IntentSender mAuthentication;
private String[] mAuthenticationIds;
private String[] mIgnoredIds;
private int mNegativeActionStyle;
private IntentSender mNegativeActionListener;
private int mSaveInfoFlags;
private int mFillResponseFlags;
private AutofillId mSaveTriggerId;
private long mDisableDuration;
private AutofillId[] mFieldClassificationIds;
private boolean mFieldClassificationIdsOverflow;
public Builder(ResponseType type) {
mResponseType = type;
}
public Builder() {
this(ResponseType.NORMAL);
}
public Builder addDataset(CannedDataset dataset) {
assertWithMessage("already set failure").that(mFailureMessage).isNull();
mDatasets.add(dataset);
return this;
}
/**
* Sets the validator for this request
*/
public Builder setValidator(Validator validator) {
mValidator = validator;
return this;
}
/**
* Sets the required savable ids based on their {@code resourceId}.
*/
public Builder setRequiredSavableIds(int type, String... ids) {
if (mRequiredSavableAutofillIds != null) {
throw new IllegalStateException("Already set required autofill ids: "
+ Arrays.toString(mRequiredSavableAutofillIds));
}
mSaveType = type;
mRequiredSavableIds = ids;
return this;
}
/**
* Sets the required savable ids based on their {@code autofillId}.
*/
public Builder setRequiredSavableAutofillIds(int type, AutofillId... ids) {
if (mRequiredSavableIds != null) {
throw new IllegalStateException("Already set required resource ids: "
+ Arrays.toString(mRequiredSavableIds));
}
mSaveType = type;
mRequiredSavableAutofillIds = ids;
return this;
}
public Builder setSaveInfoFlags(int flags) {
mSaveInfoFlags = flags;
return this;
}
public Builder setFillResponseFlags(int flags) {
mFillResponseFlags = flags;
return this;
}
/**
* Sets the optional savable ids based on they {@code resourceId}.
*/
public Builder setOptionalSavableIds(String... ids) {
mOptionalSavableIds = ids;
return this;
}
/**
* Sets the description passed to the {@link SaveInfo}.
*/
public Builder setSaveDescription(String description) {
mSaveDescription = description;
return this;
}
/**
* Sets the description passed to the {@link SaveInfo}.
*/
public Builder setCustomDescription(CustomDescription description) {
mCustomDescription = description;
return this;
}
/**
* Sets the extra passed to {@link
* android.service.autofill.FillResponse.Builder#setClientState(Bundle)}.
*/
public Builder setExtras(Bundle data) {
mExtras = data;
return this;
}
/**
* Sets the view to present the response in the UI.
*/
public Builder setPresentation(RemoteViews presentation) {
mPresentation = presentation;
return this;
}
/**
* Sets the authentication intent.
*/
public Builder setAuthentication(IntentSender authentication, String... ids) {
mAuthenticationIds = ids;
mAuthentication = authentication;
return this;
}
/**
* Sets the ignored fields based on resource ids.
*/
public Builder setIgnoreFields(String...ids) {
mIgnoredIds = ids;
return this;
}
/**
* Sets the negative action spec.
*/
public Builder setNegativeAction(int style, IntentSender listener) {
mNegativeActionStyle = style;
mNegativeActionListener = listener;
return this;
}
/**
* Adds a save sanitizer.
*/
public Builder addSanitizer(Sanitizer sanitizer, AutofillId... ids) {
mSanitizers.add(new Pair<>(sanitizer, ids));
return this;
}
public CannedFillResponse build() {
return new CannedFillResponse(this);
}
/**
* Sets the response to call {@link FillCallback#onFailure(CharSequence)}.
*/
public Builder returnFailure(String message) {
assertWithMessage("already added datasets").that(mDatasets).isEmpty();
mFailureMessage = message;
return this;
}
/**
* Sets the view that explicitly triggers save.
*/
public Builder setSaveTriggerId(AutofillId id) {
assertWithMessage("already set").that(mSaveTriggerId).isNull();
mSaveTriggerId = id;
return this;
}
public Builder disableAutofill(long duration) {
assertWithMessage("already set").that(mDisableDuration).isEqualTo(0L);
mDisableDuration = duration;
return this;
}
/**
* Sets the ids used for field classification.
*/
public Builder setFieldClassificationIds(AutofillId... ids) {
assertWithMessage("already set").that(mFieldClassificationIds).isNull();
mFieldClassificationIds = ids;
return this;
}
/**
* Forces the service to throw an exception when setting the fields classification ids.
*/
public Builder setFieldClassificationIdsOverflow() {
mFieldClassificationIdsOverflow = true;
return this;
}
public Builder setHeader(RemoteViews header) {
assertWithMessage("already set").that(mHeader).isNull();
mHeader = header;
return this;
}
public Builder setFooter(RemoteViews footer) {
assertWithMessage("already set").that(mFooter).isNull();
mFooter = footer;
return this;
}
}
/**
* Helper class used to produce a {@link Dataset} based on expected fields that should be
* present in the {@link AssistStructure}.
*
* <p>Typical usage:
*
* <pre class="prettyprint">
* InstrumentedAutoFillService.setFillResponse(new CannedFillResponse.Builder()
* .addDataset(new CannedDataset.Builder("dataset_name")
* .setField("resource_id1", AutofillValue.forText("value1"))
* .setField("resource_id2", AutofillValue.forText("value2"))
* .build())
* .build());
* </pre class="prettyprint">
*/
static class CannedDataset {
private final Map<String, AutofillValue> mFieldValues;
private final Map<AutofillId, AutofillValue> mFieldValuesById;
private final Map<AutofillId, RemoteViews> mFieldPresentationsById;
private final Map<String, RemoteViews> mFieldPresentations;
private final Map<String, Pair<Boolean, Pattern>> mFieldFilters;
private final RemoteViews mPresentation;
private final IntentSender mAuthentication;
private final String mId;
private CannedDataset(Builder builder) {
mFieldValues = builder.mFieldValues;
mFieldValuesById = builder.mFieldValuesById;
mFieldPresentationsById = builder.mFieldPresentationsById;
mFieldPresentations = builder.mFieldPresentations;
mFieldFilters = builder.mFieldFilters;
mPresentation = builder.mPresentation;
mAuthentication = builder.mAuthentication;
mId = builder.mId;
}
/**
* Creates a new dataset, replacing the field ids by the real ids from the assist structure.
*/
Dataset asDataset(Function<String, ViewNode> nodeResolver) {
final Dataset.Builder builder = (mPresentation == null)
? new Dataset.Builder()
: new Dataset.Builder(mPresentation);
if (mFieldValues != null) {
for (Map.Entry<String, AutofillValue> entry : mFieldValues.entrySet()) {
final String id = entry.getKey();
final ViewNode node = nodeResolver.apply(id);
if (node == null) {
throw new AssertionError("No node with resource id " + id);
}
final AutofillId autofillId = node.getAutofillId();
final AutofillValue value = entry.getValue();
final RemoteViews presentation = mFieldPresentations.get(id);
final Pair<Boolean, Pattern> filter = mFieldFilters.get(id);
if (presentation != null) {
if (filter == null) {
builder.setValue(autofillId, value, presentation);
} else {
builder.setValue(autofillId, value, filter.second, presentation);
}
} else {
if (filter == null) {
builder.setValue(autofillId, value);
} else {
builder.setValue(autofillId, value, filter.second);
}
}
}
}
if (mFieldValuesById != null) {
// NOTE: filter is not yet supported when calling methods that explicitly pass
// autofill id
for (Map.Entry<AutofillId, AutofillValue> entry : mFieldValuesById.entrySet()) {
final AutofillId autofillId = entry.getKey();
final AutofillValue value = entry.getValue();
final RemoteViews presentation = mFieldPresentationsById.get(autofillId);
if (presentation != null) {
builder.setValue(autofillId, value, presentation);
} else {
builder.setValue(autofillId, value);
}
}
}
builder.setId(mId).setAuthentication(mAuthentication);
return builder.build();
}
@Override
public String toString() {
return "CannedDataset " + mId + " : [hasPresentation=" + (mPresentation != null)
+ ", fieldPresentations=" + (mFieldPresentations)
+ ", fieldPresentationsById=" + (mFieldPresentationsById)
+ ", hasAuthentication=" + (mAuthentication != null)
+ ", fieldValues=" + mFieldValues
+ ", fieldValuesById=" + mFieldValuesById
+ ", fieldFilters=" + mFieldFilters + "]";
}
static class Builder {
private final Map<String, AutofillValue> mFieldValues = new HashMap<>();
private final Map<AutofillId, AutofillValue> mFieldValuesById = new HashMap<>();
private final Map<String, RemoteViews> mFieldPresentations = new HashMap<>();
private final Map<AutofillId, RemoteViews> mFieldPresentationsById = new HashMap<>();
private final Map<String, Pair<Boolean, Pattern>> mFieldFilters = new HashMap<>();
private RemoteViews mPresentation;
private IntentSender mAuthentication;
private String mId;
public Builder() {
}
public Builder(RemoteViews presentation) {
mPresentation = presentation;
}
/**
* Sets the canned value of a text field based on its {@code id}.
*
* <p>The meaning of the id is defined by the object using the canned dataset.
* For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
* {@link IdMode}.
*/
public Builder setField(String id, String text) {
return setField(id, AutofillValue.forText(text));
}
/**
* Sets the canned value of a text field based on its {@code id}.
*
* <p>The meaning of the id is defined by the object using the canned dataset.
* For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
* {@link IdMode}.
*/
public Builder setField(String id, String text, Pattern filter) {
return setField(id, AutofillValue.forText(text), true, filter);
}
public Builder setUnfilterableField(String id, String text) {
return setField(id, AutofillValue.forText(text), false, null);
}
/**
* Sets the canned value of a list field based on its its {@code id}.
*
* <p>The meaning of the id is defined by the object using the canned dataset.
* For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
* {@link IdMode}.
*/
public Builder setField(String id, int index) {
return setField(id, AutofillValue.forList(index));
}
/**
* Sets the canned value of a toggle field based on its {@code id}.
*
* <p>The meaning of the id is defined by the object using the canned dataset.
* For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
* {@link IdMode}.
*/
public Builder setField(String id, boolean toggled) {
return setField(id, AutofillValue.forToggle(toggled));
}
/**
* Sets the canned value of a date field based on its {@code id}.
*
* <p>The meaning of the id is defined by the object using the canned dataset.
* For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
* {@link IdMode}.
*/
public Builder setField(String id, long date) {
return setField(id, AutofillValue.forDate(date));
}
/**
* Sets the canned value of a date field based on its {@code id}.
*
* <p>The meaning of the id is defined by the object using the canned dataset.
* For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
* {@link IdMode}.
*/
public Builder setField(String id, AutofillValue value) {
mFieldValues.put(id, value);
return this;
}
/**
* Sets the canned value of a date field based on its {@code autofillId}.
*/
public Builder setField(AutofillId autofillId, AutofillValue value) {
mFieldValuesById.put(autofillId, value);
return this;
}
/**
* Sets the canned value of a date field based on its {@code id}.
*
* <p>The meaning of the id is defined by the object using the canned dataset.
* For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
* {@link IdMode}.
*/
public Builder setField(String id, AutofillValue value, boolean filterable,
Pattern filter) {
setField(id, value);
mFieldFilters.put(id, new Pair<>(filterable, filter));
return this;
}
/**
* Sets the canned value of a field based on its {@code id}.
*
* <p>The meaning of the id is defined by the object using the canned dataset.
* For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
* {@link IdMode}.
*/
public Builder setField(String id, String text, RemoteViews presentation) {
setField(id, text);
mFieldPresentations.put(id, presentation);
return this;
}
/**
* Sets the canned value of a date field based on its {@code autofillId}.
*/
public Builder setField(AutofillId autofillId, String text, RemoteViews presentation) {
setField(autofillId, AutofillValue.forText(text));
mFieldPresentationsById.put(autofillId, presentation);
return this;
}
/**
* Sets the canned value of a field based on its {@code id}.
*
* <p>The meaning of the id is defined by the object using the canned dataset.
* For example, {@link InstrumentedAutoFillService.Replier} resolves the id based on
* {@link IdMode}.
*/
public Builder setField(String id, String text, RemoteViews presentation,
Pattern filter) {
setField(id, text, presentation);
mFieldFilters.put(id, new Pair<>(true, filter));
return this;
}
/**
* Sets the view to present the response in the UI.
*/
public Builder setPresentation(RemoteViews presentation) {
mPresentation = presentation;
return this;
}
/**
* Sets the authentication intent.
*/
public Builder setAuthentication(IntentSender authentication) {
mAuthentication = authentication;
return this;
}
/**
* Sets the name.
*/
public Builder setId(String id) {
mId = id;
return this;
}
public CannedDataset build() {
return new CannedDataset(this);
}
}
}
}