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.Timeouts.DATASET_PICKER_NOT_SHOWN_NAPTIME_MS; 20 import static android.autofillservice.cts.Timeouts.SAVE_NOT_SHOWN_NAPTIME_MS; 21 import static android.autofillservice.cts.Timeouts.SAVE_TIMEOUT; 22 import static android.autofillservice.cts.Timeouts.UI_DATASET_PICKER_TIMEOUT; 23 import static android.autofillservice.cts.Timeouts.UI_SCREEN_ORIENTATION_TIMEOUT; 24 import static android.autofillservice.cts.Timeouts.UI_TIMEOUT; 25 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS; 26 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD; 27 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS; 28 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC; 29 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD; 30 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME; 31 32 import static com.google.common.truth.Truth.assertThat; 33 import static com.google.common.truth.Truth.assertWithMessage; 34 35 import android.app.Instrumentation; 36 import android.app.UiAutomation; 37 import android.content.Context; 38 import android.content.res.Resources; 39 import android.graphics.Bitmap; 40 import android.os.SystemClock; 41 import android.service.autofill.SaveInfo; 42 import android.support.test.InstrumentationRegistry; 43 import android.support.test.uiautomator.By; 44 import android.support.test.uiautomator.BySelector; 45 import android.support.test.uiautomator.UiDevice; 46 import android.support.test.uiautomator.UiObject2; 47 import android.support.test.uiautomator.Until; 48 import android.text.Html; 49 import android.util.Log; 50 import android.view.accessibility.AccessibilityEvent; 51 import android.view.accessibility.AccessibilityWindowInfo; 52 53 import androidx.annotation.NonNull; 54 import androidx.annotation.Nullable; 55 56 import java.io.ByteArrayOutputStream; 57 import java.io.IOException; 58 import java.util.ArrayList; 59 import java.util.Arrays; 60 import java.util.List; 61 import java.util.concurrent.TimeoutException; 62 63 /** 64 * Helper for UI-related needs. 65 */ 66 final class UiBot { 67 68 private static final String TAG = "AutoFillCtsUiBot"; 69 70 private static final String RESOURCE_ID_DATASET_PICKER = "autofill_dataset_picker"; 71 private static final String RESOURCE_ID_DATASET_HEADER = "autofill_dataset_header"; 72 private static final String RESOURCE_ID_SAVE_SNACKBAR = "autofill_save"; 73 private static final String RESOURCE_ID_SAVE_ICON = "autofill_save_icon"; 74 private static final String RESOURCE_ID_SAVE_TITLE = "autofill_save_title"; 75 private static final String RESOURCE_ID_CONTEXT_MENUITEM = "floating_toolbar_menu_item_text"; 76 private static final String RESOURCE_ID_SAVE_BUTTON_NO = "autofill_save_no"; 77 78 private static final String RESOURCE_STRING_SAVE_TITLE = "autofill_save_title"; 79 private static final String RESOURCE_STRING_SAVE_TITLE_WITH_TYPE = 80 "autofill_save_title_with_type"; 81 private static final String RESOURCE_STRING_SAVE_TYPE_PASSWORD = "autofill_save_type_password"; 82 private static final String RESOURCE_STRING_SAVE_TYPE_ADDRESS = "autofill_save_type_address"; 83 private static final String RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD = 84 "autofill_save_type_credit_card"; 85 private static final String RESOURCE_STRING_SAVE_TYPE_USERNAME = "autofill_save_type_username"; 86 private static final String RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS = 87 "autofill_save_type_email_address"; 88 private static final String RESOURCE_STRING_SAVE_BUTTON_NOT_NOW = "save_password_notnow"; 89 private static final String RESOURCE_STRING_SAVE_BUTTON_NO_THANKS = "autofill_save_no"; 90 91 private static final String RESOURCE_STRING_AUTOFILL = "autofill"; 92 private static final String RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE = 93 "autofill_picker_accessibility_title"; 94 private static final String RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE = 95 "autofill_save_accessibility_title"; 96 97 static final BySelector DATASET_PICKER_SELECTOR = By.res("android", RESOURCE_ID_DATASET_PICKER); 98 private static final BySelector SAVE_UI_SELECTOR = By.res("android", RESOURCE_ID_SAVE_SNACKBAR); 99 private static final BySelector DATASET_HEADER_SELECTOR = 100 By.res("android", RESOURCE_ID_DATASET_HEADER); 101 102 private static final boolean DUMP_ON_ERROR = true; 103 104 /** Pass to {@link #setScreenOrientation(int)} to change the display to portrait mode */ 105 public static int PORTRAIT = 0; 106 107 /** Pass to {@link #setScreenOrientation(int)} to change the display to landscape mode */ 108 public static int LANDSCAPE = 1; 109 110 private final UiDevice mDevice; 111 private final Context mContext; 112 private final String mPackageName; 113 private final UiAutomation mAutoman; 114 private final Timeout mDefaultTimeout; 115 116 private boolean mOkToCallAssertNoDatasets; 117 UiBot()118 UiBot() { 119 this(UI_TIMEOUT); 120 } 121 UiBot(Timeout defaultTimeout)122 UiBot(Timeout defaultTimeout) { 123 mDefaultTimeout = defaultTimeout; 124 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 125 mDevice = UiDevice.getInstance(instrumentation); 126 mContext = instrumentation.getContext(); 127 mPackageName = mContext.getPackageName(); 128 mAutoman = instrumentation.getUiAutomation(); 129 } 130 reset()131 void reset() { 132 mOkToCallAssertNoDatasets = false; 133 } 134 getDevice()135 UiDevice getDevice() { 136 return mDevice; 137 } 138 139 /** 140 * Asserts the dataset picker is not shown anymore. 141 * 142 * @throws IllegalStateException if called *before* an assertion was made to make sure the 143 * dataset picker is shown - if that's not the case, call 144 * {@link #assertNoDatasetsEver()} instead. 145 */ assertNoDatasets()146 void assertNoDatasets() throws Exception { 147 if (!mOkToCallAssertNoDatasets) { 148 throw new IllegalStateException( 149 "Cannot call assertNoDatasets() without calling assertDatasets first"); 150 } 151 mDevice.wait(Until.gone(DATASET_PICKER_SELECTOR), UI_DATASET_PICKER_TIMEOUT.ms()); 152 mOkToCallAssertNoDatasets = false; 153 } 154 155 /** 156 * Asserts the dataset picker was never shown. 157 * 158 * <p>This method is slower than {@link #assertNoDatasets()} and should only be called in the 159 * cases where the dataset picker was not previous shown. 160 */ assertNoDatasetsEver()161 void assertNoDatasetsEver() throws Exception { 162 assertNeverShown("dataset picker", DATASET_PICKER_SELECTOR, 163 DATASET_PICKER_NOT_SHOWN_NAPTIME_MS); 164 } 165 166 /** 167 * Asserts the dataset chooser is shown and contains exactly the given datasets. 168 * 169 * @return the dataset picker object. 170 */ assertDatasets(String...names)171 UiObject2 assertDatasets(String...names) throws Exception { 172 // TODO: change run() so it can rethrow the original message 173 return UI_DATASET_PICKER_TIMEOUT.run("assertDatasets: " + Arrays.toString(names), () -> { 174 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 175 try { 176 // TODO: use a library to check it contains, instead of asserThat + catch exception 177 assertWithMessage("wrong dataset names").that(getChildrenAsText(picker)) 178 .containsExactlyElementsIn(Arrays.asList(names)).inOrder(); 179 return picker; 180 } catch (AssertionError e) { 181 // Value mismatch - most likely UI didn't change yet, try again 182 Log.w(TAG, "datasets don't match yet: " + e.getMessage()); 183 return null; 184 } 185 }); 186 } 187 188 /** 189 * Asserts the dataset chooser is shown and contains the given datasets. 190 * 191 * @return the dataset picker object. 192 */ assertDatasetsContains(String...names)193 UiObject2 assertDatasetsContains(String...names) throws Exception { 194 // TODO: change run() so it can rethrow the original message 195 return UI_DATASET_PICKER_TIMEOUT.run("assertDatasets: " + Arrays.toString(names), () -> { 196 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 197 try { 198 // TODO: use a library to check it contains, instead of asserThat + catch exception 199 assertWithMessage("wrong dataset names").that(getChildrenAsText(picker)) 200 .containsAllIn(Arrays.asList(names)).inOrder(); 201 return picker; 202 } catch (AssertionError e) { 203 // Value mismatch - most likely UI didn't change yet, try again 204 Log.w(TAG, "datasets don't match yet: " + e.getMessage()); 205 return null; 206 } 207 }); 208 } 209 210 /** 211 * Asserts the dataset chooser is shown and contains the given datasets, header, and footer. 212 * <p>In fullscreen, header view is not under R.id.autofill_dataset_picker. 213 * 214 * @return the dataset picker object. 215 */ 216 UiObject2 assertDatasetsWithBorders(String header, String footer, String...names) 217 throws Exception { 218 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 219 final List<String> expectedChild = new ArrayList<>(); 220 if (header != null) { 221 if (Helper.isAutofillWindowFullScreen(mContext)) { 222 final UiObject2 headerView = waitForObject(DATASET_HEADER_SELECTOR, 223 UI_DATASET_PICKER_TIMEOUT); 224 assertWithMessage("fullscreen wrong dataset header") 225 .that(getChildrenAsText(headerView)) 226 .containsExactlyElementsIn(Arrays.asList(header)).inOrder(); 227 } else { 228 expectedChild.add(header); 229 } 230 } 231 expectedChild.addAll(Arrays.asList(names)); 232 if (footer != null) { 233 expectedChild.add(footer); 234 } 235 assertWithMessage("wrong elements on dataset picker").that(getChildrenAsText(picker)) 236 .containsExactlyElementsIn(expectedChild).inOrder(); 237 return picker; 238 } 239 240 /** 241 * Gets the text of this object children. 242 */ 243 List<String> getChildrenAsText(UiObject2 object) { 244 final List<String> list = new ArrayList<>(); 245 getChildrenAsText(object, list); 246 return list; 247 } 248 249 private static void getChildrenAsText(UiObject2 object, List<String> children) { 250 final String text = object.getText(); 251 if (text != null) { 252 children.add(text); 253 } 254 for (UiObject2 child : object.getChildren()) { 255 getChildrenAsText(child, children); 256 } 257 } 258 259 /** 260 * Selects a dataset that should be visible in the floating UI. 261 */ 262 void selectDataset(String name) throws Exception { 263 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 264 selectDataset(picker, name); 265 } 266 267 /** 268 * Selects a dataset that should be visible in the floating UI. 269 */ 270 void selectDataset(UiObject2 picker, String name) { 271 final UiObject2 dataset = picker.findObject(By.text(name)); 272 if (dataset == null) { 273 throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(picker)); 274 } 275 dataset.click(); 276 } 277 278 /** 279 * Selects a view by text. 280 * 281 * <p><b>NOTE:</b> when selecting an option in dataset picker is shown, prefer 282 * {@link #selectDataset(String)}. 283 */ 284 void selectByText(String name) throws Exception { 285 Log.v(TAG, "selectByText(): " + name); 286 287 final UiObject2 object = waitForObject(By.text(name)); 288 object.click(); 289 } 290 291 /** 292 * Asserts a text is shown. 293 * 294 * <p><b>NOTE:</b> when asserting the dataset picker is shown, prefer 295 * {@link #assertDatasets(String...)}. 296 */ 297 public UiObject2 assertShownByText(String text) throws Exception { 298 return assertShownByText(text, mDefaultTimeout); 299 } 300 301 public UiObject2 assertShownByText(String text, Timeout timeout) throws Exception { 302 final UiObject2 object = waitForObject(By.text(text), timeout); 303 assertWithMessage("No node with text '%s'", text).that(object).isNotNull(); 304 return object; 305 } 306 307 /** 308 * Asserts that the text is not showing for sure in the screen "as is", i.e., without waiting 309 * for it. 310 * 311 * <p>Typically called after another assertion that waits for a condition to be shown. 312 */ 313 public void assertNotShowingForSure(String text) throws Exception { 314 final UiObject2 object = mDevice.findObject(By.text(text)); 315 assertWithMessage("Find node with text '%s'", text).that(object).isNull(); 316 } 317 318 /** 319 * Asserts a node with the given content description is shown. 320 * 321 */ 322 public UiObject2 assertShownByContentDescription(String contentDescription) throws Exception { 323 final UiObject2 object = waitForObject(By.desc(contentDescription)); 324 assertWithMessage("No node with content description '%s'", contentDescription).that(object) 325 .isNotNull(); 326 return object; 327 } 328 329 /** 330 * Checks if a View with a certain text exists. 331 */ 332 boolean hasViewWithText(String name) { 333 Log.v(TAG, "hasViewWithText(): " + name); 334 335 return mDevice.findObject(By.text(name)) != null; 336 } 337 338 /** 339 * Selects a view by id. 340 */ 341 UiObject2 selectByRelativeId(String id) throws Exception { 342 Log.v(TAG, "selectByRelativeId(): " + id); 343 UiObject2 object = waitForObject(By.res(mPackageName, id)); 344 object.click(); 345 return object; 346 } 347 348 /** 349 * Asserts the id is shown on the screen. 350 */ 351 UiObject2 assertShownById(String id) throws Exception { 352 final UiObject2 object = waitForObject(By.res(id)); 353 assertThat(object).isNotNull(); 354 return object; 355 } 356 357 /** 358 * Asserts the id is shown on the screen, using a resource id from the test package. 359 */ 360 UiObject2 assertShownByRelativeId(String id) throws Exception { 361 return assertShownByRelativeId(id, mDefaultTimeout); 362 } 363 364 UiObject2 assertShownByRelativeId(String id, Timeout timeout) throws Exception { 365 final UiObject2 obj = waitForObject(By.res(mPackageName, id), timeout); 366 assertThat(obj).isNotNull(); 367 return obj; 368 } 369 370 /** 371 * Asserts the id is not shown on the screen anymore, using a resource id from the test package. 372 * 373 * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise 374 * it might pass without really asserting anything. 375 */ 376 void assertGoneByRelativeId(String id, Timeout timeout) { 377 boolean gone = mDevice.wait(Until.gone(By.res(mPackageName, id)), timeout.ms()); 378 if (!gone) { 379 final String message = "Object with id '" + id + "' should be gone after " 380 + timeout + " ms"; 381 dumpScreen(message); 382 throw new RetryableException(message); 383 } 384 } 385 386 /** 387 * Asserts that a {@code selector} is not showing after {@code timeout} milliseconds. 388 */ 389 private void assertNeverShown(String description, BySelector selector, long timeout) 390 throws Exception { 391 SystemClock.sleep(timeout); 392 final UiObject2 object = mDevice.findObject(selector); 393 if (object != null) { 394 throw new AssertionError( 395 String.format("Should not be showing %s after %dms, but got %s", 396 description, timeout, getChildrenAsText(object))); 397 } 398 } 399 400 /** 401 * Gets the text set on a view. 402 */ 403 String getTextByRelativeId(String id) throws Exception { 404 return waitForObject(By.res(mPackageName, id)).getText(); 405 } 406 407 /** 408 * Focus in the view with the given resource id. 409 */ 410 void focusByRelativeId(String id) throws Exception { 411 waitForObject(By.res(mPackageName, id)).click(); 412 } 413 414 /** 415 * Sets a new text on a view. 416 */ 417 void setTextByRelativeId(String id, String newText) throws Exception { 418 waitForObject(By.res(mPackageName, id)).setText(newText); 419 } 420 421 /** 422 * Asserts the save snackbar is showing and returns it. 423 */ 424 UiObject2 assertSaveShowing(int type) throws Exception { 425 return assertSaveShowing(SAVE_TIMEOUT, type); 426 } 427 428 /** 429 * Asserts the save snackbar is showing and returns it. 430 */ 431 UiObject2 assertSaveShowing(Timeout timeout, int type) throws Exception { 432 return assertSaveShowing(null, timeout, type); 433 } 434 435 /** 436 * Presses the Back button. 437 */ 438 void pressBack() { 439 Log.d(TAG, "pressBack()"); 440 mDevice.pressBack(); 441 } 442 443 /** 444 * Presses the Home button. 445 */ 446 void pressHome() { 447 Log.d(TAG, "pressHome()"); 448 mDevice.pressHome(); 449 } 450 451 /** 452 * Asserts the save snackbar is not showing. 453 */ 454 void assertSaveNotShowing(int type) throws Exception { 455 assertNeverShown("save UI for type " + type, SAVE_UI_SELECTOR, SAVE_NOT_SHOWN_NAPTIME_MS); 456 } 457 458 private String getSaveTypeString(int type) { 459 final String typeResourceName; 460 switch (type) { 461 case SAVE_DATA_TYPE_PASSWORD: 462 typeResourceName = RESOURCE_STRING_SAVE_TYPE_PASSWORD; 463 break; 464 case SAVE_DATA_TYPE_ADDRESS: 465 typeResourceName = RESOURCE_STRING_SAVE_TYPE_ADDRESS; 466 break; 467 case SAVE_DATA_TYPE_CREDIT_CARD: 468 typeResourceName = RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD; 469 break; 470 case SAVE_DATA_TYPE_USERNAME: 471 typeResourceName = RESOURCE_STRING_SAVE_TYPE_USERNAME; 472 break; 473 case SAVE_DATA_TYPE_EMAIL_ADDRESS: 474 typeResourceName = RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS; 475 break; 476 default: 477 throw new IllegalArgumentException("Unsupported type: " + type); 478 } 479 return getString(typeResourceName); 480 } 481 482 UiObject2 assertSaveShowing(String description, int... types) throws Exception { 483 return assertSaveShowing(SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, description, 484 SAVE_TIMEOUT, types); 485 } 486 487 UiObject2 assertSaveShowing(String description, Timeout timeout, int... types) 488 throws Exception { 489 return assertSaveShowing(SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, description, timeout, 490 types); 491 } 492 493 UiObject2 assertSaveShowing(int negativeButtonStyle, String description, 494 int... types) throws Exception { 495 return assertSaveShowing(negativeButtonStyle, description, SAVE_TIMEOUT, types); 496 } 497 498 UiObject2 assertSaveShowing(int negativeButtonStyle, String description, Timeout timeout, 499 int... types) throws Exception { 500 final UiObject2 snackbar = waitForObject(SAVE_UI_SELECTOR, timeout); 501 502 final UiObject2 titleView = 503 waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_TITLE), timeout); 504 assertWithMessage("save title (%s) is not shown", RESOURCE_ID_SAVE_TITLE).that(titleView) 505 .isNotNull(); 506 507 final UiObject2 iconView = 508 waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_ICON), timeout); 509 assertWithMessage("save icon (%s) is not shown", RESOURCE_ID_SAVE_ICON).that(iconView) 510 .isNotNull(); 511 512 final String actualTitle = titleView.getText(); 513 Log.d(TAG, "save title: " + actualTitle); 514 515 final String serviceLabel = InstrumentedAutoFillService.getServiceLabel(); 516 switch (types.length) { 517 case 1: 518 final String expectedTitle = (types[0] == SAVE_DATA_TYPE_GENERIC) 519 ? Html.fromHtml(getString(RESOURCE_STRING_SAVE_TITLE, 520 serviceLabel), 0).toString() 521 : Html.fromHtml(getString(RESOURCE_STRING_SAVE_TITLE_WITH_TYPE, 522 getSaveTypeString(types[0]), serviceLabel), 0).toString(); 523 assertThat(actualTitle).isEqualTo(expectedTitle); 524 break; 525 case 2: 526 // We cannot predict the order... 527 assertThat(actualTitle).contains(getSaveTypeString(types[0])); 528 assertThat(actualTitle).contains(getSaveTypeString(types[1])); 529 break; 530 case 3: 531 // We cannot predict the order... 532 assertThat(actualTitle).contains(getSaveTypeString(types[0])); 533 assertThat(actualTitle).contains(getSaveTypeString(types[1])); 534 assertThat(actualTitle).contains(getSaveTypeString(types[2])); 535 break; 536 default: 537 throw new IllegalArgumentException("Invalid types: " + Arrays.toString(types)); 538 } 539 540 if (description != null) { 541 final UiObject2 saveSubTitle = snackbar.findObject(By.text(description)); 542 assertWithMessage("save subtitle(%s)", description).that(saveSubTitle).isNotNull(); 543 } 544 545 final String negativeButtonStringId = 546 (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) 547 ? RESOURCE_STRING_SAVE_BUTTON_NOT_NOW 548 : RESOURCE_STRING_SAVE_BUTTON_NO_THANKS; 549 final String expectedNegativeButtonText = getString(negativeButtonStringId).toUpperCase(); 550 final UiObject2 negativeButton = waitForObject(snackbar, 551 By.res("android", RESOURCE_ID_SAVE_BUTTON_NO), timeout); 552 assertWithMessage("wrong text on negative button") 553 .that(negativeButton.getText().toUpperCase()).isEqualTo(expectedNegativeButtonText); 554 555 final String expectedAccessibilityTitle = 556 getString(RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE); 557 assertAccessibilityTitle(snackbar, expectedAccessibilityTitle); 558 559 return snackbar; 560 } 561 562 /** 563 * Taps an option in the save snackbar. 564 * 565 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 566 * @param types expected types of save info. 567 */ 568 void saveForAutofill(boolean yesDoIt, int... types) throws Exception { 569 final UiObject2 saveSnackBar = assertSaveShowing( 570 SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, types); 571 saveForAutofill(saveSnackBar, yesDoIt); 572 } 573 574 /** 575 * Taps an option in the save snackbar. 576 * 577 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 578 * @param types expected types of save info. 579 */ 580 void saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types) throws Exception { 581 final UiObject2 saveSnackBar = assertSaveShowing(negativeButtonStyle,null, types); 582 saveForAutofill(saveSnackBar, yesDoIt); 583 } 584 585 /** 586 * Taps an option in the save snackbar. 587 * 588 * @param saveSnackBar Save snackbar, typically obtained through 589 * {@link #assertSaveShowing(int)}. 590 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 591 */ 592 void saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt) { 593 final String id = yesDoIt ? "autofill_save_yes" : "autofill_save_no"; 594 595 final UiObject2 button = saveSnackBar.findObject(By.res("android", id)); 596 assertWithMessage("save button (%s)", id).that(button).isNotNull(); 597 button.click(); 598 } 599 600 /** 601 * Gets the AUTOFILL contextual menu by long pressing a text field. 602 * 603 * <p><b>NOTE:</b> this method should only be called in scenarios where we explicitly want to 604 * test the overflow menu. For all other scenarios where we want to test manual autofill, it's 605 * better to call {@code AFM.requestAutofill()} directly, because it's less error-prone and 606 * faster. 607 * 608 * @param id resource id of the field. 609 */ 610 UiObject2 getAutofillMenuOption(String id) throws Exception { 611 final UiObject2 field = waitForObject(By.res(mPackageName, id)); 612 // TODO: figure out why obj.longClick() doesn't always work 613 field.click(3000); 614 615 final List<UiObject2> menuItems = waitForObjects( 616 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout); 617 final String expectedText = getAutofillContextualMenuTitle(); 618 final StringBuffer menuNames = new StringBuffer(); 619 for (UiObject2 menuItem : menuItems) { 620 final String menuName = menuItem.getText(); 621 if (menuName.equalsIgnoreCase(expectedText)) { 622 return menuItem; 623 } 624 menuNames.append("'").append(menuName).append("' "); 625 } 626 throw new RetryableException("no '%s' on '%s'", expectedText, menuNames); 627 } 628 629 String getAutofillContextualMenuTitle() { 630 return getString(RESOURCE_STRING_AUTOFILL); 631 } 632 633 /** 634 * Gets a string from the Android resources. 635 */ 636 private String getString(String id) { 637 final Resources resources = mContext.getResources(); 638 final int stringId = resources.getIdentifier(id, "string", "android"); 639 return resources.getString(stringId); 640 } 641 642 /** 643 * Gets a string from the Android resources. 644 */ 645 private String getString(String id, Object... formatArgs) { 646 final Resources resources = mContext.getResources(); 647 final int stringId = resources.getIdentifier(id, "string", "android"); 648 return resources.getString(stringId, formatArgs); 649 } 650 651 /** 652 * Waits for and returns an object. 653 * 654 * @param selector {@link BySelector} that identifies the object. 655 */ 656 private UiObject2 waitForObject(BySelector selector) throws Exception { 657 return waitForObject(selector, mDefaultTimeout); 658 } 659 660 /** 661 * Waits for and returns an object. 662 * 663 * @param parent where to find the object (or {@code null} to use device's root). 664 * @param selector {@link BySelector} that identifies the object. 665 * @param timeout timeout in ms. 666 * @param dumpOnError whether the window hierarchy should be dumped if the object is not found. 667 */ 668 private UiObject2 waitForObject(UiObject2 parent, BySelector selector, Timeout timeout, 669 boolean dumpOnError) throws Exception { 670 // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach. 671 try { 672 return timeout.run("waitForObject(" + selector + ")", () -> { 673 return parent != null 674 ? parent.findObject(selector) 675 : mDevice.findObject(selector); 676 677 }); 678 } catch (RetryableException e) { 679 if (dumpOnError) { 680 dumpScreen("waitForObject() for " + selector + "failed"); 681 } 682 throw e; 683 } 684 } 685 686 private UiObject2 waitForObject(UiObject2 parent, BySelector selector, Timeout timeout) 687 throws Exception { 688 return waitForObject(parent, selector, timeout, DUMP_ON_ERROR); 689 } 690 691 /** 692 * Waits for and returns an object. 693 * 694 * @param selector {@link BySelector} that identifies the object. 695 * @param timeout timeout in ms 696 */ 697 private UiObject2 waitForObject(BySelector selector, Timeout timeout) throws Exception { 698 return waitForObject(null, selector, timeout); 699 } 700 701 /** 702 * Execute a Runnable and wait for TYPE_WINDOWS_CHANGED or TYPE_WINDOW_STATE_CHANGED. 703 * TODO: No longer need Retry, Refactoring the Timeout (e.g. we probably need two values: 704 * one large timeout value that expects window event, one small value that expect no window 705 * event) 706 */ 707 public void waitForWindowChange(Runnable runnable, long timeoutMillis) throws TimeoutException { 708 mAutoman.executeAndWaitForEvent(runnable, (AccessibilityEvent event) -> { 709 switch (event.getEventType()) { 710 case AccessibilityEvent.TYPE_WINDOWS_CHANGED: 711 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: 712 return true; 713 } 714 return false; 715 }, timeoutMillis); 716 } 717 718 /** 719 * Waits for and returns a list of objects. 720 * 721 * @param selector {@link BySelector} that identifies the object. 722 * @param timeout timeout in ms 723 */ 724 private List<UiObject2> waitForObjects(BySelector selector, Timeout timeout) throws Exception { 725 // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach. 726 try { 727 return timeout.run("waitForObject(" + selector + ")", () -> { 728 final List<UiObject2> uiObjects = mDevice.findObjects(selector); 729 if (uiObjects != null && !uiObjects.isEmpty()) { 730 return uiObjects; 731 } 732 return null; 733 734 }); 735 736 } catch (RetryableException e) { 737 dumpScreen("waitForObjects() for " + selector + "failed"); 738 throw e; 739 } 740 } 741 742 private UiObject2 findDatasetPicker(Timeout timeout) throws Exception { 743 final UiObject2 picker = waitForObject(DATASET_PICKER_SELECTOR, timeout); 744 745 final String expectedTitle = getString(RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE); 746 assertAccessibilityTitle(picker, expectedTitle); 747 748 if (picker != null) { 749 mOkToCallAssertNoDatasets = true; 750 } 751 752 return picker; 753 } 754 755 /** 756 * Asserts a given object has the expected accessibility title. 757 */ 758 private void assertAccessibilityTitle(UiObject2 object, String expectedTitle) { 759 // TODO: ideally it should get the AccessibilityWindowInfo from the object, but UiAutomator 760 // does not expose that. 761 for (AccessibilityWindowInfo window : mAutoman.getWindows()) { 762 final CharSequence title = window.getTitle(); 763 if (title != null && title.toString().equals(expectedTitle)) { 764 return; 765 } 766 } 767 throw new RetryableException("Title '%s' not found for %s", expectedTitle, object); 768 } 769 770 /** 771 * Sets the the screen orientation. 772 * 773 * @param orientation typically {@link #LANDSCAPE} or {@link #PORTRAIT}. 774 * 775 * @throws RetryableException if value didn't change. 776 */ 777 public void setScreenOrientation(int orientation) throws Exception { 778 mAutoman.setRotation(orientation); 779 780 UI_SCREEN_ORIENTATION_TIMEOUT.run("setScreenOrientation(" + orientation + ")", () -> { 781 return getScreenOrientation() == orientation ? Boolean.TRUE : null; 782 }); 783 } 784 785 /** 786 * Gets the value of the screen orientation. 787 * 788 * @return typically {@link #LANDSCAPE} or {@link #PORTRAIT}. 789 */ 790 public int getScreenOrientation() { 791 return mDevice.getDisplayRotation(); 792 } 793 794 /** 795 * Dumps the current view hierarchy int the output stream. 796 */ 797 public void dumpScreen(String cause) { 798 new Exception("dumpScreen(cause=" + cause + ") stacktrace").printStackTrace(System.out); 799 try { 800 try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { 801 mDevice.dumpWindowHierarchy(os); 802 os.flush(); 803 Log.w(TAG, "Dumping window hierarchy because " + cause); 804 for (String line : os.toString("UTF-8").split("\n")) { 805 Log.w(TAG, line); 806 // Sleep a little bit to avoid logs being ignored due to spam 807 SystemClock.sleep(100); 808 } 809 } 810 } catch (IOException e) { 811 // Just ignore it... 812 Log.e(TAG, "exception dumping window hierarchy", e); 813 return; 814 } 815 } 816 817 // TODO(b/74358143): ideally we should take a screenshot limited by the boundaries of the 818 // activity window, so external elements (such as the clock) are filtered out and don't cause 819 // test flakiness when the contents are compared. 820 public Bitmap takeScreenshot() { 821 final long before = SystemClock.elapsedRealtime(); 822 final Bitmap bitmap = mAutoman.takeScreenshot(); 823 final long delta = SystemClock.elapsedRealtime() - before; 824 Log.v(TAG, "Screenshot taken in " + delta + "ms"); 825 return bitmap; 826 } 827 828 /** 829 * Asserts the contents of a child element. 830 * 831 * @param parent parent object 832 * @param childId (relative) resource id of the child 833 * @param assertion if {@code null}, asserts the child does not exist; otherwise, asserts the 834 * child with it. 835 */ 836 public void assertChild(@NonNull UiObject2 parent, @NonNull String childId, 837 @Nullable Visitor<UiObject2> assertion) { 838 final UiObject2 child = parent.findObject(By.res(mPackageName, childId)); 839 if (assertion != null) { 840 assertWithMessage("Didn't find child with id '%s'", childId).that(child).isNotNull(); 841 try { 842 assertion.visit(child); 843 } catch (Throwable t) { 844 throw new AssertionError("Error on child '" + childId + "'", t); 845 } 846 } else { 847 assertWithMessage("Shouldn't find child with id '%s'", childId).that(child).isNull(); 848 } 849 } 850 } 851