FillResponse.java revision 52d5d3dfc7d4d4b6dfed1686cc904c08d7433a04
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.view.autofill.Helper.DEBUG;
20
21import android.annotation.NonNull;
22import android.annotation.Nullable;
23import android.content.IntentSender;
24import android.os.Bundle;
25import android.os.Parcel;
26import android.os.Parcelable;
27import android.view.autofill.AutofillId;
28import android.view.autofill.AutofillManager;
29import android.widget.RemoteViews;
30
31import java.util.ArrayList;
32
33/**
34 * Response for a {@link
35 * AutofillService#onFillRequest(android.app.assist.AssistStructure,
36 * Bundle, int, 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 ArrayList<Dataset> mDatasets;
136    private final SaveInfo mSaveInfo;
137    private final Bundle mExtras;
138    private final RemoteViews mPresentation;
139    private final IntentSender mAuthentication;
140    private AutofillId[] mAuthenticationIds;
141
142    private FillResponse(@NonNull Builder builder) {
143        mDatasets = builder.mDatasets;
144        mSaveInfo = builder.mSaveInfo;
145        mExtras = builder.mExtras;
146        mPresentation = builder.mPresentation;
147        mAuthentication = builder.mAuthentication;
148        mAuthenticationIds = builder.mAuthenticationIds;
149    }
150
151    /** @hide */
152    public @Nullable Bundle getExtras() {
153        return mExtras;
154    }
155
156    /** @hide */
157    public @Nullable ArrayList<Dataset> getDatasets() {
158        return mDatasets;
159    }
160
161    /** @hide */
162    public @Nullable SaveInfo getSaveInfo() {
163        return mSaveInfo;
164    }
165
166    /** @hide */
167    public @Nullable RemoteViews getPresentation() {
168        return mPresentation;
169    }
170
171    /** @hide */
172    public @Nullable IntentSender getAuthentication() {
173        return mAuthentication;
174    }
175
176    /** @hide */
177    public @Nullable AutofillId[] getAuthenticationIds() {
178        return mAuthenticationIds;
179    }
180
181    /**
182     * Builder for {@link FillResponse} objects. You must to provide at least
183     * one dataset or set an authentication intent with a presentation view.
184     */
185    public static final class Builder {
186        private ArrayList<Dataset> mDatasets;
187        private SaveInfo mSaveInfo;
188        private Bundle mExtras;
189        private RemoteViews mPresentation;
190        private IntentSender mAuthentication;
191        private AutofillId[] mAuthenticationIds;
192        private boolean mDestroyed;
193
194        /**
195         * Requires a fill response authentication before autofilling the activity with
196         * any data set in this response.
197         *
198         * <p>This is typically useful when a user interaction is required to unlock their
199         * data vault if you encrypt the data set labels and data set data. It is recommended
200         * to encrypt only the sensitive data and not the data set labels which would allow
201         * auth on the data set level leading to a better user experience. Note that if you
202         * use sensitive data as a label, for example an email address, then it should also
203         * be encrypted. The provided {@link android.app.PendingIntent intent} must be an
204         * activity which implements your authentication flow. Also if you provide an auth
205         * intent you also need to specify the presentation view to be shown in the fill UI
206         * for the user to trigger your authentication flow.
207         *
208         * <p>When a user triggers autofill, the system launches the provided intent
209         * whose extras will have the {@link AutofillManager#EXTRA_ASSIST_STRUCTURE screen
210         * content}. Once you complete your authentication flow you should set the activity
211         * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated
212         * {@link FillResponse response} by setting it to the {@link
213         * AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra.
214         * For example, if you provided an empty {@link FillResponse resppnse} because the
215         * user's data was locked and marked that the response needs an authentication then
216         * in the response returned if authentication succeeds you need to provide all
217         * available data sets some of which may need to be further authenticated, for
218         * example a credit card whose CVV needs to be entered.
219         *
220         * <p>If you provide an authentication intent you must also provide a presentation
221         * which is used to visualize visualize the response for triggering the authentication
222         * flow.
223         *
224         * <p></><strong>Note:</strong> Do not make the provided pending intent
225         * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the
226         * platform needs to fill in the authentication arguments.
227         *
228         * @param authentication Intent to an activity with your authentication flow.
229         * @param presentation The presentation to visualize the response.
230         * @param ids id of Views that when focused will display the authentication UI affordance.
231         *
232         * @return This builder.
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            // TODO(b/33197203): assert ids is not null nor empty once old version is removed
239            if (authentication == null ^ presentation == null) {
240                throw new IllegalArgumentException("authentication and presentation"
241                        + " must be both non-null or null");
242            }
243            mAuthentication = authentication;
244            mPresentation = presentation;
245            mAuthenticationIds = ids;
246            return this;
247        }
248
249        /**
250         * TODO(b/33197203): will be removed once clients use the version that takes ids
251         * @hide
252         * @deprecated
253         */
254        @Deprecated
255        public @NonNull Builder setAuthentication(@Nullable IntentSender authentication,
256                @Nullable RemoteViews presentation) {
257            return setAuthentication(null, authentication, presentation);
258        }
259
260        /**
261         * Adds a new {@link Dataset} to this response.
262         *
263         * @return This builder.
264         */
265        public @NonNull Builder addDataset(@Nullable Dataset dataset) {
266            throwIfDestroyed();
267            if (dataset == null) {
268                return this;
269            }
270            if (mDatasets == null) {
271                mDatasets = new ArrayList<>();
272            }
273            if (!mDatasets.add(dataset)) {
274                return this;
275            }
276            return this;
277        }
278
279        /**
280         * Sets the {@link SaveInfo} associated with this response.
281         *
282         * <p>See {@link FillResponse} for more info.
283         *
284         * @return This builder.
285         */
286        public @NonNull Builder setSaveInfo(@NonNull SaveInfo saveInfo) {
287            throwIfDestroyed();
288            mSaveInfo = saveInfo;
289            return this;
290        }
291
292        /**
293         * Sets a {@link Bundle} that will be passed to subsequent APIs that
294         * manipulate this response. For example, they are passed to subsequent
295         * calls to {@link AutofillService#onFillRequest(
296         * android.app.assist.AssistStructure, Bundle, int,
297         * android.os.CancellationSignal, FillCallback)} and {@link AutofillService#onSaveRequest(
298         * android.app.assist.AssistStructure, Bundle, SaveCallback)}.
299         *
300         * <p>If this method is called on multiple {@link FillResponse} objects for the same
301         * activity, just the latest bundle is passed back to the service.
302         *
303         * @param extras The response extras.
304         * @return This builder.
305         */
306        public Builder setExtras(Bundle extras) {
307            throwIfDestroyed();
308            mExtras = extras;
309            return this;
310        }
311
312
313        /**
314         * Builds a new {@link FillResponse} instance. You must provide at least
315         * one dataset or some savable ids or an authentication with a presentation
316         * view.
317         *
318         * @return A built response.
319         */
320        public FillResponse build() {
321            throwIfDestroyed();
322
323            if (mAuthentication == null && mDatasets == null && mSaveInfo == null) {
324                throw new IllegalArgumentException("need to provide at least one DataSet or a "
325                        + "SaveInfo or an authentication with a presentation");
326            }
327            mDestroyed = true;
328            return new FillResponse(this);
329        }
330
331        private void throwIfDestroyed() {
332            if (mDestroyed) {
333                throw new IllegalStateException("Already called #build()");
334            }
335        }
336    }
337
338    /////////////////////////////////////
339    // Object "contract" methods. //
340    /////////////////////////////////////
341    @Override
342    public String toString() {
343        if (!DEBUG) return super.toString();
344
345        return new StringBuilder(
346                "FillResponse: [datasets=").append(mDatasets)
347                .append(", saveInfo=").append(mSaveInfo)
348                .append(", hasExtras=").append(mExtras != null)
349                .append(", hasPresentation=").append(mPresentation != null)
350                .append(", hasAuthentication=").append(mAuthentication != null)
351                .append(", authenticationSize=").append(mAuthenticationIds != null
352                        ? mAuthenticationIds.length : "N/A")
353                .toString();
354    }
355
356    /////////////////////////////////////
357    // Parcelable "contract" methods. //
358    /////////////////////////////////////
359
360    @Override
361    public int describeContents() {
362        return 0;
363    }
364
365    @Override
366    public void writeToParcel(Parcel parcel, int flags) {
367        parcel.writeTypedArrayList(mDatasets, flags);
368        parcel.writeParcelable(mSaveInfo, flags);
369        parcel.writeParcelable(mExtras, flags);
370        parcel.writeParcelableArray(mAuthenticationIds, flags);
371        parcel.writeParcelable(mAuthentication, flags);
372        parcel.writeParcelable(mPresentation, flags);
373    }
374
375    public static final Parcelable.Creator<FillResponse> CREATOR =
376            new Parcelable.Creator<FillResponse>() {
377        @Override
378        public FillResponse createFromParcel(Parcel parcel) {
379            // Always go through the builder to ensure the data ingested by
380            // the system obeys the contract of the builder to avoid attacks
381            // using specially crafted parcels.
382            final Builder builder = new Builder();
383            final ArrayList<Dataset> datasets = parcel.readTypedArrayList(null);
384            final int datasetCount = (datasets != null) ? datasets.size() : 0;
385            for (int i = 0; i < datasetCount; i++) {
386                builder.addDataset(datasets.get(i));
387            }
388            builder.setSaveInfo(parcel.readParcelable(null));
389            builder.setExtras(parcel.readParcelable(null));
390            builder.setAuthentication(parcel.readParcelableArray(null, AutofillId.class),
391                    parcel.readParcelable(null), parcel.readParcelable(null));
392            return builder.build();
393        }
394
395        @Override
396        public FillResponse[] newArray(int size) {
397            return new FillResponse[size];
398        }
399    };
400}
401