FillResponse.java revision a9379d0b44ca1f68a0036d2b65218e17fa348514
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.NonNull;
23import android.annotation.Nullable;
24import android.content.IntentSender;
25import android.os.Bundle;
26import android.os.Parcel;
27import android.os.Parcelable;
28import android.view.autofill.AutofillId;
29import android.view.autofill.AutofillManager;
30import android.widget.RemoteViews;
31
32import java.util.ArrayList;
33import java.util.Arrays;
34
35/**
36 * Response for a {@link
37 * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}.
38 *
39 * <p>The response typically contains one or more {@link Dataset}s, each representing a set of
40 * fields that can be autofilled together, and the Android system displays a dataset picker UI
41 * affordance that the user must use before the {@link android.app.Activity} is filled with
42 * the dataset.
43 *
44 * <p>For example, for a login page with username/password where the user only has one account in
45 * the response could be:
46 *
47 * <pre class="prettyprint">
48 *  new FillResponse.Builder()
49 *      .add(new Dataset.Builder(createPresentation())
50 *          .setValue(id1, AutofillValue.forText("homer"))
51 *          .setValue(id2, AutofillValue.forText("D'OH!"))
52 *          .build())
53 *      .build();
54 * </pre>
55 *
56 * <p>If the user had 2 accounts, each with its own user-provided names, the response could be:
57 *
58 * <pre class="prettyprint">
59 *  new FillResponse.Builder()
60 *      .add(new Dataset.Builder(createFirstPresentation())
61 *          .setValue(id1, AutofillValue.forText("homer"))
62 *          .setValue(id2, AutofillValue.forText("D'OH!"))
63 *          .build())
64 *      .add(new Dataset.Builder(createSecondPresentation())
65 *          .setValue(id1, AutofillValue.forText("elbarto")
66 *          .setValue(id2, AutofillValue.forText("cowabonga")
67 *          .build())
68 *      .build();
69 * </pre>
70 *
71 * If the service is interested on saving the user-edited data back, it must set a {@link SaveInfo}
72 * in the {@link FillResponse}. Typically, the {@link SaveInfo} contains the same ids as the
73 * {@link Dataset}, but other combinations are possible - see {@link SaveInfo} for more details
74 *
75 * <p>If the service has multiple {@link Dataset}s for different sections of the activity,
76 * for example, a user section for which there are two datasets followed by an address
77 * section for which there are two datasets for each user user, then it should "partition"
78 * the activity in sections and populate the response with just a subset of the data that would
79 * fulfill the first section (the name in our example); then once the user fills the first
80 * section and taps a field from the next section (the address in our example), the Android
81 * system would issue another request for that section, and so on. Note that if the user
82 * chooses to populate the first section with a service provided dataset, the subsequent request
83 * would contain the populated values so you don't try to provide suggestions for the first
84 * section but ony for the second one based on the context of what was already filled. For
85 * example, the first response could be:
86 *
87 * <pre class="prettyprint">
88 *  new FillResponse.Builder()
89 *      .add(new Dataset.Builder(createFirstPresentation())
90 *          .setValue(id1, AutofillValue.forText("Homer"))
91 *          .setValue(id2, AutofillValue.forText("Simpson"))
92 *          .build())
93 *      .add(new Dataset.Builder(createSecondPresentation())
94 *          .setValue(id1, AutofillValue.forText("Bart"))
95 *          .setValue(id2, AutofillValue.forText("Simpson"))
96 *          .build())
97 *      .build();
98 * </pre>
99 *
100 * <p>Then after the user picks the second dataset and taps the street field to
101 * trigger another autofill request, the second response could be:
102 *
103 * <pre class="prettyprint">
104 *  new FillResponse.Builder()
105 *      .add(new Dataset.Builder(createThirdPresentation())
106 *          .setValue(id3, AutofillValue.forText("742 Evergreen Terrace"))
107 *          .setValue(id4, AutofillValue.forText("Springfield"))
108 *          .build())
109 *      .add(new Dataset.Builder(createFourthPresentation())
110 *          .setValue(id3, AutofillValue.forText("Springfield Power Plant"))
111 *          .setValue(id4, AutofillValue.forText("Springfield"))
112 *          .build())
113 *      .build();
114 * </pre>
115 *
116 * <p>The service could require user authentication at the {@link FillResponse} or the
117 * {@link Dataset} level, prior to autofilling an activity - see
118 * {@link FillResponse.Builder#setAuthentication(AutofillId[], IntentSender, RemoteViews)} and
119 * {@link Dataset.Builder#setAuthentication(IntentSender)}.
120 *
121 * <p>It is recommended that you encrypt only the sensitive data but leave the labels unencrypted
122 * which would allow you to provide a dataset presentation views with labels and if the user
123 * chooses one of them challenge the user to authenticate. For example, if the user has a
124 * home and a work address the Home and Work labels could be stored unencrypted as they don't
125 * have any sensitive data while the address data is in an encrypted storage. If the user
126 * chooses Home, then the platform will start your authentication flow. If you encrypt all
127 * data and require auth at the response level the user will have to interact with the fill
128 * UI to trigger a request for the datasets (as they don't see the presentation views for the
129 * possible options) which will start your auth flow and after successfully authenticating
130 * the user will be presented with the Home and Work options to pick one. Hence, you have
131 * flexibility how to implement your auth while storing labels non-encrypted and data
132 * encrypted provides a better user experience.
133 */
134public final class FillResponse implements Parcelable {
135
136    private final @Nullable ArrayList<Dataset> mDatasets;
137    private final @Nullable SaveInfo mSaveInfo;
138    private final @Nullable Bundle mClientState;
139    private final @Nullable RemoteViews mPresentation;
140    private final @Nullable IntentSender mAuthentication;
141    private final @Nullable AutofillId[] mAuthenticationIds;
142    private final @Nullable AutofillId[] mIgnoredIds;
143    private int mRequestId;
144
145    private FillResponse(@NonNull Builder builder) {
146        mDatasets = builder.mDatasets;
147        mSaveInfo = builder.mSaveInfo;
148        mClientState = builder.mCLientState;
149        mPresentation = builder.mPresentation;
150        mAuthentication = builder.mAuthentication;
151        mAuthenticationIds = builder.mAuthenticationIds;
152        mIgnoredIds = builder.mIgnoredIds;
153        mRequestId = INVALID_REQUEST_ID;
154    }
155
156    /** @hide */
157    public @Nullable Bundle getClientState() {
158        return mClientState;
159    }
160
161    /** @hide */
162    public @Nullable ArrayList<Dataset> getDatasets() {
163        return mDatasets;
164    }
165
166    /** @hide */
167    public @Nullable SaveInfo getSaveInfo() {
168        return mSaveInfo;
169    }
170
171    /** @hide */
172    public @Nullable RemoteViews getPresentation() {
173        return mPresentation;
174    }
175
176    /** @hide */
177    public @Nullable IntentSender getAuthentication() {
178        return mAuthentication;
179    }
180
181    /** @hide */
182    public @Nullable AutofillId[] getAuthenticationIds() {
183        return mAuthenticationIds;
184    }
185
186    /** @hide */
187    public @Nullable AutofillId[] getIgnoredIds() {
188        return mIgnoredIds;
189    }
190
191    /**
192     * Associates a {@link FillResponse} to a request.
193     *
194     * <p>Set inside of the {@link FillCallback} code, not the {@link AutofillService}.
195     *
196     * @param requestId The id of the request to associate the response to.
197     *
198     * @hide
199     */
200    public void setRequestId(int requestId) {
201        mRequestId = requestId;
202    }
203
204    /** @hide */
205    public int getRequestId() {
206        return mRequestId;
207    }
208
209    /**
210     * Builder for {@link FillResponse} objects. You must to provide at least
211     * one dataset or set an authentication intent with a presentation view.
212     */
213    public static final class Builder {
214        private ArrayList<Dataset> mDatasets;
215        private SaveInfo mSaveInfo;
216        private Bundle mCLientState;
217        private RemoteViews mPresentation;
218        private IntentSender mAuthentication;
219        private AutofillId[] mAuthenticationIds;
220        private AutofillId[] mIgnoredIds;
221        private boolean mDestroyed;
222
223        /**
224         * Requires a fill response authentication before autofilling the activity with
225         * any data set in this response.
226         *
227         * <p>This is typically useful when a user interaction is required to unlock their
228         * data vault if you encrypt the data set labels and data set data. It is recommended
229         * to encrypt only the sensitive data and not the data set labels which would allow
230         * auth on the data set level leading to a better user experience. Note that if you
231         * use sensitive data as a label, for example an email address, then it should also
232         * be encrypted. The provided {@link android.app.PendingIntent intent} must be an
233         * activity which implements your authentication flow. Also if you provide an auth
234         * intent you also need to specify the presentation view to be shown in the fill UI
235         * for the user to trigger your authentication flow.
236         *
237         * <p>When a user triggers autofill, the system launches the provided intent
238         * whose extras will have the {@link AutofillManager#EXTRA_ASSIST_STRUCTURE screen
239         * content} and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE
240         * client state}. Once you complete your authentication flow you should set the activity
241         * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated
242         * {@link FillResponse response} by setting it to the {@link
243         * AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra.
244         * For example, if you provided an empty {@link FillResponse resppnse} because the
245         * user's data was locked and marked that the response needs an authentication then
246         * in the response returned if authentication succeeds you need to provide all
247         * available data sets some of which may need to be further authenticated, for
248         * example a credit card whose CVV needs to be entered.
249         *
250         * <p>If you provide an authentication intent you must also provide a presentation
251         * which is used to visualize visualize the response for triggering the authentication
252         * flow.
253         *
254         * <p></><strong>Note:</strong> Do not make the provided pending intent
255         * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the
256         * platform needs to fill in the authentication arguments.
257         *
258         * @param authentication Intent to an activity with your authentication flow.
259         * @param presentation The presentation to visualize the response.
260         * @param ids id of Views that when focused will display the authentication UI affordance.
261         *
262         * @return This builder.
263         * @throw {@link IllegalArgumentException} if {@code ids} is {@code null} or empty, or if
264         * neither {@code authentication} nor {@code presentation} is non-{@code null}.
265         *
266         * @see android.app.PendingIntent#getIntentSender()
267         */
268        public @NonNull Builder setAuthentication(@NonNull AutofillId[] ids,
269                @Nullable IntentSender authentication, @Nullable RemoteViews presentation) {
270            throwIfDestroyed();
271            if (ids == null || ids.length == 0) {
272                throw new IllegalArgumentException("ids cannot be null or empry");
273            }
274            if (authentication == null ^ presentation == null) {
275                throw new IllegalArgumentException("authentication and presentation"
276                        + " must be both non-null or null");
277            }
278            mAuthentication = authentication;
279            mPresentation = presentation;
280            mAuthenticationIds = ids;
281            return this;
282        }
283
284        /**
285         * Specifies views that should not trigger new
286         * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
287         * FillCallback)} requests.
288         *
289         * <p>This is typically used when the service cannot autofill the view; for example, an
290         * {@code EditText} representing a captcha.
291         */
292        public Builder setIgnoredIds(AutofillId...ids) {
293            mIgnoredIds = ids;
294            return this;
295        }
296
297        /**
298         * Adds a new {@link Dataset} to this response.
299         *
300         * @return This builder.
301         */
302        public @NonNull Builder addDataset(@Nullable Dataset dataset) {
303            throwIfDestroyed();
304            if (dataset == null) {
305                return this;
306            }
307            if (mDatasets == null) {
308                mDatasets = new ArrayList<>();
309            }
310            if (!mDatasets.add(dataset)) {
311                return this;
312            }
313            return this;
314        }
315
316        /**
317         * Sets the {@link SaveInfo} associated with this response.
318         *
319         * <p>See {@link FillResponse} for more info.
320         *
321         * @return This builder.
322         */
323        public @NonNull Builder setSaveInfo(@NonNull SaveInfo saveInfo) {
324            throwIfDestroyed();
325            mSaveInfo = saveInfo;
326            return this;
327        }
328
329        /**
330         * @deprecated Use {@link #setClientState(Bundle)} instead.
331         * @hide
332         */
333        @Deprecated
334        public Builder setExtras(@Nullable Bundle extras) {
335            throwIfDestroyed();
336            mCLientState = extras;
337            return this;
338        }
339
340        /**
341         * Sets a {@link Bundle state} that will be passed to subsequent APIs that
342         * manipulate this response. For example, they are passed to subsequent
343         * calls to {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
344         * FillCallback)} and {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}.
345         * You can use this to store intermediate state that is persistent across multiple
346         * fill requests and the subsequent save request.
347         *
348         * <p>If this method is called on multiple {@link FillResponse} objects for the same
349         * activity, just the latest bundle is passed back to the service.
350         *
351         * <p>Once a {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)
352         * save request} is made the client state is cleared.
353         *
354         * @param clientState The custom client state.
355         * @return This builder.
356         */
357        public Builder setClientState(@Nullable Bundle clientState) {
358            throwIfDestroyed();
359            mCLientState = clientState;
360            return this;
361        }
362
363        /**
364         * Builds a new {@link FillResponse} instance. You must provide at least
365         * one dataset or some savable ids or an authentication with a presentation
366         * view.
367         *
368         * @return A built response.
369         */
370        public FillResponse build() {
371            throwIfDestroyed();
372
373            if (mAuthentication == null && mDatasets == null && mSaveInfo == null) {
374                throw new IllegalArgumentException("need to provide at least one DataSet or a "
375                        + "SaveInfo or an authentication with a presentation");
376            }
377            mDestroyed = true;
378            return new FillResponse(this);
379        }
380
381        private void throwIfDestroyed() {
382            if (mDestroyed) {
383                throw new IllegalStateException("Already called #build()");
384            }
385        }
386    }
387
388    /////////////////////////////////////
389    // Object "contract" methods. //
390    /////////////////////////////////////
391    @Override
392    public String toString() {
393        if (!sDebug) return super.toString();
394
395        // TODO: create a dump() method instead
396        return new StringBuilder(
397                "FillResponse : [mRequestId=" + mRequestId)
398                .append(", datasets=").append(mDatasets)
399                .append(", saveInfo=").append(mSaveInfo)
400                .append(", clientState=").append(mClientState != null)
401                .append(", hasPresentation=").append(mPresentation != null)
402                .append(", hasAuthentication=").append(mAuthentication != null)
403                .append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds))
404                .append(", ignoredIds=").append(Arrays.toString(mIgnoredIds))
405                .append("]")
406                .toString();
407    }
408
409    /////////////////////////////////////
410    // Parcelable "contract" methods. //
411    /////////////////////////////////////
412
413    @Override
414    public int describeContents() {
415        return 0;
416    }
417
418    @Override
419    public void writeToParcel(Parcel parcel, int flags) {
420        parcel.writeTypedArrayList(mDatasets, flags);
421        parcel.writeParcelable(mSaveInfo, flags);
422        parcel.writeParcelable(mClientState, flags);
423        parcel.writeParcelableArray(mAuthenticationIds, flags);
424        parcel.writeParcelable(mAuthentication, flags);
425        parcel.writeParcelable(mPresentation, flags);
426        parcel.writeParcelableArray(mIgnoredIds, flags);
427        parcel.writeInt(mRequestId);
428    }
429
430    public static final Parcelable.Creator<FillResponse> CREATOR =
431            new Parcelable.Creator<FillResponse>() {
432        @Override
433        public FillResponse createFromParcel(Parcel parcel) {
434            // Always go through the builder to ensure the data ingested by
435            // the system obeys the contract of the builder to avoid attacks
436            // using specially crafted parcels.
437            final Builder builder = new Builder();
438            final ArrayList<Dataset> datasets = parcel.readTypedArrayList(null);
439            final int datasetCount = (datasets != null) ? datasets.size() : 0;
440            for (int i = 0; i < datasetCount; i++) {
441                builder.addDataset(datasets.get(i));
442            }
443            builder.setSaveInfo(parcel.readParcelable(null));
444            builder.setClientState(parcel.readParcelable(null));
445
446            // Sets authentication state.
447            final AutofillId[] authenticationIds = parcel.readParcelableArray(null,
448                    AutofillId.class);
449            final IntentSender authentication = parcel.readParcelable(null);
450            final RemoteViews presentation = parcel.readParcelable(null);
451            if (authenticationIds != null) {
452                builder.setAuthentication(authenticationIds, authentication, presentation);
453            }
454
455            builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class));
456            final FillResponse response = builder.build();
457
458            response.setRequestId(parcel.readInt());
459
460            return response;
461        }
462
463        @Override
464        public FillResponse[] newArray(int size) {
465            return new FillResponse[size];
466        }
467    };
468}
469