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