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