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         * &mdash;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&mdash;
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