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