FillResponse.java revision 601d22007488145f8651930d23aeb7a7a95cc591
1/*
2 * Copyright (C) 2016 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.service.autofill.FillRequest.INVALID_REQUEST_ID;
20import static android.view.autofill.Helper.sDebug;
21
22import android.annotation.IntDef;
23import android.annotation.NonNull;
24import android.annotation.Nullable;
25import android.annotation.TestApi;
26import android.app.Activity;
27import android.content.IntentSender;
28import android.content.pm.ParceledListSlice;
29import android.os.Bundle;
30import android.os.Parcel;
31import android.os.Parcelable;
32import android.view.autofill.AutofillId;
33import android.widget.RemoteViews;
34
35import com.android.internal.util.Preconditions;
36
37import java.lang.annotation.Retention;
38import java.lang.annotation.RetentionPolicy;
39import java.util.ArrayList;
40import java.util.Arrays;
41import java.util.List;
42
43/**
44 * Response for a {@link
45 * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}.
46 *
47 * <p>See the main {@link AutofillService} documentation for more details and examples.
48 */
49public final class FillResponse implements Parcelable {
50
51    /**
52     * Must be set in the last response to generate
53     * {@link FillEventHistory.Event#TYPE_CONTEXT_COMMITTED} events.
54     */
55    public static final int FLAG_TRACK_CONTEXT_COMMITED = 0x1;
56
57    /**
58     * Used in conjunction to {@link FillResponse.Builder#disableAutofill(long)} to disable autofill
59     * only for the activiy associated with the {@link FillResponse}, instead of the whole app.
60     */
61    public static final int FLAG_DISABLE_ACTIVITY_ONLY = 0x2;
62
63    /** @hide */
64    @IntDef(flag = true, value = {
65            FLAG_TRACK_CONTEXT_COMMITED,
66            FLAG_DISABLE_ACTIVITY_ONLY
67    })
68    @Retention(RetentionPolicy.SOURCE)
69    @interface FillResponseFlags {}
70
71    private final @Nullable ParceledListSlice<Dataset> mDatasets;
72    private final @Nullable SaveInfo mSaveInfo;
73    private final @Nullable Bundle mClientState;
74    private final @Nullable RemoteViews mPresentation;
75    private final @Nullable IntentSender mAuthentication;
76    private final @Nullable AutofillId[] mAuthenticationIds;
77    private final @Nullable AutofillId[] mIgnoredIds;
78    private final long mDisableDuration;
79    private final @Nullable FieldsDetection mFieldsDetection;
80    private final int mFlags;
81    private int mRequestId;
82
83    private FillResponse(@NonNull Builder builder) {
84        mDatasets = (builder.mDatasets != null) ? new ParceledListSlice<>(builder.mDatasets) : null;
85        mSaveInfo = builder.mSaveInfo;
86        mClientState = builder.mClientState;
87        mPresentation = builder.mPresentation;
88        mAuthentication = builder.mAuthentication;
89        mAuthenticationIds = builder.mAuthenticationIds;
90        mIgnoredIds = builder.mIgnoredIds;
91        mDisableDuration = builder.mDisableDuration;
92        mFieldsDetection = builder.mFieldsDetection;
93        mFlags = builder.mFlags;
94        mRequestId = INVALID_REQUEST_ID;
95    }
96
97    /** @hide */
98    public @Nullable Bundle getClientState() {
99        return mClientState;
100    }
101
102    /** @hide */
103    public @Nullable List<Dataset> getDatasets() {
104        return (mDatasets != null) ? mDatasets.getList() : null;
105    }
106
107    /** @hide */
108    public @Nullable SaveInfo getSaveInfo() {
109        return mSaveInfo;
110    }
111
112    /** @hide */
113    public @Nullable RemoteViews getPresentation() {
114        return mPresentation;
115    }
116
117    /** @hide */
118    public @Nullable IntentSender getAuthentication() {
119        return mAuthentication;
120    }
121
122    /** @hide */
123    public @Nullable AutofillId[] getAuthenticationIds() {
124        return mAuthenticationIds;
125    }
126
127    /** @hide */
128    public @Nullable AutofillId[] getIgnoredIds() {
129        return mIgnoredIds;
130    }
131
132    /** @hide */
133    public long getDisableDuration() {
134        return mDisableDuration;
135    }
136
137    /** @hide */
138    public @Nullable FieldsDetection getFieldsDetection() {
139        return mFieldsDetection;
140    }
141
142    /** @hide */
143    public int getFlags() {
144        return mFlags;
145    }
146
147    /**
148     * Associates a {@link FillResponse} to a request.
149     *
150     * <p>Set inside of the {@link FillCallback} code, not the {@link AutofillService}.
151     *
152     * @param requestId The id of the request to associate the response to.
153     *
154     * @hide
155     */
156    public void setRequestId(int requestId) {
157        mRequestId = requestId;
158    }
159
160    /** @hide */
161    public int getRequestId() {
162        return mRequestId;
163    }
164
165    /**
166     * Builder for {@link FillResponse} objects. You must to provide at least
167     * one dataset or set an authentication intent with a presentation view.
168     */
169    public static final class Builder {
170        private ArrayList<Dataset> mDatasets;
171        private SaveInfo mSaveInfo;
172        private Bundle mClientState;
173        private RemoteViews mPresentation;
174        private IntentSender mAuthentication;
175        private AutofillId[] mAuthenticationIds;
176        private AutofillId[] mIgnoredIds;
177        private long mDisableDuration;
178        private FieldsDetection mFieldsDetection;
179        private int mFlags;
180        private boolean mDestroyed;
181
182        /**
183         * Triggers a custom UI before before autofilling the screen with any data set in this
184         * response.
185         *
186         * <p><b>Note:</b> Although the name of this method suggests that it should be used just for
187         * authentication flow, it can be used for other advanced flows; see {@link AutofillService}
188         * for examples.
189         *
190         * <p>This is typically useful when a user interaction is required to unlock their
191         * data vault if you encrypt the data set labels and data set data. It is recommended
192         * to encrypt only the sensitive data and not the data set labels which would allow
193         * auth on the data set level leading to a better user experience. Note that if you
194         * use sensitive data as a label, for example an email address, then it should also
195         * be encrypted. The provided {@link android.app.PendingIntent intent} must be an
196         * {@link Activity} which implements your authentication flow. Also if you provide an auth
197         * intent you also need to specify the presentation view to be shown in the fill UI
198         * for the user to trigger your authentication flow.
199         *
200         * <p>When a user triggers autofill, the system launches the provided intent
201         * whose extras will have the
202         * {@link android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen
203         * content} and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE
204         * client state}. Once you complete your authentication flow you should set the
205         * {@link Activity} result to {@link android.app.Activity#RESULT_OK} and set the
206         * {@link android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra
207         * with the fully populated {@link FillResponse response} (or {@code null} if the screen
208         * cannot be autofilled).
209         *
210         * <p>For example, if you provided an empty {@link FillResponse response} because the
211         * user's data was locked and marked that the response needs an authentication then
212         * in the response returned if authentication succeeds you need to provide all
213         * available data sets some of which may need to be further authenticated, for
214         * example a credit card whose CVV needs to be entered.
215         *
216         * <p>If you provide an authentication intent you must also provide a presentation
217         * which is used to visualize visualize the response for triggering the authentication
218         * flow.
219         *
220         * <p><b>Note:</b> Do not make the provided pending intent
221         * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the
222         * platform needs to fill in the authentication arguments.
223         *
224         * @param authentication Intent to an activity with your authentication flow.
225         * @param presentation The presentation to visualize the response.
226         * @param ids id of Views that when focused will display the authentication UI.
227         *
228         * @return This builder.
229         * @throws IllegalArgumentException if {@code ids} is {@code null} or empty, or if
230         * both {@code authentication} and {@code presentation} are {@code null}, or if
231         * both {@code authentication} and {@code presentation} are non-{@code null}
232         *
233         * @see android.app.PendingIntent#getIntentSender()
234         */
235        public @NonNull Builder setAuthentication(@NonNull AutofillId[] ids,
236                @Nullable IntentSender authentication, @Nullable RemoteViews presentation) {
237            throwIfDestroyed();
238            throwIfDisableAutofillCalled();
239            if (ids == null || ids.length == 0) {
240                throw new IllegalArgumentException("ids cannot be null or empry");
241            }
242            if (authentication == null ^ presentation == null) {
243                throw new IllegalArgumentException("authentication and presentation"
244                        + " must be both non-null or null");
245            }
246            mAuthentication = authentication;
247            mPresentation = presentation;
248            mAuthenticationIds = ids;
249            return this;
250        }
251
252        /**
253         * Specifies views that should not trigger new
254         * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
255         * FillCallback)} requests.
256         *
257         * <p>This is typically used when the service cannot autofill the view; for example, a
258         * text field representing the result of a Captcha challenge.
259         */
260        public Builder setIgnoredIds(AutofillId...ids) {
261            throwIfDestroyed();
262            mIgnoredIds = ids;
263            return this;
264        }
265
266        /**
267         * Adds a new {@link Dataset} to this response.
268         *
269         * <p><b>Note: </b> on Android {@link android.os.Build.VERSION_CODES#O}, the total number of
270         * datasets is limited by the Binder transaction size, so it's recommended to keep it
271         * small (in the range of 10-20 at most) and use pagination by adding a fake
272         * {@link Dataset.Builder#setAuthentication(IntentSender) authenticated dataset} at the end
273         * with a presentation string like "Next 10" that would return a new {@link FillResponse}
274         * with the next 10 datasets, and so on. This limitation was lifted on
275         * Android {@link android.os.Build.VERSION_CODES#O_MR1}, although the Binder transaction
276         * size can still be reached if each dataset itself is too big.
277         *
278         * @return This builder.
279         */
280        public @NonNull Builder addDataset(@Nullable Dataset dataset) {
281            throwIfDestroyed();
282            throwIfDisableAutofillCalled();
283            if (dataset == null) {
284                return this;
285            }
286            if (mDatasets == null) {
287                mDatasets = new ArrayList<>();
288            }
289            if (!mDatasets.add(dataset)) {
290                return this;
291            }
292            return this;
293        }
294
295        /**
296         * Sets the {@link SaveInfo} associated with this response.
297         *
298         * @return This builder.
299         */
300        public @NonNull Builder setSaveInfo(@NonNull SaveInfo saveInfo) {
301            throwIfDestroyed();
302            throwIfDisableAutofillCalled();
303            mSaveInfo = saveInfo;
304            return this;
305        }
306
307        /**
308         * Sets a {@link Bundle state} that will be passed to subsequent APIs that
309         * manipulate this response. For example, they are passed to subsequent
310         * calls to {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
311         * FillCallback)} and {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}.
312         * You can use this to store intermediate state that is persistent across multiple
313         * fill requests and the subsequent save request.
314         *
315         * <p>If this method is called on multiple {@link FillResponse} objects for the same
316         * screen, just the latest bundle is passed back to the service.
317         *
318         * <p>Once a {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)
319         * save request} is made the client state is cleared.
320         *
321         * @param clientState The custom client state.
322         * @return This builder.
323         */
324        public Builder setClientState(@Nullable Bundle clientState) {
325            throwIfDestroyed();
326            mClientState = clientState;
327            return this;
328        }
329
330        /**
331         * TODO(b/67867469):
332         *  - javadoc it
333         *  - javadoc how to check results
334         *  - unhide
335         *  - unhide / remove testApi
336         *  - throw exception (and document) if response has datasets or saveinfo
337         *  - throw exception (and document) if id on fieldsDetection is ignored
338         *
339         * @hide
340         */
341        @TestApi
342        public Builder setFieldsDetection(@NonNull FieldsDetection fieldsDetection) {
343            throwIfDestroyed();
344            throwIfDisableAutofillCalled();
345            mFieldsDetection = Preconditions.checkNotNull(fieldsDetection);
346            return this;
347        }
348
349        /**
350         * Sets flags changing the response behavior.
351         *
352         * @param flags a combination of {@link #FLAG_TRACK_CONTEXT_COMMITED} and
353         * {@link #FLAG_DISABLE_ACTIVITY_ONLY}, or {@code 0}.
354         *
355         * @return This builder.
356         */
357        public Builder setFlags(@FillResponseFlags int flags) {
358            throwIfDestroyed();
359            mFlags = Preconditions.checkFlagsArgument(flags,
360                    FLAG_TRACK_CONTEXT_COMMITED | FLAG_DISABLE_ACTIVITY_ONLY);
361            return this;
362        }
363
364        /**
365         * Disables autofill for the app or activity.
366         *
367         * <p>This method is useful to optimize performance in cases where the service knows it
368         * can not autofill an app&mdash;for example, when the service has a list of "blacklisted"
369         * apps such as office suites.
370         *
371         * <p>By default, it disables autofill for all activities in the app, unless the response is
372         * {@link #setFlags(int) flagged} with {@link #FLAG_DISABLE_ACTIVITY_ONLY}.
373         *
374         * <p>Autofill for the app or activity is automatically re-enabled after any of the
375         * following conditions:
376         *
377         * <ol>
378         *   <li>{@code duration} milliseconds have passed.
379         *   <li>The autofill service for the user has changed.
380         *   <li>The device has rebooted.
381         * </ol>
382         *
383         * <p><b>Note:</b> Activities that are running when autofill is re-enabled remain
384         * disabled for autofill until they finish and restart.
385         *
386         * @param duration duration to disable autofill, in milliseconds.
387         *
388         * @return this builder
389         *
390         * @throws IllegalArgumentException if {@code duration} is not a positive number.
391         * @throws IllegalStateException if either {@link #addDataset(Dataset)},
392         *       {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, or
393         *       {@link #setSaveInfo(SaveInfo)} was already called.
394         */
395        public Builder disableAutofill(long duration) {
396            throwIfDestroyed();
397            if (duration <= 0) {
398                throw new IllegalArgumentException("duration must be greater than 0");
399            }
400            if (mAuthentication != null || mDatasets != null || mSaveInfo != null
401                    || mFieldsDetection != null) {
402                throw new IllegalStateException("disableAutofill() must be the only method called");
403            }
404
405            mDisableDuration = duration;
406            return this;
407        }
408
409        /**
410         * Builds a new {@link FillResponse} instance.
411         *
412         * @throws IllegalStateException if any of the following conditions occur:
413         * <ol>
414         *   <li>{@link #build()} was already called.
415         *   <li>No call was made to {@link #addDataset(Dataset)},
416         *       {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)},
417         *       {@link #setSaveInfo(SaveInfo)}, or {@link #disableAutofill(long)}.
418         * </ol>
419         *
420         * @return A built response.
421         */
422        public FillResponse build() {
423            throwIfDestroyed();
424            if (mAuthentication == null && mDatasets == null && mSaveInfo == null
425                    && mDisableDuration == 0 && mFieldsDetection == null) {
426                throw new IllegalStateException("need to provide: at least one DataSet, or a "
427                        + "SaveInfo, or an authentication with a presentation, "
428                        + "or a FieldsDetection, or disable autofill");
429            }
430            mDestroyed = true;
431            return new FillResponse(this);
432        }
433
434        private void throwIfDestroyed() {
435            if (mDestroyed) {
436                throw new IllegalStateException("Already called #build()");
437            }
438        }
439
440        private void throwIfDisableAutofillCalled() {
441            if (mDisableDuration > 0) {
442                throw new IllegalStateException("Already called #disableAutofill()");
443            }
444        }
445    }
446
447    /////////////////////////////////////
448    // Object "contract" methods. //
449    /////////////////////////////////////
450    @Override
451    public String toString() {
452        if (!sDebug) return super.toString();
453
454        // TODO: create a dump() method instead
455        return new StringBuilder(
456                "FillResponse : [mRequestId=" + mRequestId)
457                .append(", datasets=").append(mDatasets == null ? "N/A" : mDatasets.getList())
458                .append(", saveInfo=").append(mSaveInfo)
459                .append(", clientState=").append(mClientState != null)
460                .append(", hasPresentation=").append(mPresentation != null)
461                .append(", hasAuthentication=").append(mAuthentication != null)
462                .append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds))
463                .append(", ignoredIds=").append(Arrays.toString(mIgnoredIds))
464                .append(", disableDuration=").append(mDisableDuration)
465                .append(", flags=").append(mFlags)
466                .append(", fieldDetection=").append(mFieldsDetection)
467                .append("]")
468                .toString();
469    }
470
471    /////////////////////////////////////
472    // Parcelable "contract" methods. //
473    /////////////////////////////////////
474
475    @Override
476    public int describeContents() {
477        return 0;
478    }
479
480    @Override
481    public void writeToParcel(Parcel parcel, int flags) {
482        parcel.writeParcelable(mDatasets, flags);
483        parcel.writeParcelable(mSaveInfo, flags);
484        parcel.writeParcelable(mClientState, flags);
485        parcel.writeParcelableArray(mAuthenticationIds, flags);
486        parcel.writeParcelable(mAuthentication, flags);
487        parcel.writeParcelable(mPresentation, flags);
488        parcel.writeParcelableArray(mIgnoredIds, flags);
489        parcel.writeLong(mDisableDuration);
490        parcel.writeParcelable(mFieldsDetection, flags);
491        parcel.writeInt(mFlags);
492        parcel.writeInt(mRequestId);
493    }
494
495    public static final Parcelable.Creator<FillResponse> CREATOR =
496            new Parcelable.Creator<FillResponse>() {
497        @Override
498        public FillResponse createFromParcel(Parcel parcel) {
499            // Always go through the builder to ensure the data ingested by
500            // the system obeys the contract of the builder to avoid attacks
501            // using specially crafted parcels.
502            final Builder builder = new Builder();
503            final ParceledListSlice<Dataset> datasetSlice = parcel.readParcelable(null);
504            final List<Dataset> datasets = (datasetSlice != null) ? datasetSlice.getList() : null;
505            final int datasetCount = (datasets != null) ? datasets.size() : 0;
506            for (int i = 0; i < datasetCount; i++) {
507                builder.addDataset(datasets.get(i));
508            }
509            builder.setSaveInfo(parcel.readParcelable(null));
510            builder.setClientState(parcel.readParcelable(null));
511
512            // Sets authentication state.
513            final AutofillId[] authenticationIds = parcel.readParcelableArray(null,
514                    AutofillId.class);
515            final IntentSender authentication = parcel.readParcelable(null);
516            final RemoteViews presentation = parcel.readParcelable(null);
517            if (authenticationIds != null) {
518                builder.setAuthentication(authenticationIds, authentication, presentation);
519            }
520
521            builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class));
522            final long disableDuration = parcel.readLong();
523            if (disableDuration > 0) {
524                builder.disableAutofill(disableDuration);
525            }
526            final FieldsDetection fieldsDetection = parcel.readParcelable(null);
527            if (fieldsDetection != null) {
528                builder.setFieldsDetection(fieldsDetection);
529            }
530            builder.setFlags(parcel.readInt());
531
532            final FillResponse response = builder.build();
533            response.setRequestId(parcel.readInt());
534
535            return response;
536        }
537
538        @Override
539        public FillResponse[] newArray(int size) {
540            return new FillResponse[size];
541        }
542    };
543}
544