mUniqueValueCategoryPairs;
/**
* Creates a new builder for the user data used for field
* classification.
*
* The user data must contain at least one pair of {@code value} -> {@code categoryId},
* and more pairs can be added through the {@link #add(String, String)} method. For example:
*
*
* new UserData.Builder("v1", "Bart Simpson", "name")
* .add("bart.simpson@example.com", "email")
* .add("el_barto@example.com", "email")
* .build();
*
*
* @param id id used to identify the whole {@link UserData} object. This id is also returned
* by {@link AutofillManager#getUserDataId()}, which can be used to check if the
* {@link UserData} is up-to-date without fetching the whole object (through
* {@link AutofillManager#getUserData()}).
*
* @param value value of the user data.
* @param categoryId autofill field category.
*
* @throws IllegalArgumentException if any of the following occurs:
*
* - {@code id} is empty
* - {@code categoryId} is empty
* - {@code value} is empty
* - the length of {@code value} is lower than {@link UserData#getMinValueLength()}
* - the length of {@code value} is higher than
* {@link UserData#getMaxValueLength()}
*
*/
public Builder(@NonNull String id, @NonNull String value, @NonNull String categoryId) {
mId = checkNotEmpty("id", id);
checkNotEmpty("categoryId", categoryId);
checkValidValue(value);
final int maxUserDataSize = getMaxUserDataSize();
mCategoryIds = new ArrayList<>(maxUserDataSize);
mValues = new ArrayList<>(maxUserDataSize);
mUniqueValueCategoryPairs = new ArraySet<>(maxUserDataSize);
mUniqueCategoryIds = new ArraySet<>(getMaxCategoryCount());
addMapping(value, categoryId);
}
/**
* Sets the default algorithm used for
* field classification.
*
* The currently available algorithms can be retrieve through
* {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
*
*
If not set, the
* {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is
* used instead.
*
* @param name name of the algorithm or {@code null} to used default.
* @param args optional arguments to the algorithm.
*
* @return this builder
*/
@NonNull
public Builder setFieldClassificationAlgorithm(@Nullable String name,
@Nullable Bundle args) {
throwIfDestroyed();
mDefaultAlgorithm = name;
mDefaultArgs = args;
return this;
}
/**
* Sets the algorithm used for field classification
* for the specified category.
*
*
The currently available algorithms can be retrieved through
* {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
*
*
If not set, the
* {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is
* used instead.
*
* @param categoryId autofill field category.
* @param name name of the algorithm or {@code null} to used default.
* @param args optional arguments to the algorithm.
*
* @return this builder
*/
@NonNull
public Builder setFieldClassificationAlgorithmForCategory(@NonNull String categoryId,
@Nullable String name, @Nullable Bundle args) {
throwIfDestroyed();
Objects.requireNonNull(categoryId);
if (mCategoryAlgorithms == null) {
mCategoryAlgorithms = new ArrayMap<>(getMaxCategoryCount());
}
if (mCategoryArgs == null) {
mCategoryArgs = new ArrayMap<>(getMaxCategoryCount());
}
mCategoryAlgorithms.put(categoryId, name);
mCategoryArgs.put(categoryId, args);
return this;
}
/**
* Adds a new value for user data.
*
* @param value value of the user data.
* @param categoryId string used to identify the category the value is associated with.
*
* @throws IllegalStateException if:
*
* - {@link #build()} already called
* - the {@code value} has already been added (Note: this restriction was
* lifted on Android {@link android.os.Build.VERSION_CODES#Q} and later)
* - the number of unique {@code categoryId} values added so far is more than
* {@link UserData#getMaxCategoryCount()}
* - the number of {@code values} added so far is is more than
* {@link UserData#getMaxUserDataSize()}
*
*
* @throws IllegalArgumentException if any of the following occurs:
*
* - {@code id} is empty
* - {@code categoryId} is empty
* - {@code value} is empty
* - the length of {@code value} is lower than {@link UserData#getMinValueLength()}
* - the length of {@code value} is higher than
* {@link UserData#getMaxValueLength()}
*
*/
@NonNull
public Builder add(@NonNull String value, @NonNull String categoryId) {
throwIfDestroyed();
checkNotEmpty("categoryId", categoryId);
checkValidValue(value);
if (!mUniqueCategoryIds.contains(categoryId)) {
// New category - check size
Preconditions.checkState(mUniqueCategoryIds.size() < getMaxCategoryCount(),
"already added %d unique category ids", mUniqueCategoryIds.size());
}
Preconditions.checkState(mValues.size() < getMaxUserDataSize(),
"already added %d elements", mValues.size());
addMapping(value, categoryId);
return this;
}
private void addMapping(@NonNull String value, @NonNull String categoryId) {
final String pair = value + ":" + categoryId;
if (mUniqueValueCategoryPairs.contains(pair)) {
// Don't include value on message because it could contain PII
Log.w(TAG, "Ignoring entry with same value / category");
return;
}
mCategoryIds.add(categoryId);
mValues.add(value);
mUniqueCategoryIds.add(categoryId);
mUniqueValueCategoryPairs.add(pair);
}
private String checkNotEmpty(@NonNull String name, @Nullable String value) {
Objects.requireNonNull(value);
Preconditions.checkArgument(!TextUtils.isEmpty(value), "%s cannot be empty", name);
return value;
}
private void checkValidValue(@Nullable String value) {
Objects.requireNonNull(value);
final int length = value.length();
Preconditions.checkArgumentInRange(length, getMinValueLength(),
getMaxValueLength(), "value length (" + length + ")");
}
/**
* Creates a new {@link UserData} instance.
*
* You should not interact with this builder once this method is called.
*
* @throws IllegalStateException if {@link #build()} was already called.
*
* @return The built dataset.
*/
@NonNull
public UserData build() {
throwIfDestroyed();
mDestroyed = true;
return new UserData(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("UserData: [id=").append(mId);
// Cannot disclose category ids or values because they could contain PII
builder.append(", categoryIds=");
Helper.appendRedacted(builder, mCategoryIds);
builder.append(", values=");
Helper.appendRedacted(builder, mValues);
return builder.append("]").toString();
}
/////////////////////////////////////
// Parcelable "contract" methods. //
/////////////////////////////////////
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(mId);
parcel.writeStringArray(mCategoryIds);
parcel.writeStringArray(mValues);
parcel.writeString(mDefaultAlgorithm);
parcel.writeBundle(mDefaultArgs);
parcel.writeMap(mCategoryAlgorithms);
parcel.writeMap(mCategoryArgs);
}
public static final @android.annotation.NonNull Parcelable.Creator CREATOR =
new Parcelable.Creator() {
@Override
public UserData 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 String id = parcel.readString();
final String[] categoryIds = parcel.readStringArray();
final String[] values = parcel.readStringArray();
final String defaultAlgorithm = parcel.readString();
final Bundle defaultArgs = parcel.readBundle();
final ArrayMap categoryAlgorithms = new ArrayMap<>();
parcel.readMap(categoryAlgorithms, String.class.getClassLoader());
final ArrayMap categoryArgs = new ArrayMap<>();
parcel.readMap(categoryArgs, Bundle.class.getClassLoader());
final Builder builder = new Builder(id, values[0], categoryIds[0])
.setFieldClassificationAlgorithm(defaultAlgorithm, defaultArgs);
for (int i = 1; i < categoryIds.length; i++) {
String categoryId = categoryIds[i];
builder.add(values[i], categoryId);
}
final int size = categoryAlgorithms.size();
if (size > 0) {
for (int i = 0; i < size; i++) {
final String categoryId = categoryAlgorithms.keyAt(i);
builder.setFieldClassificationAlgorithmForCategory(categoryId,
categoryAlgorithms.valueAt(i), categoryArgs.get(categoryId));
}
}
return builder.build();
}
@Override
public UserData[] newArray(int size) {
return new UserData[size];
}
};
/**
* Gets the maximum number of values that can be added to a {@link UserData}.
*/
public static int getMaxUserDataSize() {
return getInt(AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, DEFAULT_MAX_USER_DATA_SIZE);
}
/**
* Gets the maximum number of ids that can be passed to {@link
* FillResponse.Builder#setFieldClassificationIds(android.view.autofill.AutofillId...)}.
*/
public static int getMaxFieldClassificationIdsSize() {
return getInt(AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE,
DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE);
}
/**
* Gets the maximum number of unique category ids that can be passed to
* the builder's constructor and {@link Builder#add(String, String)}.
*/
public static int getMaxCategoryCount() {
return getInt(AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT, DEFAULT_MAX_CATEGORY_COUNT);
}
/**
* Gets the minimum length of values passed to the builder's constructor or
* or {@link Builder#add(String, String)}.
*/
public static int getMinValueLength() {
return getInt(AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, DEFAULT_MIN_VALUE_LENGTH);
}
/**
* Gets the maximum length of values passed to the builder's constructor or
* or {@link Builder#add(String, String)}.
*/
public static int getMaxValueLength() {
return getInt(AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, DEFAULT_MAX_VALUE_LENGTH);
}
private static int getInt(String settings, int defaultValue) {
ContentResolver cr = null;
final ActivityThread at = ActivityThread.currentActivityThread();
if (at != null) {
cr = at.getApplication().getContentResolver();
}
if (cr == null) {
Log.w(TAG, "Could not read from " + settings + "; hardcoding " + defaultValue);
return defaultValue;
}
return Settings.Secure.getInt(cr, settings, defaultValue);
}
}