1 /* 2 * Copyright (C) 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 17 package android.service.autofill; 18 19 import static android.service.autofill.AutofillServiceHelper.assertValid; 20 import static android.view.autofill.Helper.sDebug; 21 22 import android.annotation.IntDef; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.app.Activity; 26 import android.content.IntentSender; 27 import android.os.Parcel; 28 import android.os.Parcelable; 29 import android.util.ArrayMap; 30 import android.util.ArraySet; 31 import android.util.DebugUtils; 32 import android.view.autofill.AutofillId; 33 import android.view.autofill.AutofillManager; 34 import android.view.autofill.AutofillValue; 35 36 import com.android.internal.util.ArrayUtils; 37 import com.android.internal.util.Preconditions; 38 39 import java.lang.annotation.Retention; 40 import java.lang.annotation.RetentionPolicy; 41 import java.util.Arrays; 42 43 /** 44 * Information used to indicate that an {@link AutofillService} is interested on saving the 45 * user-inputed data for future use, through a 46 * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} 47 * call. 48 * 49 * <p>A {@link SaveInfo} is always associated with a {@link FillResponse}, and it contains at least 50 * two pieces of information: 51 * 52 * <ol> 53 * <li>The type(s) of user data (like password or credit card info) that would be saved. 54 * <li>The minimum set of views (represented by their {@link AutofillId}) that need to be changed 55 * to trigger a save request. 56 * </ol> 57 * 58 * <p>Typically, the {@link SaveInfo} contains the same {@code id}s as the {@link Dataset}: 59 * 60 * <pre class="prettyprint"> 61 * new FillResponse.Builder() 62 * .addDataset(new Dataset.Builder() 63 * .setValue(id1, AutofillValue.forText("homer"), createPresentation("homer")) // username 64 * .setValue(id2, AutofillValue.forText("D'OH!"), createPresentation("password for homer")) // password 65 * .build()) 66 * .setSaveInfo(new SaveInfo.Builder( 67 * SaveInfo.SAVE_DATA_TYPE_USERNAME | SaveInfo.SAVE_DATA_TYPE_PASSWORD, 68 * new AutofillId[] { id1, id2 }).build()) 69 * .build(); 70 * </pre> 71 * 72 * <p>The save type flags are used to display the appropriate strings in the autofill save UI. 73 * You can pass multiple values, but try to keep it short if possible. In the above example, just 74 * {@code SaveInfo.SAVE_DATA_TYPE_PASSWORD} would be enough. 75 * 76 * <p>There might be cases where the {@link AutofillService} knows how to fill the screen, 77 * but the user has no data for it. In that case, the {@link FillResponse} should contain just the 78 * {@link SaveInfo}, but no {@link Dataset Datasets}: 79 * 80 * <pre class="prettyprint"> 81 * new FillResponse.Builder() 82 * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_PASSWORD, 83 * new AutofillId[] { id1, id2 }).build()) 84 * .build(); 85 * </pre> 86 * 87 * <p>There might be cases where the user data in the {@link AutofillService} is enough 88 * to populate some fields but not all, and the service would still be interested on saving the 89 * other fields. In that case, the service could set the 90 * {@link SaveInfo.Builder#setOptionalIds(AutofillId[])} as well: 91 * 92 * <pre class="prettyprint"> 93 * new FillResponse.Builder() 94 * .addDataset(new Dataset.Builder() 95 * .setValue(id1, AutofillValue.forText("742 Evergreen Terrace"), 96 * createPresentation("742 Evergreen Terrace")) // street 97 * .setValue(id2, AutofillValue.forText("Springfield"), 98 * createPresentation("Springfield")) // city 99 * .build()) 100 * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_ADDRESS, 101 * new AutofillId[] { id1, id2 }) // street and city 102 * .setOptionalIds(new AutofillId[] { id3, id4 }) // state and zipcode 103 * .build()) 104 * .build(); 105 * </pre> 106 * 107 * <a name="TriggeringSaveRequest"></a> 108 * <h3>Triggering a save request</h3> 109 * 110 * <p>The {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} can be triggered after 111 * any of the following events: 112 * <ul> 113 * <li>The {@link Activity} finishes. 114 * <li>The app explicitly calls {@link AutofillManager#commit()}. 115 * <li>All required views become invisible (if the {@link SaveInfo} was created with the 116 * {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} flag). 117 * <li>The user clicks a specific view (defined by {@link Builder#setTriggerId(AutofillId)}. 118 * </ul> 119 * 120 * <p>But it is only triggered when all conditions below are met: 121 * <ul> 122 * <li>The {@link SaveInfo} associated with the {@link FillResponse} is not {@code null}. 123 * <li>The {@link AutofillValue}s of all required views (as set by the {@code requiredIds} passed 124 * to the {@link SaveInfo.Builder} constructor are not empty. 125 * <li>The {@link AutofillValue} of at least one view (be it required or optional) has changed 126 * (i.e., it's neither the same value passed in a {@link Dataset}, nor the initial value 127 * presented in the view). 128 * <li>There is no {@link Dataset} in the last {@link FillResponse} that completely matches the 129 * screen state (i.e., all required and optional fields in the dataset have the same value as 130 * the fields in the screen). 131 * <li>The user explicitly tapped the autofill save UI asking to save data for autofill. 132 * </ul> 133 * 134 * <a name="CustomizingSaveUI"></a> 135 * <h3>Customizing the autofill save UI</h3> 136 * 137 * <p>The service can also customize some aspects of the autofill save UI: 138 * <ul> 139 * <li>Add a simple subtitle by calling {@link Builder#setDescription(CharSequence)}. 140 * <li>Add a customized subtitle by calling 141 * {@link Builder#setCustomDescription(CustomDescription)}. 142 * <li>Customize the button used to reject the save request by calling 143 * {@link Builder#setNegativeAction(int, IntentSender)}. 144 * <li>Decide whether the UI should be shown based on the user input validation by calling 145 * {@link Builder#setValidator(Validator)}. 146 * </ul> 147 */ 148 public final class SaveInfo implements Parcelable { 149 150 /** 151 * Type used when the service can save the contents of a screen, but cannot describe what 152 * the content is for. 153 */ 154 public static final int SAVE_DATA_TYPE_GENERIC = 0x0; 155 156 /** 157 * Type used when the {@link FillResponse} represents user credentials that have a password. 158 */ 159 public static final int SAVE_DATA_TYPE_PASSWORD = 0x01; 160 161 /** 162 * Type used on when the {@link FillResponse} represents a physical address (such as street, 163 * city, state, etc). 164 */ 165 public static final int SAVE_DATA_TYPE_ADDRESS = 0x02; 166 167 /** 168 * Type used when the {@link FillResponse} represents a credit card. 169 */ 170 public static final int SAVE_DATA_TYPE_CREDIT_CARD = 0x04; 171 172 /** 173 * Type used when the {@link FillResponse} represents just an username, without a password. 174 */ 175 public static final int SAVE_DATA_TYPE_USERNAME = 0x08; 176 177 /** 178 * Type used when the {@link FillResponse} represents just an email address, without a password. 179 */ 180 public static final int SAVE_DATA_TYPE_EMAIL_ADDRESS = 0x10; 181 182 /** 183 * Style for the negative button of the save UI to cancel the 184 * save operation. In this case, the user tapping the negative 185 * button signals that they would prefer to not save the filled 186 * content. 187 */ 188 public static final int NEGATIVE_BUTTON_STYLE_CANCEL = 0; 189 190 /** 191 * Style for the negative button of the save UI to reject the 192 * save operation. This could be useful if the user needs to 193 * opt-in your service and the save prompt is an advertisement 194 * of the potential value you can add to the user. In this 195 * case, the user tapping the negative button sends a strong 196 * signal that the feature may not be useful and you may 197 * consider some backoff strategy. 198 */ 199 public static final int NEGATIVE_BUTTON_STYLE_REJECT = 1; 200 201 /** @hide */ 202 @IntDef(prefix = { "NEGATIVE_BUTTON_STYLE_" }, value = { 203 NEGATIVE_BUTTON_STYLE_CANCEL, 204 NEGATIVE_BUTTON_STYLE_REJECT 205 }) 206 @Retention(RetentionPolicy.SOURCE) 207 @interface NegativeButtonStyle{} 208 209 /** @hide */ 210 @IntDef(flag = true, prefix = { "SAVE_DATA_TYPE_" }, value = { 211 SAVE_DATA_TYPE_GENERIC, 212 SAVE_DATA_TYPE_PASSWORD, 213 SAVE_DATA_TYPE_ADDRESS, 214 SAVE_DATA_TYPE_CREDIT_CARD, 215 SAVE_DATA_TYPE_USERNAME, 216 SAVE_DATA_TYPE_EMAIL_ADDRESS 217 }) 218 @Retention(RetentionPolicy.SOURCE) 219 @interface SaveDataType{} 220 221 /** 222 * Usually, a save request is only automatically <a href="#TriggeringSaveRequest">triggered</a> 223 * once the {@link Activity} finishes. If this flag is set, it is triggered once all saved views 224 * become invisible. 225 */ 226 public static final int FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE = 0x1; 227 228 /** 229 * By default, a save request is automatically <a href="#TriggeringSaveRequest">triggered</a> 230 * once the {@link Activity} finishes. If this flag is set, finishing the activity doesn't 231 * trigger a save request. 232 * 233 * <p>This flag is typically used in conjunction with {@link Builder#setTriggerId(AutofillId)}. 234 */ 235 public static final int FLAG_DONT_SAVE_ON_FINISH = 0x2; 236 237 /** @hide */ 238 @IntDef(flag = true, prefix = { "FLAG_" }, value = { 239 FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, 240 FLAG_DONT_SAVE_ON_FINISH 241 }) 242 @Retention(RetentionPolicy.SOURCE) 243 @interface SaveInfoFlags{} 244 245 private final @SaveDataType int mType; 246 private final @NegativeButtonStyle int mNegativeButtonStyle; 247 private final IntentSender mNegativeActionListener; 248 private final AutofillId[] mRequiredIds; 249 private final AutofillId[] mOptionalIds; 250 private final CharSequence mDescription; 251 private final int mFlags; 252 private final CustomDescription mCustomDescription; 253 private final InternalValidator mValidator; 254 private final InternalSanitizer[] mSanitizerKeys; 255 private final AutofillId[][] mSanitizerValues; 256 private final AutofillId mTriggerId; 257 258 private SaveInfo(Builder builder) { 259 mType = builder.mType; 260 mNegativeButtonStyle = builder.mNegativeButtonStyle; 261 mNegativeActionListener = builder.mNegativeActionListener; 262 mRequiredIds = builder.mRequiredIds; 263 mOptionalIds = builder.mOptionalIds; 264 mDescription = builder.mDescription; 265 mFlags = builder.mFlags; 266 mCustomDescription = builder.mCustomDescription; 267 mValidator = builder.mValidator; 268 if (builder.mSanitizers == null) { 269 mSanitizerKeys = null; 270 mSanitizerValues = null; 271 } else { 272 final int size = builder.mSanitizers.size(); 273 mSanitizerKeys = new InternalSanitizer[size]; 274 mSanitizerValues = new AutofillId[size][]; 275 for (int i = 0; i < size; i++) { 276 mSanitizerKeys[i] = builder.mSanitizers.keyAt(i); 277 mSanitizerValues[i] = builder.mSanitizers.valueAt(i); 278 } 279 } 280 mTriggerId = builder.mTriggerId; 281 } 282 283 /** @hide */ 284 public @NegativeButtonStyle int getNegativeActionStyle() { 285 return mNegativeButtonStyle; 286 } 287 288 /** @hide */ 289 public @Nullable IntentSender getNegativeActionListener() { 290 return mNegativeActionListener; 291 } 292 293 /** @hide */ 294 public @Nullable AutofillId[] getRequiredIds() { 295 return mRequiredIds; 296 } 297 298 /** @hide */ 299 public @Nullable AutofillId[] getOptionalIds() { 300 return mOptionalIds; 301 } 302 303 /** @hide */ 304 public @SaveDataType int getType() { 305 return mType; 306 } 307 308 /** @hide */ 309 public @SaveInfoFlags int getFlags() { 310 return mFlags; 311 } 312 313 /** @hide */ 314 public CharSequence getDescription() { 315 return mDescription; 316 } 317 318 /** @hide */ 319 @Nullable 320 public CustomDescription getCustomDescription() { 321 return mCustomDescription; 322 } 323 324 /** @hide */ 325 @Nullable 326 public InternalValidator getValidator() { 327 return mValidator; 328 } 329 330 /** @hide */ 331 @Nullable 332 public InternalSanitizer[] getSanitizerKeys() { 333 return mSanitizerKeys; 334 } 335 336 /** @hide */ 337 @Nullable 338 public AutofillId[][] getSanitizerValues() { 339 return mSanitizerValues; 340 } 341 342 /** @hide */ 343 @Nullable 344 public AutofillId getTriggerId() { 345 return mTriggerId; 346 } 347 348 /** 349 * A builder for {@link SaveInfo} objects. 350 */ 351 public static final class Builder { 352 353 private final @SaveDataType int mType; 354 private @NegativeButtonStyle int mNegativeButtonStyle = NEGATIVE_BUTTON_STYLE_CANCEL; 355 private IntentSender mNegativeActionListener; 356 private final AutofillId[] mRequiredIds; 357 private AutofillId[] mOptionalIds; 358 private CharSequence mDescription; 359 private boolean mDestroyed; 360 private int mFlags; 361 private CustomDescription mCustomDescription; 362 private InternalValidator mValidator; 363 private ArrayMap<InternalSanitizer, AutofillId[]> mSanitizers; 364 // Set used to validate against duplicate ids. 365 private ArraySet<AutofillId> mSanitizerIds; 366 private AutofillId mTriggerId; 367 368 /** 369 * Creates a new builder. 370 * 371 * @param type the type of information the associated {@link FillResponse} represents. It 372 * can be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, 373 * {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD}, 374 * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD}, 375 * {@link SaveInfo#SAVE_DATA_TYPE_USERNAME}, or 376 * {@link SaveInfo#SAVE_DATA_TYPE_EMAIL_ADDRESS}. 377 * @param requiredIds ids of all required views that will trigger a save request. 378 * 379 * <p>See {@link SaveInfo} for more info. 380 * 381 * @throws IllegalArgumentException if {@code requiredIds} is {@code null} or empty, or if 382 * it contains any {@code null} entry. 383 */ 384 public Builder(@SaveDataType int type, @NonNull AutofillId[] requiredIds) { 385 mType = type; 386 mRequiredIds = assertValid(requiredIds); 387 } 388 389 /** 390 * Creates a new builder when no id is required. 391 * 392 * <p>When using this builder, caller must call {@link #setOptionalIds(AutofillId[])} before 393 * calling {@link #build()}. 394 * 395 * @param type the type of information the associated {@link FillResponse} represents. It 396 * can be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, 397 * {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD}, 398 * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD}, 399 * {@link SaveInfo#SAVE_DATA_TYPE_USERNAME}, or 400 * {@link SaveInfo#SAVE_DATA_TYPE_EMAIL_ADDRESS}. 401 * 402 * <p>See {@link SaveInfo} for more info. 403 */ 404 public Builder(@SaveDataType int type) { 405 mType = type; 406 mRequiredIds = null; 407 } 408 409 /** 410 * Sets flags changing the save behavior. 411 * 412 * @param flags {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE}, 413 * {@link #FLAG_DONT_SAVE_ON_FINISH}, or {@code 0}. 414 * @return This builder. 415 */ 416 public @NonNull Builder setFlags(@SaveInfoFlags int flags) { 417 throwIfDestroyed(); 418 419 mFlags = Preconditions.checkFlagsArgument(flags, 420 FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE | FLAG_DONT_SAVE_ON_FINISH); 421 return this; 422 } 423 424 /** 425 * Sets the ids of additional, optional views the service would be interested to save. 426 * 427 * <p>See {@link SaveInfo} for more info. 428 * 429 * @param ids The ids of the optional views. 430 * @return This builder. 431 * 432 * @throws IllegalArgumentException if {@code ids} is {@code null} or empty, or if 433 * it contains any {@code null} entry. 434 */ 435 public @NonNull Builder setOptionalIds(@NonNull AutofillId[] ids) { 436 throwIfDestroyed(); 437 mOptionalIds = assertValid(ids); 438 return this; 439 } 440 441 /** 442 * Sets an optional description to be shown in the UI when the user is asked to save. 443 * 444 * <p>Typically, it describes how the data will be stored by the service, so it can help 445 * users to decide whether they can trust the service to save their data. 446 * 447 * @param description a succint description. 448 * @return This Builder. 449 * 450 * @throws IllegalStateException if this call was made after calling 451 * {@link #setCustomDescription(CustomDescription)}. 452 */ 453 public @NonNull Builder setDescription(@Nullable CharSequence description) { 454 throwIfDestroyed(); 455 Preconditions.checkState(mCustomDescription == null, 456 "Can call setDescription() or setCustomDescription(), but not both"); 457 mDescription = description; 458 return this; 459 } 460 461 /** 462 * Sets a custom description to be shown in the UI when the user is asked to save. 463 * 464 * <p>Typically used when the service must show more info about the object being saved, 465 * like a credit card logo, masked number, and expiration date. 466 * 467 * @param customDescription the custom description. 468 * @return This Builder. 469 * 470 * @throws IllegalStateException if this call was made after calling 471 * {@link #setDescription(CharSequence)}. 472 */ 473 public @NonNull Builder setCustomDescription(@NonNull CustomDescription customDescription) { 474 throwIfDestroyed(); 475 Preconditions.checkState(mDescription == null, 476 "Can call setDescription() or setCustomDescription(), but not both"); 477 mCustomDescription = customDescription; 478 return this; 479 } 480 481 /** 482 * Sets the style and listener for the negative save action. 483 * 484 * <p>This allows an autofill service to customize the style and be 485 * notified when the user selects the negative action in the save 486 * UI. Note that selecting the negative action regardless of its style 487 * and listener being customized would dismiss the save UI and if a 488 * custom listener intent is provided then this intent is 489 * started. The default style is {@link #NEGATIVE_BUTTON_STYLE_CANCEL}</p> 490 * 491 * @param style The action style. 492 * @param listener The action listener. 493 * @return This builder. 494 * 495 * @see #NEGATIVE_BUTTON_STYLE_CANCEL 496 * @see #NEGATIVE_BUTTON_STYLE_REJECT 497 * 498 * @throws IllegalArgumentException If the style is invalid 499 */ 500 public @NonNull Builder setNegativeAction(@NegativeButtonStyle int style, 501 @Nullable IntentSender listener) { 502 throwIfDestroyed(); 503 if (style != NEGATIVE_BUTTON_STYLE_CANCEL 504 && style != NEGATIVE_BUTTON_STYLE_REJECT) { 505 throw new IllegalArgumentException("Invalid style: " + style); 506 } 507 mNegativeButtonStyle = style; 508 mNegativeActionListener = listener; 509 return this; 510 } 511 512 /** 513 * Sets an object used to validate the user input - if the input is not valid, the 514 * autofill save UI is not shown. 515 * 516 * <p>Typically used to validate credit card numbers. Examples: 517 * 518 * <p>Validator for a credit number that must have exactly 16 digits: 519 * 520 * <pre class="prettyprint"> 521 * Validator validator = new RegexValidator(ccNumberId, Pattern.compile(""^\\d{16}$")) 522 * </pre> 523 * 524 * <p>Validator for a credit number that must pass a Luhn checksum and either have 525 * 16 digits, or 15 digits starting with 108: 526 * 527 * <pre class="prettyprint"> 528 * import static android.service.autofill.Validators.and; 529 * import static android.service.autofill.Validators.or; 530 * 531 * Validator validator = 532 * and( 533 * new LuhnChecksumValidator(ccNumberId), 534 * or( 535 * new RegexValidator(ccNumberId, Pattern.compile("^\\d{16}$")), 536 * new RegexValidator(ccNumberId, Pattern.compile("^108\\d{12}$")) 537 * ) 538 * ); 539 * </pre> 540 * 541 * <p><b>Note:</b> the example above is just for illustrative purposes; the same validator 542 * could be created using a single regex for the {@code OR} part: 543 * 544 * <pre class="prettyprint"> 545 * Validator validator = 546 * and( 547 * new LuhnChecksumValidator(ccNumberId), 548 * new RegexValidator(ccNumberId, Pattern.compile(""^(\\d{16}|108\\d{12})$")) 549 * ); 550 * </pre> 551 * 552 * <p>Validator for a credit number contained in just 4 fields and that must have exactly 553 * 4 digits on each field: 554 * 555 * <pre class="prettyprint"> 556 * import static android.service.autofill.Validators.and; 557 * 558 * Validator validator = 559 * and( 560 * new RegexValidator(ccNumberId1, Pattern.compile("^\\d{4}$")), 561 * new RegexValidator(ccNumberId2, Pattern.compile("^\\d{4}$")), 562 * new RegexValidator(ccNumberId3, Pattern.compile("^\\d{4}$")), 563 * new RegexValidator(ccNumberId4, Pattern.compile("^\\d{4}$")) 564 * ); 565 * </pre> 566 * 567 * @param validator an implementation provided by the Android System. 568 * @return this builder. 569 * 570 * @throws IllegalArgumentException if {@code validator} is not a class provided 571 * by the Android System. 572 */ 573 public @NonNull Builder setValidator(@NonNull Validator validator) { 574 throwIfDestroyed(); 575 Preconditions.checkArgument((validator instanceof InternalValidator), 576 "not provided by Android System: " + validator); 577 mValidator = (InternalValidator) validator; 578 return this; 579 } 580 581 /** 582 * Adds a sanitizer for one or more field. 583 * 584 * <p>When a sanitizer is set for a field, the {@link AutofillValue} is sent to the 585 * sanitizer before a save request is <a href="#TriggeringSaveRequest">triggered</a>. 586 * 587 * <p>Typically used to avoid displaying the save UI for values that are autofilled but 588 * reformattedby the app. For example, to remove spaces between every 4 digits of a 589 * credit card number: 590 * 591 * <pre class="prettyprint"> 592 * builder.addSanitizer(new TextValueSanitizer( 593 * Pattern.compile("^(\\d{4})\\s?(\\d{4})\\s?(\\d{4})\\s?(\\d{4})$", "$1$2$3$4")), 594 * ccNumberId); 595 * </pre> 596 * 597 * <p>The same sanitizer can be reused to sanitize multiple fields. For example, to trim 598 * both the username and password fields: 599 * 600 * <pre class="prettyprint"> 601 * builder.addSanitizer( 602 * new TextValueSanitizer(Pattern.compile("^\\s*(.*)\\s*$"), "$1"), 603 * usernameId, passwordId); 604 * </pre> 605 * 606 * <p>The sanitizer can also be used as an alternative for a 607 * {@link #setValidator(Validator) validator}. If any of the {@code ids} is a 608 * {@link #SaveInfo.Builder(int, AutofillId[]) required id} and the {@code sanitizer} fails 609 * because of it, then the save UI is not shown. 610 * 611 * @param sanitizer an implementation provided by the Android System. 612 * @param ids id of fields whose value will be sanitized. 613 * @return this builder. 614 * 615 * @throws IllegalArgumentException if a sanitizer for any of the {@code ids} has already 616 * been added or if {@code ids} is empty. 617 */ 618 public @NonNull Builder addSanitizer(@NonNull Sanitizer sanitizer, 619 @NonNull AutofillId... ids) { 620 throwIfDestroyed(); 621 Preconditions.checkArgument(!ArrayUtils.isEmpty(ids), "ids cannot be empty or null"); 622 Preconditions.checkArgument((sanitizer instanceof InternalSanitizer), 623 "not provided by Android System: " + sanitizer); 624 625 if (mSanitizers == null) { 626 mSanitizers = new ArrayMap<>(); 627 mSanitizerIds = new ArraySet<>(ids.length); 628 } 629 630 // Check for duplicates first. 631 for (AutofillId id : ids) { 632 Preconditions.checkArgument(!mSanitizerIds.contains(id), "already added %s", id); 633 mSanitizerIds.add(id); 634 } 635 636 mSanitizers.put((InternalSanitizer) sanitizer, ids); 637 638 return this; 639 } 640 641 /** 642 * Explicitly defines the view that should commit the autofill context when clicked. 643 * 644 * <p>Usually, the save request is only automatically 645 * <a href="#TriggeringSaveRequest">triggered</a> after the activity is 646 * finished or all relevant views become invisible, but there are scenarios where the 647 * autofill context is automatically commited too late 648 * —for example, when the activity manually clears the autofillable views when a 649 * button is tapped. This method can be used to trigger the autofill save UI earlier in 650 * these scenarios. 651 * 652 * <p><b>Note:</b> This method should only be used in scenarios where the automatic workflow 653 * is not enough, otherwise it could trigger the autofill save UI when it should not— 654 * for example, when the user entered invalid credentials for the autofillable views. 655 */ 656 public @NonNull Builder setTriggerId(@NonNull AutofillId id) { 657 throwIfDestroyed(); 658 mTriggerId = Preconditions.checkNotNull(id); 659 return this; 660 } 661 662 /** 663 * Builds a new {@link SaveInfo} instance. 664 * 665 * @throws IllegalStateException if no 666 * {@link #SaveInfo.Builder(int, AutofillId[]) required ids} 667 * or {@link #setOptionalIds(AutofillId[]) optional ids} were set 668 */ 669 public SaveInfo build() { 670 throwIfDestroyed(); 671 Preconditions.checkState( 672 !ArrayUtils.isEmpty(mRequiredIds) || !ArrayUtils.isEmpty(mOptionalIds), 673 "must have at least one required or optional id"); 674 mDestroyed = true; 675 return new SaveInfo(this); 676 } 677 678 private void throwIfDestroyed() { 679 if (mDestroyed) { 680 throw new IllegalStateException("Already called #build()"); 681 } 682 } 683 } 684 685 ///////////////////////////////////// 686 // Object "contract" methods. // 687 ///////////////////////////////////// 688 @Override 689 public String toString() { 690 if (!sDebug) return super.toString(); 691 692 final StringBuilder builder = new StringBuilder("SaveInfo: [type=") 693 .append(DebugUtils.flagsToString(SaveInfo.class, "SAVE_DATA_TYPE_", mType)) 694 .append(", requiredIds=").append(Arrays.toString(mRequiredIds)) 695 .append(", style=").append(DebugUtils.flagsToString(SaveInfo.class, 696 "NEGATIVE_BUTTON_STYLE_", mNegativeButtonStyle)); 697 if (mOptionalIds != null) { 698 builder.append(", optionalIds=").append(Arrays.toString(mOptionalIds)); 699 } 700 if (mDescription != null) { 701 builder.append(", description=").append(mDescription); 702 } 703 if (mFlags != 0) { 704 builder.append(", flags=").append(mFlags); 705 } 706 if (mCustomDescription != null) { 707 builder.append(", customDescription=").append(mCustomDescription); 708 } 709 if (mValidator != null) { 710 builder.append(", validator=").append(mValidator); 711 } 712 if (mSanitizerKeys != null) { 713 builder.append(", sanitizerKeys=").append(mSanitizerKeys.length); 714 } 715 if (mSanitizerValues != null) { 716 builder.append(", sanitizerValues=").append(mSanitizerValues.length); 717 } 718 if (mTriggerId != null) { 719 builder.append(", triggerId=").append(mTriggerId); 720 } 721 722 return builder.append("]").toString(); 723 } 724 725 ///////////////////////////////////// 726 // Parcelable "contract" methods. // 727 ///////////////////////////////////// 728 729 @Override 730 public int describeContents() { 731 return 0; 732 } 733 734 @Override 735 public void writeToParcel(Parcel parcel, int flags) { 736 parcel.writeInt(mType); 737 parcel.writeParcelableArray(mRequiredIds, flags); 738 parcel.writeParcelableArray(mOptionalIds, flags); 739 parcel.writeInt(mNegativeButtonStyle); 740 parcel.writeParcelable(mNegativeActionListener, flags); 741 parcel.writeCharSequence(mDescription); 742 parcel.writeParcelable(mCustomDescription, flags); 743 parcel.writeParcelable(mValidator, flags); 744 parcel.writeParcelableArray(mSanitizerKeys, flags); 745 if (mSanitizerKeys != null) { 746 for (int i = 0; i < mSanitizerValues.length; i++) { 747 parcel.writeParcelableArray(mSanitizerValues[i], flags); 748 } 749 } 750 parcel.writeParcelable(mTriggerId, flags); 751 parcel.writeInt(mFlags); 752 } 753 754 public static final Parcelable.Creator<SaveInfo> CREATOR = new Parcelable.Creator<SaveInfo>() { 755 @Override 756 public SaveInfo createFromParcel(Parcel parcel) { 757 758 // Always go through the builder to ensure the data ingested by 759 // the system obeys the contract of the builder to avoid attacks 760 // using specially crafted parcels. 761 final int type = parcel.readInt(); 762 final AutofillId[] requiredIds = parcel.readParcelableArray(null, AutofillId.class); 763 final Builder builder = requiredIds != null 764 ? new Builder(type, requiredIds) 765 : new Builder(type); 766 final AutofillId[] optionalIds = parcel.readParcelableArray(null, AutofillId.class); 767 if (optionalIds != null) { 768 builder.setOptionalIds(optionalIds); 769 } 770 771 builder.setNegativeAction(parcel.readInt(), parcel.readParcelable(null)); 772 builder.setDescription(parcel.readCharSequence()); 773 final CustomDescription customDescripton = parcel.readParcelable(null); 774 if (customDescripton != null) { 775 builder.setCustomDescription(customDescripton); 776 } 777 final InternalValidator validator = parcel.readParcelable(null); 778 if (validator != null) { 779 builder.setValidator(validator); 780 } 781 final InternalSanitizer[] sanitizers = 782 parcel.readParcelableArray(null, InternalSanitizer.class); 783 if (sanitizers != null) { 784 final int size = sanitizers.length; 785 for (int i = 0; i < size; i++) { 786 final AutofillId[] autofillIds = 787 parcel.readParcelableArray(null, AutofillId.class); 788 builder.addSanitizer(sanitizers[i], autofillIds); 789 } 790 } 791 final AutofillId triggerId = parcel.readParcelable(null); 792 if (triggerId != null) { 793 builder.setTriggerId(triggerId); 794 } 795 builder.setFlags(parcel.readInt()); 796 return builder.build(); 797 } 798 799 @Override 800 public SaveInfo[] newArray(int size) { 801 return new SaveInfo[size]; 802 } 803 }; 804 } 805