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