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 */
16package android.service.autofill;
17
18import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT;
19import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE;
20import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE;
21import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH;
22import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH;
23import static android.view.autofill.Helper.sDebug;
24
25import android.annotation.NonNull;
26import android.annotation.Nullable;
27import android.app.ActivityThread;
28import android.content.ContentResolver;
29import android.os.Bundle;
30import android.os.Parcel;
31import android.os.Parcelable;
32import android.provider.Settings;
33import android.service.autofill.FieldClassification.Match;
34import android.text.TextUtils;
35import android.util.ArraySet;
36import android.util.Log;
37import android.view.autofill.AutofillManager;
38import android.view.autofill.Helper;
39
40import com.android.internal.util.Preconditions;
41
42import java.io.PrintWriter;
43import java.util.ArrayList;
44
45/**
46 * Defines the user data used for
47 * <a href="AutofillService.html#FieldClassification">field classification</a>.
48 */
49public 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@example.com", "email")
159         *   .add("el_barto@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