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