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.augmented;
17 
18 import static android.autofillservice.cts.augmented.AugmentedHelper.getContentDescriptionForUi;
19 
20 import android.autofillservice.cts.R;
21 import android.content.Context;
22 import android.service.autofill.augmented.FillCallback;
23 import android.service.autofill.augmented.FillController;
24 import android.service.autofill.augmented.FillRequest;
25 import android.service.autofill.augmented.FillResponse;
26 import android.service.autofill.augmented.FillWindow;
27 import android.service.autofill.augmented.PresentationParams;
28 import android.service.autofill.augmented.PresentationParams.Area;
29 import android.util.ArrayMap;
30 import android.util.Log;
31 import android.util.Pair;
32 import android.view.LayoutInflater;
33 import android.view.autofill.AutofillId;
34 import android.view.autofill.AutofillValue;
35 import android.widget.TextView;
36 
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 
40 import com.google.common.base.Preconditions;
41 
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.stream.Collectors;
46 
47 /**
48  * Helper class used to produce a {@link FillResponse}.
49  */
50 public final class CannedAugmentedFillResponse {
51 
52     private static final String TAG = CannedAugmentedFillResponse.class.getSimpleName();
53 
54     private final AugmentedResponseType mResponseType;
55     private final Map<AutofillId, Dataset> mDatasets;
56     private long mDelay;
57     private final Dataset mOnlyDataset;
58 
CannedAugmentedFillResponse(@onNull Builder builder)59     private CannedAugmentedFillResponse(@NonNull Builder builder) {
60         mResponseType = builder.mResponseType;
61         mDatasets = builder.mDatasets;
62         mDelay = builder.mDelay;
63         mOnlyDataset = builder.mOnlyDataset;
64     }
65 
66     /**
67      * Constant used to pass a {@code null} response to the
68      * {@link FillCallback#onSuccess(FillResponse)} method.
69      */
70     public static final CannedAugmentedFillResponse NO_AUGMENTED_RESPONSE =
71             new Builder(AugmentedResponseType.NULL).build();
72 
73     /**
74      * Constant used to emulate a timeout by not calling any method on {@link FillCallback}.
75      */
76     public static final CannedAugmentedFillResponse DO_NOT_REPLY_AUGMENTED_RESPONSE =
77             new Builder(AugmentedResponseType.TIMEOUT).build();
78 
getResponseType()79     public AugmentedResponseType getResponseType() {
80         return mResponseType;
81     }
82 
getDelay()83     public long getDelay() {
84         return mDelay;
85     }
86 
87     /**
88      * Creates the "real" response.
89      */
asFillResponse(@onNull Context context, @NonNull FillRequest request, @NonNull FillController controller)90     public FillResponse asFillResponse(@NonNull Context context, @NonNull FillRequest request,
91             @NonNull FillController controller) {
92         final AutofillId focusedId = request.getFocusedId();
93 
94         final Dataset dataset;
95         if (mOnlyDataset != null) {
96             dataset = mOnlyDataset;
97         } else {
98             dataset = mDatasets.get(focusedId);
99         }
100         if (dataset == null) {
101             Log.d(TAG, "no dataset for field " + focusedId);
102             return null;
103         }
104 
105         Log.d(TAG, "asFillResponse: id=" + focusedId + ", dataset=" + dataset);
106 
107         final PresentationParams presentationParams = request.getPresentationParams();
108         if (presentationParams == null) {
109             Log.w(TAG, "No PresentationParams");
110             return null;
111         }
112 
113         final Area strip = presentationParams.getSuggestionArea();
114         if (strip == null) {
115             Log.w(TAG, "No suggestion strip");
116             return null;
117         }
118 
119         final LayoutInflater inflater = LayoutInflater.from(context);
120         final TextView rootView = (TextView) inflater.inflate(R.layout.augmented_autofill_ui, null);
121 
122         Log.d(TAG, "Setting autofill UI text to:" + dataset.mPresentation);
123         rootView.setText(dataset.mPresentation);
124 
125         rootView.setContentDescription(getContentDescriptionForUi(focusedId));
126         final FillWindow fillWindow = new FillWindow();
127         rootView.setOnClickListener((v) -> {
128             Log.d(TAG, "Destroying window first");
129             fillWindow.destroy();
130             final List<Pair<AutofillId, AutofillValue>> values;
131             final AutofillValue onlyValue = dataset.getOnlyFieldValue();
132             if (onlyValue != null) {
133                 Log.i(TAG, "Autofilling only value for " + focusedId + " as " + onlyValue);
134                 values = new ArrayList<>(1);
135                 values.add(new Pair<AutofillId, AutofillValue>(focusedId, onlyValue));
136             } else {
137                 values = dataset.getValues();
138                 Log.i(TAG, "Autofilling: " + AugmentedHelper.toString(values));
139             }
140             controller.autofill(values);
141         });
142 
143         boolean ok = fillWindow.update(strip, rootView, 0);
144         if (!ok) {
145             Log.w(TAG, "FillWindow.update() failed for " + strip + " and " + rootView);
146             return null;
147         }
148 
149         return new FillResponse.Builder().setFillWindow(fillWindow).build();
150     }
151 
152     @Override
toString()153     public String toString() {
154         return "CannedAugmentedFillResponse: [type=" + mResponseType
155                 + ", onlyDataset=" + mOnlyDataset
156                 + ", datasets=" + mDatasets
157                 + "]";
158     }
159     public enum AugmentedResponseType {
160         NORMAL,
161         NULL,
162         TIMEOUT,
163     }
164 
165     public static final class Builder {
166         private final Map<AutofillId, Dataset> mDatasets = new ArrayMap<>();
167         private final AugmentedResponseType mResponseType;
168         private long mDelay;
169         private Dataset mOnlyDataset;
170 
Builder(@onNull AugmentedResponseType type)171         public Builder(@NonNull AugmentedResponseType type) {
172             mResponseType = type;
173         }
174 
Builder()175         public Builder() {
176             this(AugmentedResponseType.NORMAL);
177         }
178 
179         /**
180          * Sets the {@link Dataset} that will be filled when the given {@code ids} is focused and
181          * the UI is tapped.
182          */
183         @NonNull
setDataset(@onNull Dataset dataset, @NonNull AutofillId... ids)184         public Builder setDataset(@NonNull Dataset dataset, @NonNull AutofillId... ids) {
185             if (mOnlyDataset != null) {
186                 throw new IllegalStateException("already called setOnlyDataset()");
187             }
188             for (AutofillId id : ids) {
189                 mDatasets.put(id, dataset);
190             }
191             return this;
192         }
193 
194         /**
195          * Sets the delay for onFillRequest().
196          */
setDelay(long delay)197         public Builder setDelay(long delay) {
198             mDelay = delay;
199             return this;
200         }
201 
202         /**
203          * Sets the only dataset that will be returned.
204          *
205          * <p>Used when the test case doesn't know the autofill id of the focused field.
206          * @param dataset
207          */
208         @NonNull
setOnlyDataset(@onNull Dataset dataset)209         public Builder setOnlyDataset(@NonNull Dataset dataset) {
210             if (!mDatasets.isEmpty()) {
211                 throw new IllegalStateException("already called setDataset()");
212             }
213             mOnlyDataset = dataset;
214             return this;
215         }
216 
217         @NonNull
build()218         public CannedAugmentedFillResponse build() {
219             return new CannedAugmentedFillResponse(this);
220         }
221     } // CannedAugmentedFillResponse.Builder
222 
223 
224     /**
225      * Helper class used to define which fields will be autofilled when the user taps the Augmented
226      * Autofill UI.
227      */
228     public static class Dataset {
229         private final Map<AutofillId, AutofillValue> mFieldValuesById;
230         private final String mPresentation;
231         private final AutofillValue mOnlyFieldValue;
232 
Dataset(@onNull Builder builder)233         private Dataset(@NonNull Builder builder) {
234             mFieldValuesById = builder.mFieldValuesById;
235             mPresentation = builder.mPresentation;
236             mOnlyFieldValue = builder.mOnlyFieldValue;
237         }
238 
239         @NonNull
getValues()240         public List<Pair<AutofillId, AutofillValue>> getValues() {
241             return mFieldValuesById.entrySet().stream()
242                     .map((entry) -> (new Pair<>(entry.getKey(), entry.getValue())))
243                     .collect(Collectors.toList());
244         }
245 
246         @Nullable
getOnlyFieldValue()247         public AutofillValue getOnlyFieldValue() {
248             return mOnlyFieldValue;
249         }
250 
251         @Override
toString()252         public String toString() {
253             return "Dataset: [presentation=" + mPresentation
254                     + ", onlyField=" + mOnlyFieldValue
255                     + ", fields=" + mFieldValuesById
256                     + "]";
257         }
258 
259         public static class Builder {
260             private final Map<AutofillId, AutofillValue> mFieldValuesById = new ArrayMap<>();
261 
262             private final String mPresentation;
263             private AutofillValue mOnlyFieldValue;
264 
Builder(@onNull String presentation)265             public Builder(@NonNull String presentation) {
266                 mPresentation = Preconditions.checkNotNull(presentation);
267             }
268 
269             /**
270              * Sets the value that will be autofilled on the field with {@code id}.
271              */
setField(@onNull AutofillId id, @NonNull String text)272             public Builder setField(@NonNull AutofillId id, @NonNull String text) {
273                 if (mOnlyFieldValue != null) {
274                     throw new IllegalStateException("already called setOnlyField()");
275                 }
276                 mFieldValuesById.put(id, AutofillValue.forText(text));
277                 return this;
278             }
279 
280             /**
281              * Sets this dataset to return the given {@code text} for the focused field.
282              *
283              * <p>Used when the test case doesn't know the autofill id of the focused field.
284              */
setOnlyField(@onNull String text)285             public Builder setOnlyField(@NonNull String text) {
286                 if (!mFieldValuesById.isEmpty()) {
287                     throw new IllegalStateException("already called setField()");
288                 }
289                 mOnlyFieldValue = AutofillValue.forText(text);
290                 return this;
291             }
292 
293 
build()294             public Dataset build() {
295                 return new Dataset(this);
296             }
297         } // Dataset.Builder
298     } // Dataset
299 } // CannedAugmentedFillResponse
300