Home | History | Annotate | Download | only in autofill
      1 /*
      2  * Copyright 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.service.autofill;
     17 
     18 import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT;
     19 import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE;
     20 import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE;
     21 import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH;
     22 import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH;
     23 import static android.view.autofill.Helper.sDebug;
     24 
     25 import android.annotation.NonNull;
     26 import android.annotation.Nullable;
     27 import android.app.ActivityThread;
     28 import android.content.ContentResolver;
     29 import android.os.Bundle;
     30 import android.os.Parcel;
     31 import android.os.Parcelable;
     32 import android.provider.Settings;
     33 import android.service.autofill.FieldClassification.Match;
     34 import android.text.TextUtils;
     35 import android.util.ArraySet;
     36 import android.util.Log;
     37 import android.view.autofill.AutofillManager;
     38 import android.view.autofill.Helper;
     39 
     40 import com.android.internal.util.Preconditions;
     41 
     42 import java.io.PrintWriter;
     43 import java.util.ArrayList;
     44 
     45 /**
     46  * Defines the user data used for
     47  * <a href="AutofillService.html#FieldClassification">field classification</a>.
     48  */
     49 public final class UserData implements Parcelable {
     50 
     51     private static final String TAG = "UserData";
     52 
     53     private static final int DEFAULT_MAX_USER_DATA_SIZE = 50;
     54     private static final int DEFAULT_MAX_CATEGORY_COUNT = 10;
     55     private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10;
     56     private static final int DEFAULT_MIN_VALUE_LENGTH = 3;
     57     private static final int DEFAULT_MAX_VALUE_LENGTH = 100;
     58 
     59     private final String mId;
     60     private final String mAlgorithm;
     61     private final Bundle mAlgorithmArgs;
     62     private final String[] mCategoryIds;
     63     private final String[] mValues;
     64 
     65     private UserData(Builder builder) {
     66         mId = builder.mId;
     67         mAlgorithm = builder.mAlgorithm;
     68         mAlgorithmArgs = builder.mAlgorithmArgs;
     69         mCategoryIds = new String[builder.mCategoryIds.size()];
     70         builder.mCategoryIds.toArray(mCategoryIds);
     71         mValues = new String[builder.mValues.size()];
     72         builder.mValues.toArray(mValues);
     73     }
     74 
     75     /**
     76      * Gets the name of the algorithm that is used to calculate
     77      * {@link Match#getScore() match scores}.
     78      */
     79     @Nullable
     80     public String getFieldClassificationAlgorithm() {
     81         return mAlgorithm;
     82     }
     83 
     84     /**
     85      * Gets the id.
     86      */
     87     public String getId() {
     88         return mId;
     89     }
     90 
     91     /** @hide */
     92     public Bundle getAlgorithmArgs() {
     93         return mAlgorithmArgs;
     94     }
     95 
     96     /** @hide */
     97     public String[] getCategoryIds() {
     98         return mCategoryIds;
     99     }
    100 
    101     /** @hide */
    102     public String[] getValues() {
    103         return mValues;
    104     }
    105 
    106     /** @hide */
    107     public void dump(String prefix, PrintWriter pw) {
    108         pw.print(prefix); pw.print("id: "); pw.print(mId);
    109         pw.print(prefix); pw.print("Algorithm: "); pw.print(mAlgorithm);
    110         pw.print(" Args: "); pw.println(mAlgorithmArgs);
    111 
    112         // Cannot disclose field ids or values because they could contain PII
    113         pw.print(prefix); pw.print("Field ids size: "); pw.println(mCategoryIds.length);
    114         for (int i = 0; i < mCategoryIds.length; i++) {
    115             pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
    116             pw.println(Helper.getRedacted(mCategoryIds[i]));
    117         }
    118         pw.print(prefix); pw.print("Values size: "); pw.println(mValues.length);
    119         for (int i = 0; i < mValues.length; i++) {
    120             pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
    121             pw.println(Helper.getRedacted(mValues[i]));
    122         }
    123     }
    124 
    125     /** @hide */
    126     public static void dumpConstraints(String prefix, PrintWriter pw) {
    127         pw.print(prefix); pw.print("maxUserDataSize: "); pw.println(getMaxUserDataSize());
    128         pw.print(prefix); pw.print("maxFieldClassificationIdsSize: ");
    129         pw.println(getMaxFieldClassificationIdsSize());
    130         pw.print(prefix); pw.print("maxCategoryCount: "); pw.println(getMaxCategoryCount());
    131         pw.print(prefix); pw.print("minValueLength: "); pw.println(getMinValueLength());
    132         pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength());
    133     }
    134 
    135     /**
    136      * A builder for {@link UserData} objects.
    137      */
    138     public static final class Builder {
    139         private final String mId;
    140         private final ArrayList<String> mCategoryIds;
    141         private final ArrayList<String> mValues;
    142         private String mAlgorithm;
    143         private Bundle mAlgorithmArgs;
    144         private boolean mDestroyed;
    145 
    146         // Non-persistent array used to limit the number of unique ids.
    147         private final ArraySet<String> mUniqueCategoryIds;
    148 
    149         /**
    150          * Creates a new builder for the user data used for <a href="#FieldClassification">field
    151          * classification</a>.
    152          *
    153          * <p>The user data must contain at least one pair of {@code value} -> {@code categoryId},
    154          * and more pairs can be added through the {@link #add(String, String)} method. For example:
    155          *
    156          * <pre class="prettyprint">
    157          * new UserData.Builder("v1", "Bart Simpson", "name")
    158          *   .add("bart.simpson (at) example.com", "email")
    159          *   .add("el_barto (at) example.com", "email")
    160          *   .build();
    161          * </pre>
    162          *
    163          * @param id id used to identify the whole {@link UserData} object. This id is also returned
    164          * by {@link AutofillManager#getUserDataId()}, which can be used to check if the
    165          * {@link UserData} is up-to-date without fetching the whole object (through
    166          * {@link AutofillManager#getUserData()}).
    167          *
    168          * @param value value of the user data.
    169          * @param categoryId string used to identify the category the value is associated with.
    170          *
    171          * @throws IllegalArgumentException if any of the following occurs:
    172          * <ul>
    173          *   <li>{@code id} is empty</li>
    174          *   <li>{@code categoryId} is empty</li>
    175          *   <li>{@code value} is empty</li>
    176          *   <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}</li>
    177          *   <li>the length of {@code value} is higher than
    178          *       {@link UserData#getMaxValueLength()}</li>
    179          * </ul>
    180          */
    181         public Builder(@NonNull String id, @NonNull String value, @NonNull String categoryId) {
    182             mId = checkNotEmpty("id", id);
    183             checkNotEmpty("categoryId", categoryId);
    184             checkValidValue(value);
    185             final int maxUserDataSize = getMaxUserDataSize();
    186             mCategoryIds = new ArrayList<>(maxUserDataSize);
    187             mValues = new ArrayList<>(maxUserDataSize);
    188             mUniqueCategoryIds = new ArraySet<>(getMaxCategoryCount());
    189 
    190             addMapping(value, categoryId);
    191         }
    192 
    193         /**
    194          * Sets the algorithm used for <a href="#FieldClassification">field classification</a>.
    195          *
    196          * <p>The currently available algorithms can be retrieve through
    197          * {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
    198          *
    199          * <p>If not set, the
    200          * {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is
    201          * used instead.
    202          *
    203          * @param name name of the algorithm or {@code null} to used default.
    204          * @param args optional arguments to the algorithm.
    205          *
    206          * @return this builder
    207          */
    208         public Builder setFieldClassificationAlgorithm(@Nullable String name,
    209                 @Nullable Bundle args) {
    210             throwIfDestroyed();
    211             mAlgorithm = name;
    212             mAlgorithmArgs = args;
    213             return this;
    214         }
    215 
    216         /**
    217          * Adds a new value for user data.
    218          *
    219          * @param value value of the user data.
    220          * @param categoryId string used to identify the category the value is associated with.
    221          *
    222          * @throws IllegalStateException if:
    223          * <ul>
    224          *   <li>{@link #build()} already called</li>
    225          *   <li>the {@code value} has already been added</li>
    226          *   <li>the number of unique {@code categoryId} values added so far is more than
    227          *       {@link UserData#getMaxCategoryCount()}</li>
    228          *   <li>the number of {@code values} added so far is is more than
    229          *       {@link UserData#getMaxUserDataSize()}</li>
    230          * </ul>
    231          *
    232          * @throws IllegalArgumentException if any of the following occurs:
    233          * <ul>
    234          *   <li>{@code id} is empty</li>
    235          *   <li>{@code categoryId} is empty</li>
    236          *   <li>{@code value} is empty</li>
    237          *   <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}</li>
    238          *   <li>the length of {@code value} is higher than
    239          *       {@link UserData#getMaxValueLength()}</li>
    240          * </ul>
    241          */
    242         public Builder add(@NonNull String value, @NonNull String categoryId) {
    243             throwIfDestroyed();
    244             checkNotEmpty("categoryId", categoryId);
    245             checkValidValue(value);
    246 
    247             if (!mUniqueCategoryIds.contains(categoryId)) {
    248                 // New category - check size
    249                 Preconditions.checkState(mUniqueCategoryIds.size() < getMaxCategoryCount(),
    250                         "already added " + mUniqueCategoryIds.size() + " unique category ids");
    251 
    252             }
    253 
    254             Preconditions.checkState(!mValues.contains(value),
    255                     // Don't include value on message because it could contain PII
    256                     "already has entry with same value");
    257             Preconditions.checkState(mValues.size() < getMaxUserDataSize(),
    258                     "already added " + mValues.size() + " elements");
    259             addMapping(value, categoryId);
    260 
    261             return this;
    262         }
    263 
    264         private void addMapping(@NonNull String value, @NonNull String categoryId) {
    265             mCategoryIds.add(categoryId);
    266             mValues.add(value);
    267             mUniqueCategoryIds.add(categoryId);
    268         }
    269 
    270         private String checkNotEmpty(@NonNull String name, @Nullable String value) {
    271             Preconditions.checkNotNull(value);
    272             Preconditions.checkArgument(!TextUtils.isEmpty(value), "%s cannot be empty", name);
    273             return value;
    274         }
    275 
    276         private void checkValidValue(@Nullable String value) {
    277             Preconditions.checkNotNull(value);
    278             final int length = value.length();
    279             Preconditions.checkArgumentInRange(length, getMinValueLength(),
    280                     getMaxValueLength(), "value length (" + length + ")");
    281         }
    282 
    283         /**
    284          * Creates a new {@link UserData} instance.
    285          *
    286          * <p>You should not interact with this builder once this method is called.
    287          *
    288          * @throws IllegalStateException if {@link #build()} was already called.
    289          *
    290          * @return The built dataset.
    291          */
    292         public UserData build() {
    293             throwIfDestroyed();
    294             mDestroyed = true;
    295             return new UserData(this);
    296         }
    297 
    298         private void throwIfDestroyed() {
    299             if (mDestroyed) {
    300                 throw new IllegalStateException("Already called #build()");
    301             }
    302         }
    303     }
    304 
    305     /////////////////////////////////////
    306     // Object "contract" methods. //
    307     /////////////////////////////////////
    308     @Override
    309     public String toString() {
    310         if (!sDebug) return super.toString();
    311 
    312         final StringBuilder builder = new StringBuilder("UserData: [id=").append(mId)
    313                 .append(", algorithm=").append(mAlgorithm);
    314         // Cannot disclose category ids or values because they could contain PII
    315         builder.append(", categoryIds=");
    316         Helper.appendRedacted(builder, mCategoryIds);
    317         builder.append(", values=");
    318         Helper.appendRedacted(builder, mValues);
    319         return builder.append("]").toString();
    320     }
    321 
    322     /////////////////////////////////////
    323     // Parcelable "contract" methods. //
    324     /////////////////////////////////////
    325 
    326     @Override
    327     public int describeContents() {
    328         return 0;
    329     }
    330 
    331     @Override
    332     public void writeToParcel(Parcel parcel, int flags) {
    333         parcel.writeString(mId);
    334         parcel.writeStringArray(mCategoryIds);
    335         parcel.writeStringArray(mValues);
    336         parcel.writeString(mAlgorithm);
    337         parcel.writeBundle(mAlgorithmArgs);
    338     }
    339 
    340     public static final Parcelable.Creator<UserData> CREATOR =
    341             new Parcelable.Creator<UserData>() {
    342         @Override
    343         public UserData createFromParcel(Parcel parcel) {
    344             // Always go through the builder to ensure the data ingested by
    345             // the system obeys the contract of the builder to avoid attacks
    346             // using specially crafted parcels.
    347             final String id = parcel.readString();
    348             final String[] categoryIds = parcel.readStringArray();
    349             final String[] values = parcel.readStringArray();
    350             final Builder builder = new Builder(id, values[0], categoryIds[0])
    351                     .setFieldClassificationAlgorithm(parcel.readString(), parcel.readBundle());
    352             for (int i = 1; i < categoryIds.length; i++) {
    353                 builder.add(values[i], categoryIds[i]);
    354             }
    355             return builder.build();
    356         }
    357 
    358         @Override
    359         public UserData[] newArray(int size) {
    360             return new UserData[size];
    361         }
    362     };
    363 
    364     /**
    365      * Gets the maximum number of values that can be added to a {@link UserData}.
    366      */
    367     public static int getMaxUserDataSize() {
    368         return getInt(AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, DEFAULT_MAX_USER_DATA_SIZE);
    369     }
    370 
    371     /**
    372      * Gets the maximum number of ids that can be passed to {@link
    373      * FillResponse.Builder#setFieldClassificationIds(android.view.autofill.AutofillId...)}.
    374      */
    375     public static int getMaxFieldClassificationIdsSize() {
    376         return getInt(AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE,
    377             DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE);
    378     }
    379 
    380     /**
    381      * Gets the maximum number of unique category ids that can be passed to
    382      * the builder's constructor and {@link Builder#add(String, String)}.
    383      */
    384     public static int getMaxCategoryCount() {
    385         return getInt(AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT, DEFAULT_MAX_CATEGORY_COUNT);
    386     }
    387 
    388     /**
    389      * Gets the minimum length of values passed to the builder's constructor or
    390      * or {@link Builder#add(String, String)}.
    391      */
    392     public static int getMinValueLength() {
    393         return getInt(AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, DEFAULT_MIN_VALUE_LENGTH);
    394     }
    395 
    396     /**
    397      * Gets the maximum length of values passed to the builder's constructor or
    398      * or {@link Builder#add(String, String)}.
    399      */
    400     public static int getMaxValueLength() {
    401         return getInt(AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, DEFAULT_MAX_VALUE_LENGTH);
    402     }
    403 
    404     private static int getInt(String settings, int defaultValue) {
    405         ContentResolver cr = null;
    406         final ActivityThread at = ActivityThread.currentActivityThread();
    407         if (at != null) {
    408             cr = at.getApplication().getContentResolver();
    409         }
    410 
    411         if (cr == null) {
    412             Log.w(TAG, "Could not read from " + settings + "; hardcoding " + defaultValue);
    413             return defaultValue;
    414         }
    415         return Settings.Secure.getInt(cr, settings, defaultValue);
    416     }
    417 }
    418