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