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