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 17 package android.autofillservice.cts; 18 19 import static android.autofillservice.cts.Helper.NOT_SHOWING_TIMEOUT_MS; 20 import static android.autofillservice.cts.Helper.SAVE_TIMEOUT_MS; 21 import static android.autofillservice.cts.Helper.UI_TIMEOUT_MS; 22 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS; 23 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD; 24 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS; 25 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC; 26 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD; 27 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME; 28 29 import static com.google.common.truth.Truth.assertThat; 30 import static com.google.common.truth.Truth.assertWithMessage; 31 32 import android.app.Instrumentation; 33 import android.app.UiAutomation; 34 import android.content.res.Resources; 35 import android.os.SystemClock; 36 import android.service.autofill.SaveInfo; 37 import android.support.test.InstrumentationRegistry; 38 import android.support.test.uiautomator.By; 39 import android.support.test.uiautomator.BySelector; 40 import android.support.test.uiautomator.UiDevice; 41 import android.support.test.uiautomator.UiObject2; 42 import android.text.Html; 43 import android.util.Log; 44 import android.view.accessibility.AccessibilityWindowInfo; 45 46 import java.util.ArrayList; 47 import java.util.Arrays; 48 import java.util.List; 49 50 /** 51 * Helper for UI-related needs. 52 */ 53 final class UiBot { 54 55 private static final String RESOURCE_ID_DATASET_PICKER = "autofill_dataset_picker"; 56 private static final String RESOURCE_ID_SAVE_SNACKBAR = "autofill_save"; 57 private static final String RESOURCE_ID_SAVE_TITLE = "autofill_save_title"; 58 private static final String RESOURCE_ID_CONTEXT_MENUITEM = "floating_toolbar_menu_item_text"; 59 60 private static final String RESOURCE_STRING_SAVE_TITLE = "autofill_save_title"; 61 private static final String RESOURCE_STRING_SAVE_TITLE_WITH_TYPE = 62 "autofill_save_title_with_type"; 63 private static final String RESOURCE_STRING_SAVE_TYPE_PASSWORD = "autofill_save_type_password"; 64 private static final String RESOURCE_STRING_SAVE_TYPE_ADDRESS = "autofill_save_type_address"; 65 private static final String RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD = 66 "autofill_save_type_credit_card"; 67 private static final String RESOURCE_STRING_SAVE_TYPE_USERNAME = "autofill_save_type_username"; 68 private static final String RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS = 69 "autofill_save_type_email_address"; 70 private static final String RESOURCE_STRING_AUTOFILL = "autofill"; 71 private static final String RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE = 72 "autofill_picker_accessibility_title"; 73 private static final String RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE = 74 "autofill_save_accessibility_title"; 75 76 private static final String TAG = "AutoFillCtsUiBot"; 77 78 private final UiDevice mDevice; 79 private final String mPackageName; 80 private final UiAutomation mAutoman; 81 UiBot(Instrumentation instrumentation)82 UiBot(Instrumentation instrumentation) throws Exception { 83 mDevice = UiDevice.getInstance(instrumentation); 84 mPackageName = instrumentation.getContext().getPackageName(); 85 mAutoman = instrumentation.getUiAutomation(); 86 } 87 88 /** 89 * Asserts the dataset chooser is not shown. 90 */ assertNoDatasets()91 void assertNoDatasets() { 92 final UiObject2 picker; 93 try { 94 picker = findDatasetPicker(NOT_SHOWING_TIMEOUT_MS); 95 } catch (Throwable t) { 96 // Use a more elegant check than catching the expection because it's not showing... 97 return; 98 } 99 throw new RetryableException( 100 "Should not be showing datasets, but got " + getChildrenAsText(picker)); 101 } 102 103 /** 104 * Asserts the dataset chooser is shown and contains the given datasets. 105 * 106 * @return the dataset picker object. 107 */ assertDatasets(String...names)108 UiObject2 assertDatasets(String...names) { 109 final UiObject2 picker = findDatasetPicker(); 110 assertWithMessage("wrong dataset names").that(getChildrenAsText(picker)) 111 .containsExactlyElementsIn(Arrays.asList(names)); 112 return picker; 113 } 114 115 /** 116 * Gets the text of this object children. 117 */ getChildrenAsText(UiObject2 object)118 List<String> getChildrenAsText(UiObject2 object) { 119 final List<String> list = new ArrayList<>(); 120 getChildrenAsText(object, list); 121 return list; 122 } 123 getChildrenAsText(UiObject2 object, List<String> children)124 private static void getChildrenAsText(UiObject2 object, List<String> children) { 125 final String text = object.getText(); 126 if (text != null) { 127 children.add(text); 128 } 129 for (UiObject2 child : object.getChildren()) { 130 getChildrenAsText(child, children); 131 } 132 } 133 134 /** 135 * Selects a dataset that should be visible in the floating UI. 136 */ selectDataset(String name)137 void selectDataset(String name) { 138 final UiObject2 picker = findDatasetPicker(); 139 selectDataset(picker, name); 140 } 141 142 /** 143 * Selects a dataset that should be visible in the floating UI. 144 */ selectDataset(UiObject2 picker, String name)145 void selectDataset(UiObject2 picker, String name) { 146 final UiObject2 dataset = picker.findObject(By.text(name)); 147 if (dataset == null) { 148 throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(picker)); 149 } 150 dataset.click(); 151 } 152 153 /** 154 * Selects a view by text. 155 * 156 * <p><b>NOTE:</b> when selecting an option in dataset picker is shown, prefer 157 * {@link #selectDataset(String)}. 158 */ selectByText(String name)159 void selectByText(String name) { 160 Log.v(TAG, "selectByText(): " + name); 161 162 final UiObject2 object = waitForObject(By.text(name)); 163 object.click(); 164 } 165 166 /** 167 * Asserts a text is shown. 168 * 169 * <p><b>NOTE:</b> when asserting the dataset picker is shown, prefer 170 * {@link #assertDatasets(String...)}. 171 */ assertShownByText(String text)172 public UiObject2 assertShownByText(String text) { 173 final UiObject2 object = waitForObject(By.text(text)); 174 assertWithMessage(text).that(object).isNotNull(); 175 return object; 176 } 177 178 /** 179 * Checks if a View with a certain text exists. 180 */ hasViewWithText(String name)181 boolean hasViewWithText(String name) { 182 Log.v(TAG, "hasViewWithText(): " + name); 183 184 return mDevice.findObject(By.text(name)) != null; 185 } 186 187 /** 188 * Selects a view by id. 189 */ selectById(String id)190 void selectById(String id) { 191 Log.v(TAG, "selectById(): " + id); 192 193 final UiObject2 view = waitForObject(By.res(id)); 194 view.click(); 195 } 196 197 /** 198 * Asserts the id is shown on the screen. 199 */ assertShownById(String id)200 void assertShownById(String id) { 201 assertThat(waitForObject(By.res(id))).isNotNull(); 202 } 203 204 /** 205 * Gets the text set on a view. 206 */ getTextById(String id)207 String getTextById(String id) { 208 final UiObject2 obj = waitForObject(By.res(id)); 209 return obj.getText(); 210 } 211 212 /** 213 * Focus in the view with the given resource id. 214 */ focusByRelativeId(String id)215 void focusByRelativeId(String id) { 216 waitForObject(By.res(mPackageName, id)).click(); 217 } 218 219 /** 220 * Sets a new text on a view. 221 */ setTextById(String id, String newText)222 void setTextById(String id, String newText) { 223 UiObject2 view = waitForObject(By.res(id)); 224 view.setText(newText); 225 } 226 227 /** 228 * Asserts the save snackbar is showing and returns it. 229 */ assertSaveShowing(int type)230 UiObject2 assertSaveShowing(int type) { 231 return assertSaveShowing(SAVE_TIMEOUT_MS, type); 232 } 233 234 /** 235 * Asserts the save snackbar is showing and returns it. 236 */ assertSaveShowing(long timeout, int type)237 UiObject2 assertSaveShowing(long timeout, int type) { 238 return assertSaveShowing(null, timeout, type); 239 } 240 241 /** 242 * Presses the back button. 243 */ pressBack()244 void pressBack() { 245 Log.d(TAG, "pressBack()"); 246 mDevice.pressBack(); 247 } 248 249 /** 250 * Presses the home button. 251 */ pressHome()252 void pressHome() { 253 Log.d(TAG, "pressHome()"); 254 mDevice.pressHome(); 255 } 256 /** 257 * Asserts the save snackbar is not showing and returns it. 258 */ assertSaveNotShowing(int type)259 void assertSaveNotShowing(int type) { 260 try { 261 assertSaveShowing(NOT_SHOWING_TIMEOUT_MS, type); 262 } catch (Throwable t) { 263 // TODO: use a more elegant check than catching the expection because it's not showing 264 // (in which case it wouldn't need a type as parameter). 265 return; 266 } 267 throw new RetryableException("snack bar is showing"); 268 } 269 getSaveTypeString(int type)270 private String getSaveTypeString(int type) { 271 final String typeResourceName; 272 switch (type) { 273 case SAVE_DATA_TYPE_PASSWORD: 274 typeResourceName = RESOURCE_STRING_SAVE_TYPE_PASSWORD; 275 break; 276 case SAVE_DATA_TYPE_ADDRESS: 277 typeResourceName = RESOURCE_STRING_SAVE_TYPE_ADDRESS; 278 break; 279 case SAVE_DATA_TYPE_CREDIT_CARD: 280 typeResourceName = RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD; 281 break; 282 case SAVE_DATA_TYPE_USERNAME: 283 typeResourceName = RESOURCE_STRING_SAVE_TYPE_USERNAME; 284 break; 285 case SAVE_DATA_TYPE_EMAIL_ADDRESS: 286 typeResourceName = RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS; 287 break; 288 default: 289 throw new IllegalArgumentException("Unsupported type: " + type); 290 } 291 return getString(typeResourceName); 292 } 293 assertSaveShowing(String description, int... types)294 UiObject2 assertSaveShowing(String description, int... types) { 295 return assertSaveShowing(SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, description, 296 SAVE_TIMEOUT_MS, types); 297 } 298 assertSaveShowing(String description, long timeout, int... types)299 UiObject2 assertSaveShowing(String description, long timeout, int... types) { 300 return assertSaveShowing(SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, description, timeout, 301 types); 302 } 303 assertSaveShowing(int negativeButtonStyle, String description, int... types)304 UiObject2 assertSaveShowing(int negativeButtonStyle, String description, 305 int... types) { 306 return assertSaveShowing(negativeButtonStyle, description, SAVE_TIMEOUT_MS, types); 307 } 308 assertSaveShowing(int negativeButtonStyle, String description, long timeout, int... types)309 UiObject2 assertSaveShowing(int negativeButtonStyle, String description, long timeout, 310 int... types) { 311 final UiObject2 snackbar = waitForObject(By.res("android", RESOURCE_ID_SAVE_SNACKBAR), 312 timeout); 313 314 final UiObject2 titleView = snackbar.findObject(By.res("android", RESOURCE_ID_SAVE_TITLE)); 315 assertWithMessage("save title (%s)", RESOURCE_ID_SAVE_TITLE).that(titleView).isNotNull(); 316 317 final String actualTitle = titleView.getText(); 318 Log.d(TAG, "save title: " + actualTitle); 319 320 final String serviceLabel = InstrumentedAutoFillService.class.getSimpleName(); 321 switch (types.length) { 322 case 1: 323 final String expectedTitle = (types[0] == SAVE_DATA_TYPE_GENERIC) 324 ? Html.fromHtml(getString(RESOURCE_STRING_SAVE_TITLE, 325 serviceLabel), 0).toString() 326 : Html.fromHtml(getString(RESOURCE_STRING_SAVE_TITLE_WITH_TYPE, 327 getSaveTypeString(types[0]), serviceLabel), 0).toString(); 328 assertThat(actualTitle).isEqualTo(expectedTitle); 329 break; 330 case 2: 331 // We cannot predict the order... 332 assertThat(actualTitle).contains(getSaveTypeString(types[0])); 333 assertThat(actualTitle).contains(getSaveTypeString(types[1])); 334 break; 335 case 3: 336 // We cannot predict the order... 337 assertThat(actualTitle).contains(getSaveTypeString(types[0])); 338 assertThat(actualTitle).contains(getSaveTypeString(types[1])); 339 assertThat(actualTitle).contains(getSaveTypeString(types[2])); 340 break; 341 default: 342 throw new IllegalArgumentException("Invalid types: " + Arrays.toString(types)); 343 } 344 345 if (description != null) { 346 final UiObject2 saveSubTitle = snackbar.findObject(By.text(description)); 347 assertWithMessage("save subtitle(%s)", description).that(saveSubTitle).isNotNull(); 348 } 349 350 final String negativeButtonText = (negativeButtonStyle 351 == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) ? "NOT NOW" : "NO THANKS"; 352 UiObject2 negativeButton = snackbar.findObject(By.text(negativeButtonText)); 353 assertWithMessage("negative button (%s)", negativeButtonText) 354 .that(negativeButton).isNotNull(); 355 356 final String expectedAccessibilityTitle = 357 getString(RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE); 358 assertAccessibilityTitle(snackbar, expectedAccessibilityTitle); 359 360 return snackbar; 361 } 362 363 /** 364 * Taps an option in the save snackbar. 365 * 366 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 367 * @param types expected types of save info. 368 */ saveForAutofill(boolean yesDoIt, int... types)369 void saveForAutofill(boolean yesDoIt, int... types) { 370 final UiObject2 saveSnackBar = assertSaveShowing( 371 SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, types); 372 saveForAutofill(saveSnackBar, yesDoIt); 373 } 374 375 /** 376 * Taps an option in the save snackbar. 377 * 378 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 379 * @param types expected types of save info. 380 */ saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types)381 void saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types) { 382 final UiObject2 saveSnackBar = assertSaveShowing(negativeButtonStyle,null, types); 383 saveForAutofill(saveSnackBar, yesDoIt); 384 } 385 386 /** 387 * Taps an option in the save snackbar. 388 * 389 * @param saveSnackBar Save snackbar, typically obtained through 390 * {@link #assertSaveShowing(int)}. 391 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 392 */ saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt)393 void saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt) { 394 final String id = yesDoIt ? "autofill_save_yes" : "autofill_save_no"; 395 396 final UiObject2 button = saveSnackBar.findObject(By.res("android", id)); 397 assertWithMessage("save button (%s)", id).that(button).isNotNull(); 398 button.click(); 399 } 400 401 /** 402 * Gets the AUTOFILL contextual menu by long pressing a text field. 403 * 404 * <p><b>NOTE:</b> this method should only be called in scenarios where we explicitly want to 405 * test the overflow menu. For all other scenarios where we want to test manual autofill, it's 406 * better to call {@code AFM.requestAutofill()} directly, because it's less error-prone and 407 * faster. 408 * 409 * @param id resource id of the field. 410 */ getAutofillMenuOption(String id)411 UiObject2 getAutofillMenuOption(String id) { 412 final UiObject2 field = waitForObject(By.res(mPackageName, id)); 413 // TODO: figure out why obj.longClick() doesn't always work 414 field.click(3000); 415 416 final List<UiObject2> menuItems = waitForObjects( 417 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM)); 418 final String expectedText = getString(RESOURCE_STRING_AUTOFILL); 419 final StringBuffer menuNames = new StringBuffer(); 420 for (UiObject2 menuItem : menuItems) { 421 final String menuName = menuItem.getText(); 422 if (menuName.equalsIgnoreCase(expectedText)) { 423 return menuItem; 424 } 425 menuNames.append("'").append(menuName).append("' "); 426 } 427 throw new RetryableException("no '%s' on '%s'", expectedText, menuNames); 428 } 429 430 /** 431 * Gets a string from the Android resources. 432 */ getString(String id)433 private String getString(String id) { 434 final Resources resources = InstrumentationRegistry.getContext().getResources(); 435 final int stringId = resources.getIdentifier(id, "string", "android"); 436 return resources.getString(stringId); 437 } 438 439 /** 440 * Gets a string from the Android resources. 441 */ getString(String id, Object... formatArgs)442 private String getString(String id, Object... formatArgs) { 443 final Resources resources = InstrumentationRegistry.getContext().getResources(); 444 final int stringId = resources.getIdentifier(id, "string", "android"); 445 return resources.getString(stringId, formatArgs); 446 } 447 448 /** 449 * Waits for and returns an object. 450 * 451 * @param selector {@link BySelector} that identifies the object. 452 */ waitForObject(BySelector selector)453 private UiObject2 waitForObject(BySelector selector) { 454 return waitForObject(selector, UI_TIMEOUT_MS); 455 } 456 457 /** 458 * Waits for and returns an object. 459 * 460 * @param selector {@link BySelector} that identifies the object. 461 * @param timeout timeout in ms 462 */ waitForObject(BySelector selector, long timeout)463 private UiObject2 waitForObject(BySelector selector, long timeout) { 464 // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach. 465 final int maxTries = 5; 466 final long napTime = timeout / maxTries; 467 for (int i = 1; i <= maxTries; i++) { 468 final UiObject2 uiObject = mDevice.findObject(selector); 469 if (uiObject != null) { 470 return uiObject; 471 } 472 SystemClock.sleep(napTime); 473 } 474 throw new RetryableException("Object with selector '%s' not found in %d ms", 475 selector, UI_TIMEOUT_MS); 476 } 477 478 /** 479 * Waits for and returns a list of objects. 480 * 481 * @param selector {@link BySelector} that identifies the object. 482 */ waitForObjects(BySelector selector)483 private List<UiObject2> waitForObjects(BySelector selector) { 484 return waitForObjects(selector, UI_TIMEOUT_MS); 485 } 486 487 /** 488 * Waits for and returns a list of objects. 489 * 490 * @param selector {@link BySelector} that identifies the object. 491 * @param timeout timeout in ms 492 */ waitForObjects(BySelector selector, long timeout)493 private List<UiObject2> waitForObjects(BySelector selector, long timeout) { 494 // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach. 495 final int maxTries = 5; 496 final long napTime = timeout / maxTries; 497 for (int i = 1; i <= maxTries; i++) { 498 final List<UiObject2> uiObjects = mDevice.findObjects(selector); 499 if (uiObjects != null && !uiObjects.isEmpty()) { 500 return uiObjects; 501 } 502 SystemClock.sleep(napTime); 503 } 504 throw new RetryableException("Objects with selector '%s' not found in %d ms", 505 selector, UI_TIMEOUT_MS); 506 } 507 findDatasetPicker()508 private UiObject2 findDatasetPicker() { 509 return findDatasetPicker(UI_TIMEOUT_MS); 510 } 511 findDatasetPicker(long timeout)512 private UiObject2 findDatasetPicker(long timeout) { 513 final UiObject2 picker = waitForObject(By.res("android", RESOURCE_ID_DATASET_PICKER), 514 timeout); 515 516 final String expectedTitle = getString(RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE); 517 assertAccessibilityTitle(picker, expectedTitle); 518 519 return picker; 520 } 521 522 /** 523 * Asserts a given object has the expected accessibility title. 524 */ assertAccessibilityTitle(UiObject2 object, String expectedTitle)525 private void assertAccessibilityTitle(UiObject2 object, String expectedTitle) { 526 // TODO: ideally it should get the AccessibilityWindowInfo from the object, but UiAutomator 527 // does not expose that. 528 for (AccessibilityWindowInfo window : mAutoman.getWindows()) { 529 final CharSequence title = window.getTitle(); 530 if (title != null && title.toString().equals(expectedTitle)) { 531 return; 532 } 533 } 534 throw new RetryableException("Title '%s' not found for %s", expectedTitle, object); 535 } 536 } 537