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