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