1 /*
2  * Copyright (C) 2019 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.testcore;
17 
18 import static android.autofillservice.cts.testcore.AugmentedHelper.getContentDescriptionForUi;
19 
20 import android.autofillservice.cts.R;
21 import android.content.ClipData;
22 import android.content.ClipDescription;
23 import android.content.Context;
24 import android.content.IntentSender;
25 import android.os.Bundle;
26 import android.service.autofill.InlinePresentation;
27 import android.service.autofill.augmented.FillCallback;
28 import android.service.autofill.augmented.FillController;
29 import android.service.autofill.augmented.FillRequest;
30 import android.service.autofill.augmented.FillResponse;
31 import android.service.autofill.augmented.FillWindow;
32 import android.service.autofill.augmented.PresentationParams;
33 import android.service.autofill.augmented.PresentationParams.Area;
34 import android.util.ArrayMap;
35 import android.util.Log;
36 import android.util.Pair;
37 import android.view.LayoutInflater;
38 import android.view.autofill.AutofillId;
39 import android.view.autofill.AutofillValue;
40 import android.widget.TextView;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Objects;
49 import java.util.stream.Collectors;
50 
51 /**
52  * Helper class used to produce a {@link FillResponse}.
53  */
54 public final class CannedAugmentedFillResponse {
55 
56     private static final String TAG = CannedAugmentedFillResponse.class.getSimpleName();
57 
58     public static final String CLIENT_STATE_KEY = "clientStateKey";
59     public static final String CLIENT_STATE_VALUE = "clientStateValue";
60 
61     private final AugmentedResponseType mResponseType;
62     private final Map<AutofillId, Dataset> mDatasets;
63     private long mDelay;
64     private final Dataset mOnlyDataset;
65     private final @Nullable List<Dataset> mInlineSuggestions;
66 
CannedAugmentedFillResponse(@onNull Builder builder)67     private CannedAugmentedFillResponse(@NonNull Builder builder) {
68         mResponseType = builder.mResponseType;
69         mDatasets = builder.mDatasets;
70         mDelay = builder.mDelay;
71         mOnlyDataset = builder.mOnlyDataset;
72         mInlineSuggestions = builder.mInlineSuggestions;
73     }
74 
75     /**
76      * Constant used to pass a {@code null} response to the
77      * {@link FillCallback#onSuccess(FillResponse)} method.
78      */
79     public static final CannedAugmentedFillResponse NO_AUGMENTED_RESPONSE =
80             new Builder(AugmentedResponseType.NULL).build();
81 
82     /**
83      * Constant used to emulate a timeout by not calling any method on {@link FillCallback}.
84      */
85     public static final CannedAugmentedFillResponse DO_NOT_REPLY_AUGMENTED_RESPONSE =
86             new Builder(AugmentedResponseType.TIMEOUT).build();
87 
getResponseType()88     public AugmentedResponseType getResponseType() {
89         return mResponseType;
90     }
91 
getDelay()92     public long getDelay() {
93         return mDelay;
94     }
95 
96     /**
97      * Creates the "real" response.
98      */
asFillResponse(@onNull Context context, @NonNull FillRequest request, @NonNull FillController controller)99     public FillResponse asFillResponse(@NonNull Context context, @NonNull FillRequest request,
100             @NonNull FillController controller) {
101         final AutofillId focusedId = request.getFocusedId();
102 
103         final Dataset dataset;
104         if (mOnlyDataset != null) {
105             dataset = mOnlyDataset;
106         } else {
107             dataset = mDatasets.get(focusedId);
108         }
109         if (dataset == null) {
110             Log.d(TAG, "no dataset for field " + focusedId);
111             return null;
112         }
113 
114         Log.d(TAG, "asFillResponse: id=" + focusedId + ", dataset=" + dataset);
115 
116         final PresentationParams presentationParams = request.getPresentationParams();
117         if (presentationParams == null) {
118             Log.w(TAG, "No PresentationParams");
119             return null;
120         }
121 
122         final Area strip = presentationParams.getSuggestionArea();
123         if (strip == null) {
124             Log.w(TAG, "No suggestion strip");
125             return null;
126         }
127 
128         if (mInlineSuggestions != null) {
129             return createResponseWithInlineSuggestion();
130         }
131 
132         final LayoutInflater inflater = LayoutInflater.from(context);
133         final TextView rootView = (TextView) inflater.inflate(R.layout.augmented_autofill_ui, null);
134 
135         Log.d(TAG, "Setting autofill UI text to:" + dataset.mPresentation);
136         rootView.setText(dataset.mPresentation);
137 
138         rootView.setContentDescription(getContentDescriptionForUi(focusedId));
139         final FillWindow fillWindow = new FillWindow();
140         rootView.setOnClickListener((v) -> {
141             Log.d(TAG, "Destroying window first");
142             fillWindow.destroy();
143             final List<Pair<AutofillId, AutofillValue>> values;
144             final AutofillValue onlyValue = dataset.getOnlyFieldValue();
145             if (onlyValue != null) {
146                 Log.i(TAG, "Autofilling only value for " + focusedId + " as " + onlyValue);
147                 values = new ArrayList<>(1);
148                 values.add(new Pair<AutofillId, AutofillValue>(focusedId, onlyValue));
149             } else {
150                 values = dataset.getValues();
151                 Log.i(TAG, "Autofilling: " + AugmentedHelper.toString(values));
152             }
153             controller.autofill(values);
154         });
155 
156         boolean ok = fillWindow.update(strip, rootView, 0);
157         if (!ok) {
158             Log.w(TAG, "FillWindow.update() failed for " + strip + " and " + rootView);
159             return null;
160         }
161 
162         return new FillResponse.Builder().setFillWindow(fillWindow).build();
163     }
164 
165     @Override
toString()166     public String toString() {
167         return "CannedAugmentedFillResponse: [type=" + mResponseType
168                 + ", onlyDataset=" + mOnlyDataset
169                 + ", datasets=" + mDatasets
170                 + "]";
171     }
172 
173     public enum AugmentedResponseType {
174         NORMAL,
175         NULL,
176         TIMEOUT,
177     }
178 
newClientState()179     private Bundle newClientState() {
180         Bundle b = new Bundle();
181         b.putString(CLIENT_STATE_KEY, CLIENT_STATE_VALUE);
182         return b;
183     }
184 
createResponseWithInlineSuggestion()185     private FillResponse createResponseWithInlineSuggestion() {
186         List<android.service.autofill.Dataset> list = new ArrayList<>();
187         for (Dataset dataset : mInlineSuggestions) {
188             if (!dataset.getValues().isEmpty()) {
189                 android.service.autofill.Dataset.Builder datasetBuilder =
190                         new android.service.autofill.Dataset.Builder();
191                 for (Pair<AutofillId, AutofillValue> pair : dataset.getValues()) {
192                     final AutofillId id = pair.first;
193                     datasetBuilder.setFieldInlinePresentation(id, pair.second, null,
194                             dataset.mFieldPresentationById.get(id));
195                     datasetBuilder.setAuthentication(dataset.mAuthentication);
196                 }
197                 list.add(datasetBuilder.build());
198             } else if (dataset.getContent() != null) {
199                 Pair<AutofillId, ClipData> fieldContent = dataset.getContent();
200                 InlinePresentation inlinePresentation = Helper.createInlinePresentation(
201                         fieldContent.second.getDescription().getLabel().toString());
202                 android.service.autofill.Dataset realDataset =
203                         new android.service.autofill.Dataset.Builder(inlinePresentation)
204                                 .setContent(fieldContent.first, fieldContent.second)
205                                 .setAuthentication(dataset.mAuthentication)
206                                 .build();
207                 list.add(realDataset);
208             }
209         }
210         return new FillResponse.Builder().setInlineSuggestions(list).setClientState(
211                 newClientState()).build();
212     }
213 
214     public static final class Builder {
215         private final Map<AutofillId, Dataset> mDatasets = new ArrayMap<>();
216         private final AugmentedResponseType mResponseType;
217         private long mDelay;
218         private Dataset mOnlyDataset;
219         private @Nullable List<Dataset> mInlineSuggestions;
220 
Builder(@onNull AugmentedResponseType type)221         public Builder(@NonNull AugmentedResponseType type) {
222             mResponseType = type;
223         }
224 
Builder()225         public Builder() {
226             this(AugmentedResponseType.NORMAL);
227         }
228 
229         /**
230          * Sets the {@link Dataset} that will be filled when the given {@code ids} is focused and
231          * the UI is tapped.
232          */
233         @NonNull
setDataset(@onNull Dataset dataset, @NonNull AutofillId... ids)234         public Builder setDataset(@NonNull Dataset dataset, @NonNull AutofillId... ids) {
235             if (mOnlyDataset != null) {
236                 throw new IllegalStateException("already called setOnlyDataset()");
237             }
238             for (AutofillId id : ids) {
239                 mDatasets.put(id, dataset);
240             }
241             return this;
242         }
243 
244         /**
245          * The {@link android.service.autofill.Dataset}s representing the inline suggestions data.
246          * Defaults to null if no inline suggestions are available from the service.
247          */
248         @NonNull
addInlineSuggestion(@onNull Dataset dataset)249         public Builder addInlineSuggestion(@NonNull Dataset dataset) {
250             if (mInlineSuggestions == null) {
251                 mInlineSuggestions = new ArrayList<>();
252             }
253             mInlineSuggestions.add(dataset);
254             return this;
255         }
256 
257         /**
258          * Sets the delay for onFillRequest().
259          */
setDelay(long delay)260         public Builder setDelay(long delay) {
261             mDelay = delay;
262             return this;
263         }
264 
265         /**
266          * Sets the only dataset that will be returned.
267          *
268          * <p>Used when the test case doesn't know the autofill id of the focused field.
269          * @param dataset
270          */
271         @NonNull
setOnlyDataset(@onNull Dataset dataset)272         public Builder setOnlyDataset(@NonNull Dataset dataset) {
273             if (!mDatasets.isEmpty()) {
274                 throw new IllegalStateException("already called setDataset()");
275             }
276             mOnlyDataset = dataset;
277             return this;
278         }
279 
280         @NonNull
build()281         public CannedAugmentedFillResponse build() {
282             return new CannedAugmentedFillResponse(this);
283         }
284     } // CannedAugmentedFillResponse.Builder
285 
286 
287     /**
288      * Helper class used to define which fields will be autofilled when the user taps the Augmented
289      * Autofill UI.
290      */
291     public static class Dataset {
292         private final Map<AutofillId, AutofillValue> mFieldValuesById;
293         private final Map<AutofillId, InlinePresentation> mFieldPresentationById;
294         private final String mPresentation;
295         private final AutofillValue mOnlyFieldValue;
296         private final Pair<AutofillId, ClipData> mFieldContent;
297         private final IntentSender mAuthentication;
298 
Dataset(@onNull Builder builder)299         private Dataset(@NonNull Builder builder) {
300             mFieldValuesById = builder.mFieldValuesById;
301             mPresentation = builder.mPresentation;
302             mOnlyFieldValue = builder.mOnlyFieldValue;
303             mFieldPresentationById = builder.mFieldPresentationById;
304             mFieldContent = (builder.mFieldIdForContent == null) ? null
305                     : Pair.create(builder.mFieldIdForContent, builder.mFieldContent);
306             this.mAuthentication = builder.mAuthentication;
307         }
308 
309         @NonNull
getValues()310         public List<Pair<AutofillId, AutofillValue>> getValues() {
311             return mFieldValuesById.entrySet().stream()
312                     .map((entry) -> (new Pair<>(entry.getKey(), entry.getValue())))
313                     .collect(Collectors.toList());
314         }
315 
316         @Nullable
getOnlyFieldValue()317         public AutofillValue getOnlyFieldValue() {
318             return mOnlyFieldValue;
319         }
320 
321         @Nullable
getContent()322         public Pair<AutofillId, ClipData> getContent() {
323             return mFieldContent;
324         }
325 
326         @Override
toString()327         public String toString() {
328             return "Dataset: [presentation=" + mPresentation
329                     + (mOnlyFieldValue == null ? "" : ", onlyField=" + mOnlyFieldValue)
330                     + (mFieldValuesById.isEmpty() ? "" : ", fields=" + mFieldValuesById)
331                     + (mFieldContent == null ? "" : ", content=" + mFieldContent)
332                     + (mAuthentication == null ? "" : ", auth=" + mAuthentication)
333                     + "]";
334         }
335 
336         public static class Builder {
337             private final Map<AutofillId, AutofillValue> mFieldValuesById = new ArrayMap<>();
338             private final Map<AutofillId, InlinePresentation> mFieldPresentationById =
339                     new ArrayMap<>();
340 
341             private final String mPresentation;
342             private AutofillValue mOnlyFieldValue;
343             private AutofillId mFieldIdForContent;
344             private ClipData mFieldContent;
345             private IntentSender mAuthentication;
346 
Builder(@onNull String presentation)347             public Builder(@NonNull String presentation) {
348                 mPresentation = Objects.requireNonNull(presentation);
349             }
350 
351             /**
352              * Sets the value that will be autofilled on the field with {@code id}.
353              */
setField(@onNull AutofillId id, @NonNull String text)354             public Builder setField(@NonNull AutofillId id, @NonNull String text) {
355                 if (mOnlyFieldValue != null || mFieldIdForContent != null) {
356                     throw new IllegalStateException(
357                             "already called setOnlyField() or setContent()");
358                 }
359                 mFieldValuesById.put(id, AutofillValue.forText(text));
360                 return this;
361             }
362 
363             /**
364              * Sets the value that will be autofilled on the field with {@code id}.
365              */
setField(@onNull AutofillId id, @NonNull String text, @NonNull InlinePresentation presentation)366             public Builder setField(@NonNull AutofillId id, @NonNull String text,
367                     @NonNull InlinePresentation presentation) {
368                 if (mOnlyFieldValue != null || mFieldIdForContent != null) {
369                     throw new IllegalStateException(
370                             "already called setOnlyField() or setContent()");
371                 }
372                 mFieldValuesById.put(id, AutofillValue.forText(text));
373                 mFieldPresentationById.put(id, presentation);
374                 return this;
375             }
376 
377             /**
378              * Sets this dataset to return the given {@code text} for the focused field.
379              *
380              * <p>Used when the test case doesn't know the autofill id of the focused field.
381              */
setOnlyField(@onNull String text)382             public Builder setOnlyField(@NonNull String text) {
383                 if (!mFieldValuesById.isEmpty() || mFieldIdForContent != null) {
384                     throw new IllegalStateException("already called setField() or setContent()");
385                 }
386                 mOnlyFieldValue = AutofillValue.forText(text);
387                 return this;
388             }
389 
390             /**
391              * Sets the content that will be autofilled on the field with {@code id}.
392              *
393              * <p>The {@link ClipDescription#getLabel() label} of the passed-in {@link ClipData}
394              * will be used as the chip title (the text displayed in the inline suggestion chip).
395              *
396              * <p>For a given field, either a {@link AutofillValue value} or content can be filled,
397              * but not both. Furthermore, when filling content, only a single field can be filled.
398              */
399             @NonNull
setContent(@onNull AutofillId id, @Nullable ClipData content)400             public Builder setContent(@NonNull AutofillId id, @Nullable ClipData content) {
401                 if (!mFieldValuesById.isEmpty() || mOnlyFieldValue != null) {
402                     throw new IllegalStateException("already called setField() or setOnlyField()");
403                 }
404                 mFieldIdForContent = id;
405                 mFieldContent = content;
406                 return this;
407             }
408 
409             /**
410              * Sets the authentication intent for this dataset.
411              */
setAuthentication(IntentSender authentication)412             public Builder setAuthentication(IntentSender authentication) {
413                 mAuthentication = authentication;
414                 return this;
415             }
416 
build()417             public Dataset build() {
418                 return new Dataset(this);
419             }
420         } // Dataset.Builder
421     } // Dataset
422 } // CannedAugmentedFillResponse
423