UserData.java revision 329d04097e1db9b6f801972d94f56c5b56c09e8a
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_FIELD_CLASSIFICATION_IDS_SIZE;
19import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE;
20import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH;
21import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH;
22import static android.view.autofill.Helper.sDebug;
23
24import android.annotation.NonNull;
25import android.annotation.Nullable;
26import android.annotation.TestApi;
27import android.app.ActivityThread;
28import android.content.ContentResolver;
29import android.os.Parcel;
30import android.os.Parcelable;
31import android.provider.Settings;
32import android.util.Log;
33import android.view.autofill.Helper;
34
35import com.android.internal.util.Preconditions;
36
37import java.io.PrintWriter;
38import java.util.ArrayList;
39
40/**
41 * Class used by service to improve autofillable fields detection by tracking the meaning of fields
42 * manually edited by the user (when they match values provided by the service).
43 *
44 * TODO(b/67867469):
45 *  - improve javadoc / add link to section on AutofillService
46 *  - unhide / remove testApi
47 * @hide
48 */
49@TestApi
50public final class UserData implements Parcelable {
51
52    private static final String TAG = "UserData";
53
54    private static final int DEFAULT_MAX_USER_DATA_SIZE = 10;
55    private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10;
56    private static final int DEFAULT_MIN_VALUE_LENGTH = 5;
57    private static final int DEFAULT_MAX_VALUE_LENGTH = 100;
58
59    private final InternalScorer mScorer;
60    private final String[] mRemoteIds;
61    private final String[] mValues;
62
63    private UserData(Builder builder) {
64        mScorer = builder.mScorer;
65        mRemoteIds = new String[builder.mRemoteIds.size()];
66        builder.mRemoteIds.toArray(mRemoteIds);
67        mValues = new String[builder.mValues.size()];
68        builder.mValues.toArray(mValues);
69    }
70
71    /** @hide */
72    public InternalScorer getScorer() {
73        return mScorer;
74    }
75
76    /** @hide */
77    public String[] getRemoteIds() {
78        return mRemoteIds;
79    }
80
81    /** @hide */
82    public String[] getValues() {
83        return mValues;
84    }
85
86    /** @hide */
87    public void dump(String prefix, PrintWriter pw) {
88        pw.print(prefix); pw.print("Scorer: "); pw.println(mScorer);
89        // Cannot disclose remote ids or values because they could contain PII
90        pw.print(prefix); pw.print("Remote ids size: "); pw.println(mRemoteIds.length);
91        for (int i = 0; i < mRemoteIds.length; i++) {
92            pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
93            pw.println(Helper.getRedacted(mRemoteIds[i]));
94        }
95        pw.print(prefix); pw.print("Values size: "); pw.println(mValues.length);
96        for (int i = 0; i < mValues.length; i++) {
97            pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
98            pw.println(Helper.getRedacted(mValues[i]));
99        }
100    }
101
102    /** @hide */
103    public static void dumpConstraints(String prefix, PrintWriter pw) {
104        pw.print(prefix); pw.print("maxUserDataSize: "); pw.println(getMaxUserDataSize());
105        pw.print(prefix); pw.print("maxFieldClassificationIdsSize: ");
106        pw.println(getMaxFieldClassificationIdsSize());
107        pw.print(prefix); pw.print("minValueLength: "); pw.println(getMinValueLength());
108        pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength());
109    }
110
111    /**
112     * A builder for {@link UserData} objects.
113     *
114     * TODO(b/67867469): unhide / remove testApi
115     *
116     * @hide
117     */
118    @TestApi
119    public static final class Builder {
120        private final InternalScorer mScorer;
121        private final ArrayList<String> mRemoteIds;
122        private final ArrayList<String> mValues;
123        private boolean mDestroyed;
124
125        /**
126         * Creates a new builder for the user data used for <a href="#FieldsClassification">fields
127         * classification</a>.
128         *
129         * @throws IllegalArgumentException if any of the following occurs:
130         * <ol>
131         *   <li>{@code remoteId} is empty
132         *   <li>{@code value} is empty
133         *   <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}
134         *   <li>the length of {@code value} is higher than {@link UserData#getMaxValueLength()}
135         *   <li>{@code scorer} is not instance of a class provided by the Android System.
136         * </ol>
137         */
138        public Builder(@NonNull Scorer scorer, @NonNull String remoteId, @NonNull String value) {
139            Preconditions.checkArgument((scorer instanceof InternalScorer),
140                    "not provided by Android System: " + scorer);
141            mScorer = (InternalScorer) scorer;
142            checkValidRemoteId(remoteId);
143            checkValidValue(value);
144            final int capacity = getMaxUserDataSize();
145            mRemoteIds = new ArrayList<>(capacity);
146            mValues = new ArrayList<>(capacity);
147            mRemoteIds.add(remoteId);
148            mValues.add(value);
149        }
150
151        /**
152         * Adds a new value for user data.
153         *
154         * @param remoteId unique string used to identify the user data.
155         * @param value value of the user data.
156         *
157         * @throws IllegalStateException if {@link #build()} or
158         * {@link #add(String, String)} with the same {@code remoteId} has already
159         * been called, or if the number of values add (i.e., calls made to this method plus
160         * constructor) is more than {@link UserData#getMaxUserDataSize()}.
161         *
162         * @throws IllegalArgumentException if {@code remoteId} or {@code value} are empty or if the
163         * length of {@code value} is lower than {@link UserData#getMinValueLength()}
164         * or higher than {@link UserData#getMaxValueLength()}.
165         */
166        public Builder add(@NonNull String remoteId, @NonNull String value) {
167            throwIfDestroyed();
168            checkValidRemoteId(remoteId);
169            checkValidValue(value);
170
171            Preconditions.checkState(!mRemoteIds.contains(remoteId),
172                    // Don't include remoteId on message because it could contain PII
173                    "already has entry with same remoteId");
174            Preconditions.checkState(!mValues.contains(value),
175                    // Don't include remoteId on message because it could contain PII
176                    "already has entry with same value");
177            Preconditions.checkState(mRemoteIds.size() < getMaxUserDataSize(),
178                    "already added " + mRemoteIds.size() + " elements");
179            mRemoteIds.add(remoteId);
180            mValues.add(value);
181
182            return this;
183        }
184
185        private void checkValidRemoteId(@Nullable String remoteId) {
186            Preconditions.checkNotNull(remoteId);
187            Preconditions.checkArgument(!remoteId.isEmpty(), "remoteId cannot be empty");
188        }
189
190        private void checkValidValue(@Nullable String value) {
191            Preconditions.checkNotNull(value);
192            final int length = value.length();
193            Preconditions.checkArgumentInRange(length, getMinValueLength(),
194                    getMaxValueLength(), "value length (" + length + ")");
195        }
196
197        /**
198         * Creates a new {@link UserData} instance.
199         *
200         * <p>You should not interact with this builder once this method is called.
201         *
202         * @throws IllegalStateException if {@link #build()} was already called.
203         *
204         * @return The built dataset.
205         */
206        public UserData build() {
207            throwIfDestroyed();
208            mDestroyed = true;
209            return new UserData(this);
210        }
211
212        private void throwIfDestroyed() {
213            if (mDestroyed) {
214                throw new IllegalStateException("Already called #build()");
215            }
216        }
217    }
218
219    /////////////////////////////////////
220    // Object "contract" methods. //
221    /////////////////////////////////////
222    @Override
223    public String toString() {
224        if (!sDebug) return super.toString();
225
226        final StringBuilder builder = new StringBuilder("UserData: [scorer=").append(mScorer);
227        // Cannot disclose remote ids or values because they could contain PII
228        builder.append(", remoteIds=");
229        Helper.appendRedacted(builder, mRemoteIds);
230        builder.append(", values=");
231        Helper.appendRedacted(builder, mValues);
232        return builder.append("]").toString();
233    }
234
235    /////////////////////////////////////
236    // Parcelable "contract" methods. //
237    /////////////////////////////////////
238
239    @Override
240    public int describeContents() {
241        return 0;
242    }
243
244    @Override
245    public void writeToParcel(Parcel parcel, int flags) {
246        parcel.writeParcelable(mScorer, flags);
247        parcel.writeStringArray(mRemoteIds);
248        parcel.writeStringArray(mValues);
249    }
250
251    public static final Parcelable.Creator<UserData> CREATOR =
252            new Parcelable.Creator<UserData>() {
253        @Override
254        public UserData createFromParcel(Parcel parcel) {
255            // Always go through the builder to ensure the data ingested by
256            // the system obeys the contract of the builder to avoid attacks
257            // using specially crafted parcels.
258            final InternalScorer scorer = parcel.readParcelable(null);
259            final String[] remoteIds = parcel.readStringArray();
260            final String[] values = parcel.readStringArray();
261            final Builder builder = new Builder(scorer, remoteIds[0], values[0]);
262            for (int i = 1; i < remoteIds.length; i++) {
263                builder.add(remoteIds[i], values[i]);
264            }
265            return builder.build();
266        }
267
268        @Override
269        public UserData[] newArray(int size) {
270            return new UserData[size];
271        }
272    };
273
274    /**
275     * Gets the maximum number of values that can be added to a {@link UserData}.
276     */
277    public static int getMaxUserDataSize() {
278        return getInt(AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, DEFAULT_MAX_USER_DATA_SIZE);
279    }
280
281    /**
282     * Gets the maximum number of ids that can be passed to {@link
283     * FillResponse.Builder#setFieldClassificationIds(android.view.autofill.AutofillId...)}.
284     */
285    public static int getMaxFieldClassificationIdsSize() {
286        return getInt(AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE,
287            DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE);
288    }
289
290    /**
291     * Gets the minimum length of values passed to {@link Builder#Builder(Scorer, String, String)}.
292     */
293    public static int getMinValueLength() {
294        return getInt(AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, DEFAULT_MIN_VALUE_LENGTH);
295    }
296
297    /**
298     * Gets the maximum length of values passed to {@link Builder#Builder(Scorer, String, String)}.
299     */
300    public static int getMaxValueLength() {
301        return getInt(AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, DEFAULT_MAX_VALUE_LENGTH);
302    }
303
304    private static int getInt(String settings, int defaultValue) {
305        ContentResolver cr = null;
306        final ActivityThread at = ActivityThread.currentActivityThread();
307        if (at != null) {
308            cr = at.getApplication().getContentResolver();
309        }
310
311        if (cr == null) {
312            Log.w(TAG, "Could not read from " + settings + "; hardcoding " + defaultValue);
313            return defaultValue;
314        }
315        return Settings.Secure.getInt(cr, settings, defaultValue);
316    }
317}
318