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.annotation.TestApi;
28 import android.app.ActivityThread;
29 import android.content.ContentResolver;
30 import android.os.Bundle;
31 import android.os.Parcel;
32 import android.os.Parcelable;
33 import android.provider.Settings;
34 import android.service.autofill.FieldClassification.Match;
35 import android.text.TextUtils;
36 import android.util.ArrayMap;
37 import android.util.ArraySet;
38 import android.util.Log;
39 import android.view.autofill.AutofillManager;
40 import android.view.autofill.Helper;
41 
42 import com.android.internal.util.Preconditions;
43 
44 import java.io.PrintWriter;
45 import java.util.ArrayList;
46 import java.util.Objects;
47 
48 /**
49  * Defines the user data used for
50  * <a href="AutofillService.html#FieldClassification">field classification</a>.
51  */
52 public final class UserData implements FieldClassificationUserData, Parcelable {
53 
54     private static final String TAG = "UserData";
55 
56     private static final int DEFAULT_MAX_USER_DATA_SIZE = 50;
57     private static final int DEFAULT_MAX_CATEGORY_COUNT = 10;
58     private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10;
59     private static final int DEFAULT_MIN_VALUE_LENGTH = 3;
60     private static final int DEFAULT_MAX_VALUE_LENGTH = 100;
61 
62     private final String mId;
63     private final String[] mCategoryIds;
64     private final String[] mValues;
65 
66     private final String mDefaultAlgorithm;
67     private final Bundle mDefaultArgs;
68     private final ArrayMap<String, String> mCategoryAlgorithms;
69     private final ArrayMap<String, Bundle> mCategoryArgs;
70 
UserData(Builder builder)71     private UserData(Builder builder) {
72         mId = builder.mId;
73         mCategoryIds = new String[builder.mCategoryIds.size()];
74         builder.mCategoryIds.toArray(mCategoryIds);
75         mValues = new String[builder.mValues.size()];
76         builder.mValues.toArray(mValues);
77         builder.mValues.toArray(mValues);
78 
79         mDefaultAlgorithm = builder.mDefaultAlgorithm;
80         mDefaultArgs = builder.mDefaultArgs;
81         mCategoryAlgorithms = builder.mCategoryAlgorithms;
82         mCategoryArgs = builder.mCategoryArgs;
83     }
84 
85     /**
86      * Gets the name of the default algorithm that is used to calculate
87      * {@link Match#getScore()} match scores}.
88      */
89     @Nullable
90     @Override
getFieldClassificationAlgorithm()91     public String getFieldClassificationAlgorithm() {
92         return mDefaultAlgorithm;
93     }
94 
95     /** @hide */
96     @Override
getDefaultFieldClassificationArgs()97     public Bundle getDefaultFieldClassificationArgs() {
98         return mDefaultArgs;
99     }
100 
101     /**
102      * Gets the name of the algorithm corresponding to the specific autofill category
103      * that is used to calculate {@link Match#getScore() match scores}
104      *
105      * @param categoryId autofill field category
106      *
107      * @return String name of algorithm, null if none found.
108      */
109     @Nullable
110     @Override
getFieldClassificationAlgorithmForCategory(@onNull String categoryId)111     public String getFieldClassificationAlgorithmForCategory(@NonNull String categoryId) {
112         Objects.requireNonNull(categoryId);
113         if (mCategoryAlgorithms == null || !mCategoryAlgorithms.containsKey(categoryId)) {
114             return null;
115         }
116         return mCategoryAlgorithms.get(categoryId);
117     }
118 
119     /**
120      * Gets the id.
121      */
getId()122     public String getId() {
123         return mId;
124     }
125 
126     /** @hide */
127     @Override
getCategoryIds()128     public String[] getCategoryIds() {
129         return mCategoryIds;
130     }
131 
132     /** @hide */
133     @Override
getValues()134     public String[] getValues() {
135         return mValues;
136     }
137 
138     /** @hide */
139     @TestApi
140     @Override
getFieldClassificationAlgorithms()141     public ArrayMap<String, String> getFieldClassificationAlgorithms() {
142         return mCategoryAlgorithms;
143     }
144 
145     /** @hide */
146     @Override
getFieldClassificationArgs()147     public ArrayMap<String, Bundle> getFieldClassificationArgs() {
148         return mCategoryArgs;
149     }
150 
151     /** @hide */
dump(String prefix, PrintWriter pw)152     public void dump(String prefix, PrintWriter pw) {
153         pw.print(prefix); pw.print("id: "); pw.print(mId);
154         pw.print(prefix); pw.print("Default Algorithm: "); pw.print(mDefaultAlgorithm);
155         pw.print(prefix); pw.print("Default Args"); pw.print(mDefaultArgs);
156         if (mCategoryAlgorithms != null && mCategoryAlgorithms.size() > 0) {
157             pw.print(prefix); pw.print("Algorithms per category: ");
158             for (int i = 0; i < mCategoryAlgorithms.size(); i++) {
159                 pw.print(prefix); pw.print(prefix); pw.print(mCategoryAlgorithms.keyAt(i));
160                 pw.print(": "); pw.println(Helper.getRedacted(mCategoryAlgorithms.valueAt(i)));
161                 pw.print("args="); pw.print(mCategoryArgs.get(mCategoryAlgorithms.keyAt(i)));
162             }
163         }
164         // Cannot disclose field ids or values because they could contain PII
165         pw.print(prefix); pw.print("Field ids size: "); pw.println(mCategoryIds.length);
166         for (int i = 0; i < mCategoryIds.length; i++) {
167             pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
168             pw.println(Helper.getRedacted(mCategoryIds[i]));
169         }
170         pw.print(prefix); pw.print("Values size: "); pw.println(mValues.length);
171         for (int i = 0; i < mValues.length; i++) {
172             pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
173             pw.println(Helper.getRedacted(mValues[i]));
174         }
175     }
176 
177     /** @hide */
dumpConstraints(String prefix, PrintWriter pw)178     public static void dumpConstraints(String prefix, PrintWriter pw) {
179         pw.print(prefix); pw.print("maxUserDataSize: "); pw.println(getMaxUserDataSize());
180         pw.print(prefix); pw.print("maxFieldClassificationIdsSize: ");
181         pw.println(getMaxFieldClassificationIdsSize());
182         pw.print(prefix); pw.print("maxCategoryCount: "); pw.println(getMaxCategoryCount());
183         pw.print(prefix); pw.print("minValueLength: "); pw.println(getMinValueLength());
184         pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength());
185     }
186 
187     /**
188      * A builder for {@link UserData} objects.
189      */
190     public static final class Builder {
191         private final String mId;
192         private final ArrayList<String> mCategoryIds;
193         private final ArrayList<String> mValues;
194         private String mDefaultAlgorithm;
195         private Bundle mDefaultArgs;
196 
197         // Map of autofill field categories to fleid classification algorithms and args
198         private ArrayMap<String, String> mCategoryAlgorithms;
199         private ArrayMap<String, Bundle> mCategoryArgs;
200 
201         private boolean mDestroyed;
202 
203         // Non-persistent array used to limit the number of unique ids.
204         private final ArraySet<String> mUniqueCategoryIds;
205         // Non-persistent array used to ignore duplaicated value/category pairs.
206         private final ArraySet<String> mUniqueValueCategoryPairs;
207 
208         /**
209          * Creates a new builder for the user data used for <a href="#FieldClassification">field
210          * classification</a>.
211          *
212          * <p>The user data must contain at least one pair of {@code value} -> {@code categoryId},
213          * and more pairs can be added through the {@link #add(String, String)} method. For example:
214          *
215          * <pre class="prettyprint">
216          * new UserData.Builder("v1", "Bart Simpson", "name")
217          *   .add("bart.simpson@example.com", "email")
218          *   .add("el_barto@example.com", "email")
219          *   .build();
220          * </pre>
221          *
222          * @param id id used to identify the whole {@link UserData} object. This id is also returned
223          * by {@link AutofillManager#getUserDataId()}, which can be used to check if the
224          * {@link UserData} is up-to-date without fetching the whole object (through
225          * {@link AutofillManager#getUserData()}).
226          *
227          * @param value value of the user data.
228          * @param categoryId autofill field category.
229          *
230          * @throws IllegalArgumentException if any of the following occurs:
231          * <ul>
232          *   <li>{@code id} is empty</li>
233          *   <li>{@code categoryId} is empty</li>
234          *   <li>{@code value} is empty</li>
235          *   <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}</li>
236          *   <li>the length of {@code value} is higher than
237          *       {@link UserData#getMaxValueLength()}</li>
238          * </ul>
239          */
Builder(@onNull String id, @NonNull String value, @NonNull String categoryId)240         public Builder(@NonNull String id, @NonNull String value, @NonNull String categoryId) {
241             mId = checkNotEmpty("id", id);
242             checkNotEmpty("categoryId", categoryId);
243             checkValidValue(value);
244             final int maxUserDataSize = getMaxUserDataSize();
245             mCategoryIds = new ArrayList<>(maxUserDataSize);
246             mValues = new ArrayList<>(maxUserDataSize);
247             mUniqueValueCategoryPairs = new ArraySet<>(maxUserDataSize);
248 
249             mUniqueCategoryIds = new ArraySet<>(getMaxCategoryCount());
250 
251             addMapping(value, categoryId);
252         }
253 
254         /**
255          * Sets the default algorithm used for
256          * <a href="#FieldClassification">field classification</a>.
257          *
258          * <p>The currently available algorithms can be retrieve through
259          * {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
260          *
261          * <p>If not set, the
262          * {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is
263          * used instead.
264          *
265          * @param name name of the algorithm or {@code null} to used default.
266          * @param args optional arguments to the algorithm.
267          *
268          * @return this builder
269          */
270         @NonNull
setFieldClassificationAlgorithm(@ullable String name, @Nullable Bundle args)271         public Builder setFieldClassificationAlgorithm(@Nullable String name,
272                 @Nullable Bundle args) {
273             throwIfDestroyed();
274             mDefaultAlgorithm = name;
275             mDefaultArgs = args;
276             return this;
277         }
278 
279         /**
280          * Sets the algorithm used for <a href="#FieldClassification">field classification</a>
281          * for the specified category.
282          *
283          * <p>The currently available algorithms can be retrieved through
284          * {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
285          *
286          * <p>If not set, the
287          * {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is
288          * used instead.
289          *
290          * @param categoryId autofill field category.
291          * @param name name of the algorithm or {@code null} to used default.
292          * @param args optional arguments to the algorithm.
293          *
294          * @return this builder
295          */
296         @NonNull
setFieldClassificationAlgorithmForCategory(@onNull String categoryId, @Nullable String name, @Nullable Bundle args)297         public Builder setFieldClassificationAlgorithmForCategory(@NonNull String categoryId,
298                 @Nullable String name, @Nullable Bundle args) {
299             throwIfDestroyed();
300             Objects.requireNonNull(categoryId);
301             if (mCategoryAlgorithms == null) {
302                 mCategoryAlgorithms = new ArrayMap<>(getMaxCategoryCount());
303             }
304             if (mCategoryArgs == null) {
305                 mCategoryArgs = new ArrayMap<>(getMaxCategoryCount());
306             }
307             mCategoryAlgorithms.put(categoryId, name);
308             mCategoryArgs.put(categoryId, args);
309             return this;
310         }
311 
312         /**
313          * Adds a new value for user data.
314          *
315          * @param value value of the user data.
316          * @param categoryId string used to identify the category the value is associated with.
317          *
318          * @throws IllegalStateException if:
319          * <ul>
320          *   <li>{@link #build()} already called</li>
321          *   <li>the {@code value} has already been added (<b>Note: </b> this restriction was
322          *   lifted on Android {@link android.os.Build.VERSION_CODES#Q} and later)</li>
323          *   <li>the number of unique {@code categoryId} values added so far is more than
324          *       {@link UserData#getMaxCategoryCount()}</li>
325          *   <li>the number of {@code values} added so far is is more than
326          *       {@link UserData#getMaxUserDataSize()}</li>
327          * </ul>
328          *
329          * @throws IllegalArgumentException if any of the following occurs:
330          * <ul>
331          *   <li>{@code id} is empty</li>
332          *   <li>{@code categoryId} is empty</li>
333          *   <li>{@code value} is empty</li>
334          *   <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}</li>
335          *   <li>the length of {@code value} is higher than
336          *       {@link UserData#getMaxValueLength()}</li>
337          * </ul>
338          */
339         @NonNull
add(@onNull String value, @NonNull String categoryId)340         public Builder add(@NonNull String value, @NonNull String categoryId) {
341             throwIfDestroyed();
342             checkNotEmpty("categoryId", categoryId);
343             checkValidValue(value);
344 
345             if (!mUniqueCategoryIds.contains(categoryId)) {
346                 // New category - check size
347                 Preconditions.checkState(mUniqueCategoryIds.size() < getMaxCategoryCount(),
348                         "already added %d unique category ids", mUniqueCategoryIds.size());
349             }
350 
351             Preconditions.checkState(mValues.size() < getMaxUserDataSize(),
352                     "already added %d elements", mValues.size());
353             addMapping(value, categoryId);
354 
355             return this;
356         }
357 
358         private void addMapping(@NonNull String value, @NonNull String categoryId) {
359             final String pair = value + ":" + categoryId;
360             if (mUniqueValueCategoryPairs.contains(pair)) {
361                 // Don't include value on message because it could contain PII
362                 Log.w(TAG, "Ignoring entry with same value / category");
363                 return;
364             }
365             mCategoryIds.add(categoryId);
366             mValues.add(value);
367             mUniqueCategoryIds.add(categoryId);
368             mUniqueValueCategoryPairs.add(pair);
369         }
370 
371         private String checkNotEmpty(@NonNull String name, @Nullable String value) {
372             Objects.requireNonNull(value);
373             Preconditions.checkArgument(!TextUtils.isEmpty(value), "%s cannot be empty", name);
374             return value;
375         }
376 
377         private void checkValidValue(@Nullable String value) {
378             Objects.requireNonNull(value);
379             final int length = value.length();
380             Preconditions.checkArgumentInRange(length, getMinValueLength(),
381                     getMaxValueLength(), "value length (" + length + ")");
382         }
383 
384         /**
385          * Creates a new {@link UserData} instance.
386          *
387          * <p>You should not interact with this builder once this method is called.
388          *
389          * @throws IllegalStateException if {@link #build()} was already called.
390          *
391          * @return The built dataset.
392          */
393         @NonNull
394         public UserData build() {
395             throwIfDestroyed();
396             mDestroyed = true;
397             return new UserData(this);
398         }
399 
400         private void throwIfDestroyed() {
401             if (mDestroyed) {
402                 throw new IllegalStateException("Already called #build()");
403             }
404         }
405     }
406 
407     /////////////////////////////////////
408     // Object "contract" methods. //
409     /////////////////////////////////////
410     @Override
411     public String toString() {
412         if (!sDebug) return super.toString();
413 
414         final StringBuilder builder = new StringBuilder("UserData: [id=").append(mId);
415         // Cannot disclose category ids or values because they could contain PII
416         builder.append(", categoryIds=");
417         Helper.appendRedacted(builder, mCategoryIds);
418         builder.append(", values=");
419         Helper.appendRedacted(builder, mValues);
420         return builder.append("]").toString();
421     }
422 
423     /////////////////////////////////////
424     // Parcelable "contract" methods. //
425     /////////////////////////////////////
426 
427     @Override
428     public int describeContents() {
429         return 0;
430     }
431 
432     @Override
433     public void writeToParcel(Parcel parcel, int flags) {
434         parcel.writeString(mId);
435         parcel.writeStringArray(mCategoryIds);
436         parcel.writeStringArray(mValues);
437         parcel.writeString(mDefaultAlgorithm);
438         parcel.writeBundle(mDefaultArgs);
439         parcel.writeMap(mCategoryAlgorithms);
440         parcel.writeMap(mCategoryArgs);
441     }
442 
443     public static final @android.annotation.NonNull Parcelable.Creator<UserData> CREATOR =
444             new Parcelable.Creator<UserData>() {
445         @Override
446         public UserData createFromParcel(Parcel parcel) {
447             // Always go through the builder to ensure the data ingested by
448             // the system obeys the contract of the builder to avoid attacks
449             // using specially crafted parcels.
450             final String id = parcel.readString();
451             final String[] categoryIds = parcel.readStringArray();
452             final String[] values = parcel.readStringArray();
453             final String defaultAlgorithm = parcel.readString();
454             final Bundle defaultArgs = parcel.readBundle();
455             final ArrayMap<String, String> categoryAlgorithms = new ArrayMap<>();
456             parcel.readMap(categoryAlgorithms, String.class.getClassLoader());
457             final ArrayMap<String, Bundle> categoryArgs = new ArrayMap<>();
458             parcel.readMap(categoryArgs, Bundle.class.getClassLoader());
459 
460             final Builder builder = new Builder(id, values[0], categoryIds[0])
461                     .setFieldClassificationAlgorithm(defaultAlgorithm, defaultArgs);
462 
463             for (int i = 1; i < categoryIds.length; i++) {
464                 String categoryId = categoryIds[i];
465                 builder.add(values[i], categoryId);
466             }
467 
468             final int size = categoryAlgorithms.size();
469             if (size > 0) {
470                 for (int i = 0; i < size; i++) {
471                     final String categoryId = categoryAlgorithms.keyAt(i);
472                     builder.setFieldClassificationAlgorithmForCategory(categoryId,
473                             categoryAlgorithms.valueAt(i), categoryArgs.get(categoryId));
474                 }
475             }
476             return builder.build();
477         }
478 
479         @Override
480         public UserData[] newArray(int size) {
481             return new UserData[size];
482         }
483     };
484 
485     /**
486      * Gets the maximum number of values that can be added to a {@link UserData}.
487      */
488     public static int getMaxUserDataSize() {
489         return getInt(AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, DEFAULT_MAX_USER_DATA_SIZE);
490     }
491 
492     /**
493      * Gets the maximum number of ids that can be passed to {@link
494      * FillResponse.Builder#setFieldClassificationIds(android.view.autofill.AutofillId...)}.
495      */
496     public static int getMaxFieldClassificationIdsSize() {
497         return getInt(AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE,
498             DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE);
499     }
500 
501     /**
502      * Gets the maximum number of unique category ids that can be passed to
503      * the builder's constructor and {@link Builder#add(String, String)}.
504      */
505     public static int getMaxCategoryCount() {
506         return getInt(AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT, DEFAULT_MAX_CATEGORY_COUNT);
507     }
508 
509     /**
510      * Gets the minimum length of values passed to the builder's constructor or
511      * or {@link Builder#add(String, String)}.
512      */
513     public static int getMinValueLength() {
514         return getInt(AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, DEFAULT_MIN_VALUE_LENGTH);
515     }
516 
517     /**
518      * Gets the maximum length of values passed to the builder's constructor or
519      * or {@link Builder#add(String, String)}.
520      */
521     public static int getMaxValueLength() {
522         return getInt(AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, DEFAULT_MAX_VALUE_LENGTH);
523     }
524 
525     private static int getInt(String settings, int defaultValue) {
526         ContentResolver cr = null;
527         final ActivityThread at = ActivityThread.currentActivityThread();
528         if (at != null) {
529             cr = at.getApplication().getContentResolver();
530         }
531 
532         if (cr == null) {
533             Log.w(TAG, "Could not read from " + settings + "; hardcoding " + defaultValue);
534             return defaultValue;
535         }
536         return Settings.Secure.getInt(cr, settings, defaultValue);
537     }
538 }
539