Dataset.java revision 2ef19c1d73f89ca4718b5a8f0c2e7221621e844f
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.sDebug;
20
21import android.annotation.NonNull;
22import android.annotation.Nullable;
23import android.content.IntentSender;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.view.autofill.AutofillId;
27import android.view.autofill.AutofillValue;
28import android.widget.RemoteViews;
29import com.android.internal.util.Preconditions;
30
31import java.util.ArrayList;
32
33/**
34 * A dataset object represents a group of key/value pairs used to autofill parts of a screen.
35 *
36 * <p>In its simplest form, a dataset contains one or more key / value pairs (comprised of
37 * {@link AutofillId} and {@link AutofillValue} respectively); and one or more
38 * {@link RemoteViews presentation} for these pairs (a pair could have its own
39 * {@link RemoteViews presentation}, or use the default {@link RemoteViews presentation} associated
40 * with the whole dataset). When an autofill service returns datasets in a {@link FillResponse}
41 * and the screen input is focused in a view that is present in at least one of these datasets,
42 * the Android System displays a UI affordance containing the {@link RemoteViews presentation} of
43 * all datasets pairs that have that view's {@link AutofillId}. Then, when the user selects a
44 * dataset from the affordance, all views in that dataset are autofilled.
45 *
46 * <p>In a more sophisticated form, the dataset value can be protected until the user authenticates
47 * the dataset - see {@link Dataset.Builder#setAuthentication(IntentSender)}.
48 *
49 * @see android.service.autofill.AutofillService for more information and examples about the
50 * role of datasets in the autofill workflow.
51 */
52public final class Dataset implements Parcelable {
53
54    private final ArrayList<AutofillId> mFieldIds;
55    private final ArrayList<AutofillValue> mFieldValues;
56    private final ArrayList<RemoteViews> mFieldPresentations;
57    private final RemoteViews mPresentation;
58    private final IntentSender mAuthentication;
59    @Nullable String mId;
60
61    private Dataset(Builder builder) {
62        mFieldIds = builder.mFieldIds;
63        mFieldValues = builder.mFieldValues;
64        mFieldPresentations = builder.mFieldPresentations;
65        mPresentation = builder.mPresentation;
66        mAuthentication = builder.mAuthentication;
67        mId = builder.mId;
68    }
69
70    /** @hide */
71    public @Nullable ArrayList<AutofillId> getFieldIds() {
72        return mFieldIds;
73    }
74
75    /** @hide */
76    public @Nullable ArrayList<AutofillValue> getFieldValues() {
77        return mFieldValues;
78    }
79
80    /** @hide */
81    public RemoteViews getFieldPresentation(int index) {
82        final RemoteViews customPresentation = mFieldPresentations.get(index);
83        return customPresentation != null ? customPresentation : mPresentation;
84    }
85
86    /** @hide */
87    public @Nullable IntentSender getAuthentication() {
88        return mAuthentication;
89    }
90
91    /** @hide */
92    public boolean isEmpty() {
93        return mFieldIds == null || mFieldIds.isEmpty();
94    }
95
96    @Override
97    public String toString() {
98        if (!sDebug) return super.toString();
99
100        return new StringBuilder("Dataset " + mId + " [")
101                .append("fieldIds=").append(mFieldIds)
102                .append(", fieldValues=").append(mFieldValues)
103                .append(", fieldPresentations=")
104                .append(mFieldPresentations == null ? 0 : mFieldPresentations.size())
105                .append(", hasPresentation=").append(mPresentation != null)
106                .append(", hasAuthentication=").append(mAuthentication != null)
107                .append(']').toString();
108    }
109
110    /**
111     * Gets the id of this dataset.
112     *
113     * @return The id of this dataset or {@code null} if not set
114     *
115     * @hide
116     */
117    public String getId() {
118        return mId;
119    }
120
121    /**
122     * A builder for {@link Dataset} objects. You must provide at least
123     * one value for a field or set an authentication intent.
124     */
125    public static final class Builder {
126        private ArrayList<AutofillId> mFieldIds;
127        private ArrayList<AutofillValue> mFieldValues;
128        private ArrayList<RemoteViews> mFieldPresentations;
129        private RemoteViews mPresentation;
130        private IntentSender mAuthentication;
131        private boolean mDestroyed;
132        @Nullable private String mId;
133
134        /**
135         * Creates a new builder.
136         *
137         * @param presentation The presentation used to visualize this dataset.
138         */
139        public Builder(@NonNull RemoteViews presentation) {
140            Preconditions.checkNotNull(presentation, "presentation must be non-null");
141            mPresentation = presentation;
142        }
143
144        /**
145         * Creates a new builder for a dataset where each field will be visualized independently.
146         *
147         * <p>When using this constructor, fields must be set through
148         * {@link #setValue(AutofillId, AutofillValue, RemoteViews)}.
149         */
150        public Builder() {
151        }
152
153        /**
154         * Requires a dataset authentication before autofilling the activity with this dataset.
155         *
156         * <p>This method is called when you need to provide an authentication
157         * UI for the data set. For example, when a data set contains credit card information
158         * (such as number, expiration date, and verification code), you can display UI
159         * asking for the verification code before filing in the data. Even if the
160         * data set is completely populated the system will launch the specified authentication
161         * intent and will need your approval to fill it in. Since the data set is "locked"
162         * until the user authenticates it, typically this data set name is masked
163         * (for example, "VISA....1234"). Typically you would want to store the data set
164         * labels non-encrypted and the actual sensitive data encrypted and not in memory.
165         * This allows showing the labels in the UI while involving the user if one of
166         * the items with these labels is chosen. Note that if you use sensitive data as
167         * a label, for example an email address, then it should also be encrypted.</p>
168         *
169         * <p>When a user triggers autofill, the system launches the provided intent
170         * whose extras will have the {@link
171         * android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen content},
172         * and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE client
173         * state}. Once you complete your authentication flow you should set the activity
174         * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated
175         * {@link Dataset dataset} or a fully-populated {@link FillResponse response} by
176         * setting it to the {@link
177         * android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra. If you
178         * provide a dataset in the result, it will replace the authenticated dataset and
179         * will be immediately filled in. If you provide a response, it will replace the
180         * current response and the UI will be refreshed. For example, if you provided
181         * credit card information without the CVV for the data set in the {@link FillResponse
182         * response} then the returned data set should contain the CVV entry.
183         *
184         * <p><b>NOTE:</b> Do not make the provided pending intent
185         * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the
186         * platform needs to fill in the authentication arguments.
187         *
188         * @param authentication Intent to an activity with your authentication flow.
189         * @return This builder.
190         *
191         * @see android.app.PendingIntent
192         */
193        public @NonNull Builder setAuthentication(@Nullable IntentSender authentication) {
194            throwIfDestroyed();
195            mAuthentication = authentication;
196            return this;
197        }
198
199        /**
200         * Sets the id for the dataset so its usage history can be retrieved later.
201         *
202         * <p>The id of the last selected dataset can be read from
203         * {@link AutofillService#getFillEventHistory()}. If the id is not set it will not be clear
204         * if a dataset was selected as {@link AutofillService#getFillEventHistory()} uses
205         * {@code null} to indicate that no dataset was selected.
206         *
207         * @param id id for this dataset or {@code null} to unset.
208
209         * @return This builder.
210         */
211        public @NonNull Builder setId(@Nullable String id) {
212            throwIfDestroyed();
213
214            mId = id;
215            return this;
216        }
217
218        /**
219         * Sets the value of a field.
220         *
221         * @param id id returned by {@link
222         *         android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
223         * @param value value to be autofilled. Pass {@code null} if you do not have the value
224         *        but the target view is a logical part of the dataset. For example, if
225         *        the dataset needs an authentication and you have no access to the value.
226         * @return This builder.
227         * @throws IllegalStateException if the builder was constructed without a
228         * {@link RemoteViews presentation}.
229         */
230        public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value) {
231            throwIfDestroyed();
232            if (mPresentation == null) {
233                throw new IllegalStateException("Dataset presentation not set on constructor");
234            }
235            setValueAndPresentation(id, value, null);
236            return this;
237        }
238
239        /**
240         * Sets the value of a field, using a custom {@link RemoteViews presentation} to
241         * visualize it.
242         *
243         * @param id id returned by {@link
244         *         android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
245         * @param value value to be auto filled. Pass {@code null} if you do not have the value
246         *        but the target view is a logical part of the dataset. For example, if
247         *        the dataset needs an authentication and you have no access to the value.
248         *        Filtering matches any user typed string to {@code null} values.
249         * @param presentation The presentation used to visualize this field.
250         * @return This builder.
251         */
252        public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
253                @NonNull RemoteViews presentation) {
254            throwIfDestroyed();
255            Preconditions.checkNotNull(presentation, "presentation cannot be null");
256            setValueAndPresentation(id, value, presentation);
257            return this;
258        }
259
260        private void setValueAndPresentation(AutofillId id, AutofillValue value,
261                RemoteViews presentation) {
262            Preconditions.checkNotNull(id, "id cannot be null");
263            if (mFieldIds != null) {
264                final int existingIdx = mFieldIds.indexOf(id);
265                if (existingIdx >= 0) {
266                    mFieldValues.set(existingIdx, value);
267                    mFieldPresentations.set(existingIdx, presentation);
268                    return;
269                }
270            } else {
271                mFieldIds = new ArrayList<>();
272                mFieldValues = new ArrayList<>();
273                mFieldPresentations = new ArrayList<>();
274            }
275            mFieldIds.add(id);
276            mFieldValues.add(value);
277            mFieldPresentations.add(presentation);
278        }
279
280        /**
281         * Creates a new {@link Dataset} instance.
282         *
283         * <p>You should not interact with this builder once this method is called.
284         *
285         * <p>It is required that you specify at least one field before calling this method. It's
286         * also mandatory to provide a presentation view to visualize the data set in the UI.
287         *
288         * @return The built dataset.
289         */
290        public @NonNull Dataset build() {
291            throwIfDestroyed();
292            mDestroyed = true;
293            if (mFieldIds == null) {
294                throw new IllegalArgumentException("at least one value must be set");
295            }
296            return new Dataset(this);
297        }
298
299        private void throwIfDestroyed() {
300            if (mDestroyed) {
301                throw new IllegalStateException("Already called #build()");
302            }
303        }
304    }
305
306    /////////////////////////////////////
307    //  Parcelable "contract" methods. //
308    /////////////////////////////////////
309
310    @Override
311    public int describeContents() {
312        return 0;
313    }
314
315    @Override
316    public void writeToParcel(Parcel parcel, int flags) {
317        parcel.writeParcelable(mPresentation, flags);
318        parcel.writeTypedArrayList(mFieldIds, flags);
319        parcel.writeTypedArrayList(mFieldValues, flags);
320        parcel.writeParcelableList(mFieldPresentations, flags);
321        parcel.writeParcelable(mAuthentication, flags);
322        parcel.writeString(mId);
323    }
324
325    public static final Creator<Dataset> CREATOR = new Creator<Dataset>() {
326        @Override
327        public Dataset createFromParcel(Parcel parcel) {
328            // Always go through the builder to ensure the data ingested by
329            // the system obeys the contract of the builder to avoid attacks
330            // using specially crafted parcels.
331            final RemoteViews presentation = parcel.readParcelable(null);
332            final Builder builder = (presentation == null)
333                    ? new Builder()
334                    : new Builder(presentation);
335            final ArrayList<AutofillId> ids = parcel.readTypedArrayList(null);
336            final ArrayList<AutofillValue> values = parcel.readTypedArrayList(null);
337            final ArrayList<RemoteViews> presentations = new ArrayList<>();
338            parcel.readParcelableList(presentations, null);
339            final int idCount = (ids != null) ? ids.size() : 0;
340            final int valueCount = (values != null) ? values.size() : 0;
341            for (int i = 0; i < idCount; i++) {
342                final AutofillId id = ids.get(i);
343                final AutofillValue value = (valueCount > i) ? values.get(i) : null;
344                final RemoteViews fieldPresentation = presentations.isEmpty() ? null
345                        : presentations.get(i);
346                builder.setValueAndPresentation(id, value, fieldPresentation);
347            }
348            builder.setAuthentication(parcel.readParcelable(null));
349            builder.setId(parcel.readString());
350            return builder.build();
351        }
352
353        @Override
354        public Dataset[] newArray(int size) {
355            return new Dataset[size];
356        }
357    };
358}
359