1 /*
2  * Copyright (C) 2017 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 package android.autofillservice.cts;
17 
18 import static com.google.common.truth.Truth.assertWithMessage;
19 
20 import static android.autofillservice.cts.Helper.getAutofillIds;
21 import android.app.assist.AssistStructure;
22 import android.app.assist.AssistStructure.ViewNode;
23 import android.content.IntentSender;
24 import android.os.Bundle;
25 import android.service.autofill.Dataset;
26 import android.service.autofill.FillCallback;
27 import android.service.autofill.FillResponse;
28 import android.service.autofill.SaveInfo;
29 import android.view.autofill.AutofillId;
30 import android.view.autofill.AutofillValue;
31 import android.widget.RemoteViews;
32 
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.function.Function;
39 
40 /**
41  * Helper class used to produce a {@link FillResponse} based on expected fields that should be
42  * present in the {@link AssistStructure}.
43  *
44  * <p>Typical usage:
45  *
46  * <pre class="prettyprint">
47  * InstrumentedAutoFillService.setFillResponse(new CannedFillResponse.Builder()
48  *               .addDataset(new CannedDataset.Builder("dataset_name")
49  *                   .setField("resource_id1", AutofillValue.forText("value1"))
50  *                   .setField("resource_id2", AutofillValue.forText("value2"))
51  *                   .build())
52  *               .build());
53  * </pre class="prettyprint">
54  */
55 final class CannedFillResponse {
56 
57     private final ResponseType mResponseType;
58     private final List<CannedDataset> mDatasets;
59     private final String mFailureMessage;
60     private final int mSaveType;
61     private final String[] mRequiredSavableIds;
62     private final String[] mOptionalSavableIds;
63     private final String mSaveDescription;
64     private final Bundle mExtras;
65     private final RemoteViews mPresentation;
66     private final IntentSender mAuthentication;
67     private final String[] mAuthenticationIds;
68     private final String[] mIgnoredIds;
69     private final int mNegativeActionStyle;
70     private final IntentSender mNegativeActionListener;
71     private final int mFlags;
72 
CannedFillResponse(Builder builder)73     private CannedFillResponse(Builder builder) {
74         mResponseType = builder.mResponseType;
75         mDatasets = builder.mDatasets;
76         mFailureMessage = builder.mFailureMessage;
77         mRequiredSavableIds = builder.mRequiredSavableIds;
78         mOptionalSavableIds = builder.mOptionalSavableIds;
79         mSaveDescription = builder.mSaveDescription;
80         mSaveType = builder.mSaveType;
81         mExtras = builder.mExtras;
82         mPresentation = builder.mPresentation;
83         mAuthentication = builder.mAuthentication;
84         mAuthenticationIds = builder.mAuthenticationIds;
85         mIgnoredIds = builder.mIgnoredIds;
86         mNegativeActionStyle = builder.mNegativeActionStyle;
87         mNegativeActionListener = builder.mNegativeActionListener;
88         mFlags = builder.mFlags;
89     }
90 
91     /**
92      * Constant used to pass a {@code null} response to the
93      * {@link FillCallback#onSuccess(FillResponse)} method.
94      */
95     static final CannedFillResponse NO_RESPONSE =
96             new Builder(ResponseType.NULL).build();
97 
98     /**
99      * Constant used to emulate a timeout by not calling any method on {@link FillCallback}.
100      */
101     static final CannedFillResponse DO_NOT_REPLY_RESPONSE =
102             new Builder(ResponseType.TIMEOUT).build();
103 
104 
getFailureMessage()105     String getFailureMessage() {
106         return mFailureMessage;
107     }
108 
getResponseType()109     ResponseType getResponseType() {
110         return mResponseType;
111     }
112 
113     /**
114      * Creates a new response, replacing the dataset field ids by the real ids from the assist
115      * structure.
116      */
asFillResponse(Function<String, ViewNode> nodeResolver)117     FillResponse asFillResponse(Function<String, ViewNode> nodeResolver) {
118         final FillResponse.Builder builder = new FillResponse.Builder();
119         if (mDatasets != null) {
120             for (CannedDataset cannedDataset : mDatasets) {
121                 final Dataset dataset = cannedDataset.asDataset(nodeResolver);
122                 assertWithMessage("Cannot create datase").that(dataset).isNotNull();
123                 builder.addDataset(dataset);
124             }
125         }
126         if (mRequiredSavableIds != null) {
127             final SaveInfo.Builder saveInfo = new SaveInfo.Builder(mSaveType,
128                     getAutofillIds(nodeResolver, mRequiredSavableIds));
129 
130             saveInfo.setFlags(mFlags);
131 
132             if (mOptionalSavableIds != null) {
133                 saveInfo.setOptionalIds(getAutofillIds(nodeResolver, mOptionalSavableIds));
134             }
135             if (mSaveDescription != null) {
136                 saveInfo.setDescription(mSaveDescription);
137             }
138             saveInfo.setNegativeAction(mNegativeActionStyle, mNegativeActionListener);
139             builder.setSaveInfo(saveInfo.build());
140         }
141         if (mIgnoredIds != null) {
142             builder.setIgnoredIds(getAutofillIds(nodeResolver, mIgnoredIds));
143         }
144         if (mAuthenticationIds != null) {
145             builder.setAuthentication(getAutofillIds(nodeResolver, mAuthenticationIds),
146                     mAuthentication, mPresentation);
147         }
148         return builder
149                 .setClientState(mExtras)
150                 .build();
151     }
152 
153     @Override
toString()154     public String toString() {
155         return "CannedFillResponse: [type=" + mResponseType
156                 + ",datasets=" + mDatasets
157                 + ", requiredSavableIds=" + Arrays.toString(mRequiredSavableIds)
158                 + ", optionalSavableIds=" + Arrays.toString(mOptionalSavableIds)
159                 + ", flags=" + mFlags
160                 + ", failureMessage=" + mFailureMessage
161                 + ", saveDescription=" + mSaveDescription
162                 + ", hasPresentation=" + (mPresentation != null)
163                 + ", hasAuthentication=" + (mAuthentication != null)
164                 + ", authenticationIds=" + Arrays.toString(mAuthenticationIds)
165                 + ", ignoredIds=" + Arrays.toString(mIgnoredIds)
166                 + "]";
167     }
168 
169     enum ResponseType {
170         NORMAL,
171         NULL,
172         TIMEOUT
173     }
174 
175     static class Builder {
176         private final List<CannedDataset> mDatasets = new ArrayList<>();
177         private final ResponseType mResponseType;
178         private String mFailureMessage;
179         private String[] mRequiredSavableIds;
180         private String[] mOptionalSavableIds;
181         private String mSaveDescription;
182         public int mSaveType = -1;
183         private Bundle mExtras;
184         private RemoteViews mPresentation;
185         private IntentSender mAuthentication;
186         private String[] mAuthenticationIds;
187         private String[] mIgnoredIds;
188         private int mNegativeActionStyle;
189         private IntentSender mNegativeActionListener;
190         private int mFlags;
191 
Builder(ResponseType type)192         public Builder(ResponseType type) {
193             mResponseType = type;
194         }
195 
Builder()196         public Builder() {
197             this(ResponseType.NORMAL);
198         }
199 
addDataset(CannedDataset dataset)200         public Builder addDataset(CannedDataset dataset) {
201             assertWithMessage("already set failure").that(mFailureMessage).isNull();
202             mDatasets.add(dataset);
203             return this;
204         }
205 
206         /**
207          * Sets the required savable ids based on they {@code resourceId}.
208          */
setRequiredSavableIds(int type, String... ids)209         public Builder setRequiredSavableIds(int type, String... ids) {
210             mSaveType = type;
211             mRequiredSavableIds = ids;
212             return this;
213         }
214 
setFlags(int flags)215         public Builder setFlags(int flags) {
216             mFlags = flags;
217             return this;
218         }
219 
220         /**
221          * Sets the optional savable ids based on they {@code resourceId}.
222          */
setOptionalSavableIds(String... ids)223         public Builder setOptionalSavableIds(String... ids) {
224             mOptionalSavableIds = ids;
225             return this;
226         }
227 
228         /**
229          * Sets the description passed to the {@link SaveInfo}.
230          */
setSaveDescription(String description)231         public Builder setSaveDescription(String description) {
232             mSaveDescription = description;
233             return this;
234         }
235 
236         /**
237          * Sets the extra passed to {@link
238          * android.service.autofill.FillResponse.Builder#setClientState(Bundle)}.
239          */
setExtras(Bundle data)240         public Builder setExtras(Bundle data) {
241             mExtras = data;
242             return this;
243         }
244 
245         /**
246          * Sets the view to present the response in the UI.
247          */
setPresentation(RemoteViews presentation)248         public Builder setPresentation(RemoteViews presentation) {
249             mPresentation = presentation;
250             return this;
251         }
252 
253         /**
254          * Sets the authentication intent.
255          */
setAuthentication(IntentSender authentication, String... ids)256         public Builder setAuthentication(IntentSender authentication, String... ids) {
257             mAuthenticationIds = ids;
258             mAuthentication = authentication;
259             return this;
260         }
261 
262         /**
263          * Sets the ignored fields based on resource ids.
264          */
setIgnoreFields(String...ids)265         public Builder setIgnoreFields(String...ids) {
266             mIgnoredIds = ids;
267             return this;
268         }
269 
270         /**
271          * Sets the negative action spec.
272          */
setNegativeAction(int style, IntentSender listener)273         public Builder setNegativeAction(int style, IntentSender listener) {
274             mNegativeActionStyle = style;
275             mNegativeActionListener = listener;
276             return this;
277         }
278 
build()279         public CannedFillResponse build() {
280             return new CannedFillResponse(this);
281         }
282 
283         /**
284          * Sets the response to call {@link FillCallback#onFailure(CharSequence)}.
285          */
returnFailure(String message)286         public Builder returnFailure(String message) {
287             assertWithMessage("already added datasets").that(mDatasets).isEmpty();
288             mFailureMessage = message;
289             return this;
290         }
291     }
292 
293     /**
294      * Helper class used to produce a {@link Dataset} based on expected fields that should be
295      * present in the {@link AssistStructure}.
296      *
297      * <p>Typical usage:
298      *
299      * <pre class="prettyprint">
300      * InstrumentedAutoFillService.setFillResponse(new CannedFillResponse.Builder()
301      *               .addDataset(new CannedDataset.Builder("dataset_name")
302      *                   .setField("resource_id1", AutofillValue.forText("value1"))
303      *                   .setField("resource_id2", AutofillValue.forText("value2"))
304      *                   .build())
305      *               .build());
306      * </pre class="prettyprint">
307      */
308     static class CannedDataset {
309         private final Map<String, AutofillValue> mFieldValues;
310         private final Map<String, RemoteViews> mFieldPresentations;
311         private final RemoteViews mPresentation;
312         private final IntentSender mAuthentication;
313         private final String mId;
314 
CannedDataset(Builder builder)315         private CannedDataset(Builder builder) {
316             mFieldValues = builder.mFieldValues;
317             mFieldPresentations = builder.mFieldPresentations;
318             mPresentation = builder.mPresentation;
319             mAuthentication = builder.mAuthentication;
320             mId = builder.mId;
321         }
322 
323         /**
324          * Creates a new dataset, replacing the field ids by the real ids from the assist structure.
325          */
asDataset(Function<String, ViewNode> nodeResolver)326         Dataset asDataset(Function<String, ViewNode> nodeResolver) {
327             final Dataset.Builder builder = (mPresentation == null)
328                     ? new Dataset.Builder()
329                     : new Dataset.Builder(mPresentation);
330 
331             if (mFieldValues != null) {
332                 for (Map.Entry<String, AutofillValue> entry : mFieldValues.entrySet()) {
333                     final String resourceId = entry.getKey();
334                     final ViewNode node = nodeResolver.apply(resourceId);
335                     if (node == null) {
336                         throw new AssertionError("No node with resource id " + resourceId);
337                     }
338                     final AutofillId id = node.getAutofillId();
339                     final AutofillValue value = entry.getValue();
340                     final RemoteViews presentation = mFieldPresentations.get(resourceId);
341                     if (presentation != null) {
342                         builder.setValue(id, value, presentation);
343                     } else {
344                         builder.setValue(id, value);
345                     }
346                 }
347             }
348             builder.setId(mId).setAuthentication(mAuthentication);
349             return builder.build();
350         }
351 
352         @Override
toString()353         public String toString() {
354             return "CannedDataset " + mId + " : [hasPresentation=" + (mPresentation != null)
355                     + ", fieldPresentations=" + (mFieldPresentations)
356                     + ", hasAuthentication=" + (mAuthentication != null)
357                     + ", fieldValuess=" + mFieldValues + "]";
358         }
359 
360         static class Builder {
361             private final Map<String, AutofillValue> mFieldValues = new HashMap<>();
362             private final Map<String, RemoteViews> mFieldPresentations = new HashMap<>();
363             private RemoteViews mPresentation;
364             private IntentSender mAuthentication;
365             private String mId;
366 
Builder()367             public Builder() {
368 
369             }
370 
Builder(RemoteViews presentation)371             public Builder(RemoteViews presentation) {
372                 mPresentation = presentation;
373             }
374 
375             /**
376              * Sets the canned value of a text field based on its {@code resourceId}.
377              */
setField(String resourceId, String text)378             public Builder setField(String resourceId, String text) {
379                 return setField(resourceId, AutofillValue.forText(text));
380             }
381 
382             /**
383              * Sets the canned value of a list field based on its {@code resourceId}.
384              */
setField(String resourceId, int index)385             public Builder setField(String resourceId, int index) {
386                 return setField(resourceId, AutofillValue.forList(index));
387             }
388 
389             /**
390              * Sets the canned value of a toggle field based on its {@code resourceId}.
391              */
setField(String resourceId, boolean toggled)392             public Builder setField(String resourceId, boolean toggled) {
393                 return setField(resourceId, AutofillValue.forToggle(toggled));
394             }
395 
396             /**
397              * Sets the canned value of a date field based on its {@code resourceId}.
398              */
setField(String resourceId, long date)399             public Builder setField(String resourceId, long date) {
400                 return setField(resourceId, AutofillValue.forDate(date));
401             }
402 
403             /**
404              * Sets the canned value of a date field based on its {@code resourceId}.
405              */
setField(String resourceId, AutofillValue value)406             public Builder setField(String resourceId, AutofillValue value) {
407                 mFieldValues.put(resourceId, value);
408                 return this;
409             }
410 
411             /**
412              * Sets the canned value of a field based on its {@code resourceId}.
413              */
setField(String resourceId, String text, RemoteViews presentation)414             public Builder setField(String resourceId, String text, RemoteViews presentation) {
415                 setField(resourceId, text);
416                 mFieldPresentations.put(resourceId, presentation);
417                 return this;
418             }
419 
420             /**
421              * Sets the view to present the response in the UI.
422              */
setPresentation(RemoteViews presentation)423             public Builder setPresentation(RemoteViews presentation) {
424                 mPresentation = presentation;
425                 return this;
426             }
427 
428             /**
429              * Sets the authentication intent.
430              */
setAuthentication(IntentSender authentication)431             public Builder setAuthentication(IntentSender authentication) {
432                 mAuthentication = authentication;
433                 return this;
434             }
435 
436             /**
437              * Sets the name.
438              */
setId(String id)439             public Builder setId(String id) {
440                 mId = id;
441                 return this;
442             }
443 
build()444             public CannedDataset build() {
445                 return new CannedDataset(this);
446             }
447         }
448     }
449 }
450