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