FillResponse.java revision 782043caf81055aa1c331e9cc15b24a10e1bf17a
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 */
16package android.service.autofill;
17
18import android.annotation.NonNull;
19import android.annotation.Nullable;
20import android.content.IntentSender;
21import android.os.Bundle;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.util.ArraySet;
25import android.view.autofill.AutoFillId;
26import android.view.autofill.AutoFillManager;
27
28/**
29 * Response for a {@link
30 * AutoFillService#onFillRequest(android.app.assist.AssistStructure,
31 * Bundle, android.os.CancellationSignal, FillCallback)} and
32 * authentication requests.
33 *
34 * <p>The response typically contains one or more {@link Dataset}s, each representing a set of
35 * fields that can be auto-filled together, and the Android system displays a dataset picker UI
36 * affordance that the user must use before the {@link android.app.Activity} is filled with
37 * the dataset.
38 *
39 * <p>For example, for a login page with username/password where the user only has one account in
40 * the response could be:
41 *
42 * <pre class="prettyprint">
43 *  new FillResponse.Builder()
44 *      .add(new Dataset.Builder("homer")
45 *          .setTextFieldValue(id1, "homer")
46 *          .setTextFieldValue(id2, "D'OH!")
47 *          .build())
48 *      .build();
49 * </pre>
50 *
51 * <p>If the user had 2 accounts, each with its own user-provided names, the response could be:
52 *
53 * <pre class="prettyprint">
54 *  new FillResponse.Builder()
55 *      .add(new Dataset.Builder("Homer's Account")
56 *          .setTextFieldValue(id1, "homer")
57 *          .setTextFieldValue(id2, "D'OH!")
58 *          .build())
59 *      .add(new Dataset.Builder("Bart's Account")
60 *          .setTextFieldValue(id1, "elbarto")
61 *          .setTextFieldValue(id2, "cowabonga")
62 *          .build())
63 *      .build();
64 * </pre>
65 *
66 * <p>If the user does not have any data associated with this {@link android.app.Activity} but
67 * the service wants to offer the user the option to save the data that was entered, then the
68 * service could populate the response with {@code savableIds} instead of {@link Dataset}s:
69 *
70 * <pre class="prettyprint">
71 *  new FillResponse.Builder()
72 *      .addSavableFields(id1, id2)
73 *      .build();
74 * </pre>
75 *
76 * <p>Similarly, there might be cases where the user data on the service is enough to populate some
77 * fields but not all, and the service would still be interested on saving the other fields. In this
78 * scenario, the service could populate the response with both {@link Dataset}s and {@code
79 * savableIds}:
80 *
81 * <pre class="prettyprint">
82 *   new FillResponse.Builder()
83 *       .add(new Dataset.Builder("Homer")
84 *          .setTextFieldValue(id1, "Homer")                  // first name
85 *          .setTextFieldValue(id2, "Simpson")                // last name
86 *          .setTextFieldValue(id3, "742 Evergreen Terrace")  // street
87 *          .setTextFieldValue(id4, "Springfield")            // city
88 *          .build())
89 *       .addSavableFields(id5, id6) // state and zipcode
90 *       .build();
91 *
92 * </pre>
93 *
94 * <p>Notice that the ids that are part of a dataset (ids 1 to 4, in this example) are automatically
95 * added to the {@code savableIds} list.
96 *
97 * <p>If the service has multiple {@link Dataset}s for different sections of the activity,
98 * for example, a user section for which there are two datasets followed by an address
99 * section for which there are two datasets for each user user, then it should "partition"
100 * the activity in sections and populate the response with just a subset of the data that would
101 * fulfill the first section (the name in our example); then once the user fills the first
102 * section and taps a field from the next section (the address in our example), the Android
103 * system would issue another request for that section, and so on. Note that if the user
104 * chooses to populate the first section with a service provided dataset, the subsequent request
105 * would contain the populated values so you don't try to provide suggestions for the first
106 * section but ony for the second one based on the context of what was already filled. For
107 * example, the first response could be:
108 *
109 * <pre class="prettyprint">
110 *  new FillResponse.Builder()
111 *      .add(new Dataset.Builder("Homer")
112 *          .setTextFieldValue(id1, "Homer")
113 *          .setTextFieldValue(id2, "Simpson")
114 *          .build())
115 *      .add(new Dataset.Builder("Bart")
116 *          .setTextFieldValue(id1, "Bart")
117 *          .setTextFieldValue(id2, "Simpson")
118 *          .build())
119 *      .build();
120 * </pre>
121 *
122 * <p>Then after the user picks the {@code Homer} dataset and taps the {@code Street} field to
123 * trigger another auto-fill request, the second response could be:
124 *
125 * <pre class="prettyprint">
126 *  new FillResponse.Builder()
127 *      .add(new Dataset.Builder("Home")
128 *          .setTextFieldValue(id3, "742 Evergreen Terrace")
129 *          .setTextFieldValue(id4, "Springfield")
130 *          .build())
131 *      .add(new Dataset.Builder("Work")
132 *          .setTextFieldValue(id3, "Springfield Power Plant")
133 *          .setTextFieldValue(id4, "Springfield")
134 *          .build())
135 *      .build();
136 * </pre>
137 *
138 * <p>The service could require user authentication at the {@link FillResponse} or the
139 * {@link Dataset} level, prior to auto-filling an activity - see {@link FillResponse.Builder
140 * #setAuthentication(IntentSender)} and {@link Dataset.Builder#setAuthentication(IntentSender)}.
141 * It is recommended that you encrypt only the sensitive data but leave the labels unencrypted
142 * which would allow you to provide the dataset names to the user and if they choose one
143 * them challenge the user to onAuthenticate. For example, if the user has a home and a work
144 * address the Home and Work labels could be stored unencrypted as they don't have any sensitive
145 * data while the address data is in an encrypted storage. If the user chooses Home, then the
146 * platform will start your authentication flow. If you encrypt all data and require auth
147 * at the response level the user will have to interact with the fill UI to trigger a request
148 * for the datasets as they don't see Home and Work options which will trigger your auth
149 * flow and after successfully authenticating the user will be presented with the Home and
150 * Work options where they can pick one. Hence, you have flexibility how to implement your
151 * auth while storing labels non-encrypted and data encrypted provides a better user
152 * experience.</p>
153 */
154public final class FillResponse implements Parcelable {
155    private static final boolean DEBUG = false;
156
157    private final ArraySet<Dataset> mDatasets;
158    private final ArraySet<AutoFillId> mSavableIds;
159    private final Bundle mExtras;
160    private final IntentSender mAuthentication;
161
162    private FillResponse(@NonNull Builder builder) {
163        mDatasets = builder.mDatasets;
164        mSavableIds = builder.mSavableIds;
165        mExtras = builder.mExtras;
166        mAuthentication = builder.mAuthentication;
167    }
168
169    /** @hide */
170    public @Nullable Bundle getExtras() {
171        return mExtras;
172    }
173
174    /** @hide */
175    public @Nullable ArraySet<Dataset> getDatasets() {
176        return mDatasets;
177    }
178
179    /** @hide */
180    public @Nullable ArraySet<AutoFillId> getSavableIds() {
181        return mSavableIds;
182    }
183
184    /** @hide */
185    public @Nullable IntentSender getAuthentication() {
186        return mAuthentication;
187    }
188
189    /**
190     * Builder for {@link FillResponse} objects. You must to provide at least
191     * one dataset or set an authentication intent.
192     */
193    public static final class Builder {
194        private ArraySet<Dataset> mDatasets;
195        private ArraySet<AutoFillId> mSavableIds;
196        private Bundle mExtras;
197        private IntentSender mAuthentication;
198        private boolean mDestroyed;
199
200        /**
201         * Creates a new {@link FillResponse} builder.
202         */
203        public Builder() {
204
205        }
206
207        /**
208         * Requires a fill response authentication before auto-filling the activity with
209         * any data set in this response.
210         *
211         * <p>This is typically useful when a user interaction is required to unlock their
212         * data vault if you encrypt the data set labels and data set data. It is recommended
213         * to encrypt only the sensitive data and not the data set labels which would allow
214         * auth on the data set level leading to a better user experience. Note that if you
215         * use sensitive data as a label, for example an email address, then it should also
216         * be encrypted. The provided {@link android.app.PendingIntent intent} must be an
217         * activity which implements your authentication flow.</p>
218         *
219         * <p>When a user triggers auto-fill, the system launches the provided intent
220         * whose extras will have the {@link
221         * AutoFillManager#EXTRA_ASSIST_STRUCTURE screen
222         * content}. Once you complete your authentication flow you should set the activity
223         * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated {@link
224         * FillResponse response} by setting it to the {@link
225         * AutoFillManager#EXTRA_AUTHENTICATION_RESULT} extra.
226         * For example, if you provided an empty {@link FillResponse resppnse} because the
227         * user's data was locked and marked that the response needs an authentication then
228         * in the response returned if authentication succeeds you need to provide all
229         * available data sets some of which may need to be further authenticated, for
230         * example a credit card whose CVV needs to be entered.</p>
231         *
232         * <p></><strong>Note:</strong> Do not make the provided pending intent
233         * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the
234         * platform needs to fill in the authentication arguments.</p>
235         *
236         * @param authentication Intent to an activity with your authentication flow.
237         *
238         * @see android.app.PendingIntent#getIntentSender()
239         */
240        public @NonNull Builder setAuthentication(@Nullable IntentSender authentication) {
241            throwIfDestroyed();
242            mAuthentication = authentication;
243            return this;
244        }
245
246        /**
247         * Adds a new {@link Dataset} to this response. Adding a dataset with the
248         * same id updates the existing one.
249         *
250         * @throws IllegalArgumentException if a dataset with same {@code name} already exists.
251         */
252        public@NonNull Builder addDataset(@Nullable Dataset dataset) {
253            throwIfDestroyed();
254            if (dataset == null) {
255                return this;
256            }
257            if (mDatasets == null) {
258                mDatasets = new ArraySet<>();
259            }
260            final int datasetCount = mDatasets.size();
261            for (int i = 0; i < datasetCount; i++) {
262                if (mDatasets.valueAt(i).getName().equals(dataset.getName())) {
263                    throw new IllegalArgumentException("Duplicate dataset name: "
264                            + dataset.getName());
265                }
266            }
267            if (!mDatasets.add(dataset)) {
268                return this;
269            }
270            final int fieldCount = dataset.getFieldIds().size();
271            for (int i = 0; i < fieldCount; i++) {
272                final AutoFillId id = dataset.getFieldIds().get(i);
273                if (mSavableIds == null) {
274                    mSavableIds = new ArraySet<>();
275                }
276                mSavableIds.add(id);
277            }
278            return this;
279        }
280
281        /**
282         * Adds ids of additional fields that the service would be interested to save (through
283         * {@link AutoFillService#onSaveRequest(
284         * android.app.assist.AssistStructure, Bundle, SaveCallback)})
285         * but were not indirectly set through {@link #addDataset(Dataset)}.
286         *
287         * <p>See {@link FillResponse} for examples.
288         */
289        public @NonNull Builder addSavableFields(@Nullable AutoFillId... ids) {
290            throwIfDestroyed();
291            if (ids == null) {
292                return this;
293            }
294            for (AutoFillId id : ids) {
295                if (mSavableIds == null) {
296                    mSavableIds = new ArraySet<>();
297                }
298                mSavableIds.add(id);
299            }
300            return this;
301        }
302
303        /**
304         * Sets a {@link Bundle} that will be passed to subsequent APIs that
305         * manipulate this response. For example, they are passed to subsequent
306         * calls to {@link AutoFillService#onFillRequest(
307         * android.app.assist.AssistStructure, Bundle, android.os.CancellationSignal,
308         * FillCallback)} and {@link
309         * AutoFillService#onSaveRequest(
310         * android.app.assist.AssistStructure, Bundle,
311         * SaveCallback)}.
312         */
313        public Builder setExtras(Bundle extras) {
314            throwIfDestroyed();
315            mExtras = extras;
316            return this;
317        }
318
319        /**
320         * Builds a new {@link FillResponse} instance.
321         */
322        public FillResponse build() {
323            throwIfDestroyed();
324            mDestroyed = true;
325            return new FillResponse(this);
326        }
327
328        private void throwIfDestroyed() {
329            if (mDestroyed) {
330                throw new IllegalStateException("Already called #build()");
331            }
332        }
333    }
334
335    /////////////////////////////////////
336    //  Object "contract" methods. //
337    /////////////////////////////////////
338    @Override
339    public String toString() {
340        if (!DEBUG) return super.toString();
341        final StringBuilder builder = new StringBuilder(
342                "FillResponse: [datasets=").append(mDatasets)
343                .append(", savableIds=").append(mSavableIds)
344                .append(", hasExtras=").append(mExtras != null)
345                .append(", hasAuthentication=").append(mAuthentication != null);
346        return builder.append(']').toString();
347    }
348
349    /////////////////////////////////////
350    //  Parcelable "contract" methods. //
351    /////////////////////////////////////
352
353    @Override
354    public int describeContents() {
355        return 0;
356    }
357
358    @Override
359    public void writeToParcel(Parcel parcel, int flags) {
360        parcel.writeTypedArraySet(mDatasets, 0);
361        parcel.writeTypedArraySet(mSavableIds, 0);
362        parcel.writeParcelable(mExtras, 0);
363        parcel.writeParcelable(mAuthentication, 0);
364    }
365
366    public static final Parcelable.Creator<FillResponse> CREATOR =
367            new Parcelable.Creator<FillResponse>() {
368        @Override
369        public FillResponse createFromParcel(Parcel parcel) {
370            // Always go through the builder to ensure the data ingested by
371            // the system obeys the contract of the builder to avoid attacks
372            // using specially crafted parcels.
373            final Builder builder = new Builder();
374            final ArraySet<Dataset> datasets = parcel.readTypedArraySet(null);
375            final int datasetCount = (datasets != null) ? datasets.size() : 0;
376            for (int i = 0; i < datasetCount; i++) {
377                builder.addDataset(datasets.valueAt(i));
378            }
379            final ArraySet<AutoFillId> fillIds = parcel.readTypedArraySet(null);
380            final int fillIdCount = (fillIds != null) ? fillIds.size() : 0;
381            for (int i = 0; i < fillIdCount; i++) {
382                builder.addSavableFields(fillIds.valueAt(i));
383            }
384            builder.setExtras(parcel.readParcelable(null));
385            builder.setAuthentication(parcel.readParcelable(null));
386            return builder.build();
387        }
388
389        @Override
390        public FillResponse[] newArray(int size) {
391            return new FillResponse[size];
392        }
393    };
394}
395