/* * Copyright 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.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT; import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE; import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE; import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH; import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH; import static android.view.autofill.Helper.sDebug; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.app.ActivityThread; import android.content.ContentResolver; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.provider.Settings; import android.service.autofill.FieldClassification.Match; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.view.autofill.AutofillManager; import android.view.autofill.Helper; import com.android.internal.util.Preconditions; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Objects; /** * Defines the user data used for * field classification. */ public final class UserData implements FieldClassificationUserData, Parcelable { private static final String TAG = "UserData"; private static final int DEFAULT_MAX_USER_DATA_SIZE = 50; private static final int DEFAULT_MAX_CATEGORY_COUNT = 10; private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10; private static final int DEFAULT_MIN_VALUE_LENGTH = 3; private static final int DEFAULT_MAX_VALUE_LENGTH = 100; private final String mId; private final String[] mCategoryIds; private final String[] mValues; private final String mDefaultAlgorithm; private final Bundle mDefaultArgs; private final ArrayMap mCategoryAlgorithms; private final ArrayMap mCategoryArgs; private UserData(Builder builder) { mId = builder.mId; mCategoryIds = new String[builder.mCategoryIds.size()]; builder.mCategoryIds.toArray(mCategoryIds); mValues = new String[builder.mValues.size()]; builder.mValues.toArray(mValues); builder.mValues.toArray(mValues); mDefaultAlgorithm = builder.mDefaultAlgorithm; mDefaultArgs = builder.mDefaultArgs; mCategoryAlgorithms = builder.mCategoryAlgorithms; mCategoryArgs = builder.mCategoryArgs; } /** * Gets the name of the default algorithm that is used to calculate * {@link Match#getScore()} match scores}. */ @Nullable @Override public String getFieldClassificationAlgorithm() { return mDefaultAlgorithm; } /** @hide */ @Override public Bundle getDefaultFieldClassificationArgs() { return mDefaultArgs; } /** * Gets the name of the algorithm corresponding to the specific autofill category * that is used to calculate {@link Match#getScore() match scores} * * @param categoryId autofill field category * * @return String name of algorithm, null if none found. */ @Nullable @Override public String getFieldClassificationAlgorithmForCategory(@NonNull String categoryId) { Objects.requireNonNull(categoryId); if (mCategoryAlgorithms == null || !mCategoryAlgorithms.containsKey(categoryId)) { return null; } return mCategoryAlgorithms.get(categoryId); } /** * Gets the id. */ public String getId() { return mId; } /** @hide */ @Override public String[] getCategoryIds() { return mCategoryIds; } /** @hide */ @Override public String[] getValues() { return mValues; } /** @hide */ @TestApi @Override public ArrayMap getFieldClassificationAlgorithms() { return mCategoryAlgorithms; } /** @hide */ @Override public ArrayMap getFieldClassificationArgs() { return mCategoryArgs; } /** @hide */ public void dump(String prefix, PrintWriter pw) { pw.print(prefix); pw.print("id: "); pw.print(mId); pw.print(prefix); pw.print("Default Algorithm: "); pw.print(mDefaultAlgorithm); pw.print(prefix); pw.print("Default Args"); pw.print(mDefaultArgs); if (mCategoryAlgorithms != null && mCategoryAlgorithms.size() > 0) { pw.print(prefix); pw.print("Algorithms per category: "); for (int i = 0; i < mCategoryAlgorithms.size(); i++) { pw.print(prefix); pw.print(prefix); pw.print(mCategoryAlgorithms.keyAt(i)); pw.print(": "); pw.println(Helper.getRedacted(mCategoryAlgorithms.valueAt(i))); pw.print("args="); pw.print(mCategoryArgs.get(mCategoryAlgorithms.keyAt(i))); } } // Cannot disclose field ids or values because they could contain PII pw.print(prefix); pw.print("Field ids size: "); pw.println(mCategoryIds.length); for (int i = 0; i < mCategoryIds.length; i++) { pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": "); pw.println(Helper.getRedacted(mCategoryIds[i])); } pw.print(prefix); pw.print("Values size: "); pw.println(mValues.length); for (int i = 0; i < mValues.length; i++) { pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": "); pw.println(Helper.getRedacted(mValues[i])); } } /** @hide */ public static void dumpConstraints(String prefix, PrintWriter pw) { pw.print(prefix); pw.print("maxUserDataSize: "); pw.println(getMaxUserDataSize()); pw.print(prefix); pw.print("maxFieldClassificationIdsSize: "); pw.println(getMaxFieldClassificationIdsSize()); pw.print(prefix); pw.print("maxCategoryCount: "); pw.println(getMaxCategoryCount()); pw.print(prefix); pw.print("minValueLength: "); pw.println(getMinValueLength()); pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength()); } /** * A builder for {@link UserData} objects. */ public static final class Builder { private final String mId; private final ArrayList mCategoryIds; private final ArrayList mValues; private String mDefaultAlgorithm; private Bundle mDefaultArgs; // Map of autofill field categories to fleid classification algorithms and args private ArrayMap mCategoryAlgorithms; private ArrayMap mCategoryArgs; private boolean mDestroyed; // Non-persistent array used to limit the number of unique ids. private final ArraySet mUniqueCategoryIds; // Non-persistent array used to ignore duplaicated value/category pairs. private final ArraySet 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); } }