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.android.compatibility.common.util.ShellUtils.runShellCommand; 33 34 import static com.google.common.truth.Truth.assertThat; 35 import static com.google.common.truth.Truth.assertWithMessage; 36 37 import static org.junit.Assume.assumeTrue; 38 39 import android.app.Instrumentation; 40 import android.app.UiAutomation; 41 import android.content.Context; 42 import android.content.res.Resources; 43 import android.graphics.Bitmap; 44 import android.os.SystemClock; 45 import android.service.autofill.SaveInfo; 46 import android.support.test.uiautomator.By; 47 import android.support.test.uiautomator.BySelector; 48 import android.support.test.uiautomator.SearchCondition; 49 import android.support.test.uiautomator.UiDevice; 50 import android.support.test.uiautomator.UiObject2; 51 import android.support.test.uiautomator.Until; 52 import android.text.Html; 53 import android.util.Log; 54 import android.view.accessibility.AccessibilityEvent; 55 import android.view.accessibility.AccessibilityWindowInfo; 56 57 import androidx.annotation.NonNull; 58 import androidx.annotation.Nullable; 59 import androidx.test.platform.app.InstrumentationRegistry; 60 61 import com.android.compatibility.common.util.RetryableException; 62 import com.android.compatibility.common.util.Timeout; 63 64 import java.io.File; 65 import java.io.FileInputStream; 66 import java.util.ArrayList; 67 import java.util.Arrays; 68 import java.util.List; 69 import java.util.concurrent.TimeoutException; 70 71 /** 72 * Helper for UI-related needs. 73 */ 74 public final class UiBot { 75 76 private static final String TAG = "AutoFillCtsUiBot"; 77 78 private static final String RESOURCE_ID_DATASET_PICKER = "autofill_dataset_picker"; 79 private static final String RESOURCE_ID_DATASET_HEADER = "autofill_dataset_header"; 80 private static final String RESOURCE_ID_SAVE_SNACKBAR = "autofill_save"; 81 private static final String RESOURCE_ID_SAVE_ICON = "autofill_save_icon"; 82 private static final String RESOURCE_ID_SAVE_TITLE = "autofill_save_title"; 83 private static final String RESOURCE_ID_CONTEXT_MENUITEM = "floating_toolbar_menu_item_text"; 84 private static final String RESOURCE_ID_SAVE_BUTTON_NO = "autofill_save_no"; 85 private static final String RESOURCE_ID_SAVE_BUTTON_YES = "autofill_save_yes"; 86 private static final String RESOURCE_ID_OVERFLOW = "overflow"; 87 88 private static final String RESOURCE_STRING_SAVE_TITLE = "autofill_save_title"; 89 private static final String RESOURCE_STRING_SAVE_TITLE_WITH_TYPE = 90 "autofill_save_title_with_type"; 91 private static final String RESOURCE_STRING_SAVE_TYPE_PASSWORD = "autofill_save_type_password"; 92 private static final String RESOURCE_STRING_SAVE_TYPE_ADDRESS = "autofill_save_type_address"; 93 private static final String RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD = 94 "autofill_save_type_credit_card"; 95 private static final String RESOURCE_STRING_SAVE_TYPE_USERNAME = "autofill_save_type_username"; 96 private static final String RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS = 97 "autofill_save_type_email_address"; 98 private static final String RESOURCE_STRING_SAVE_BUTTON_NOT_NOW = "save_password_notnow"; 99 private static final String RESOURCE_STRING_SAVE_BUTTON_NO_THANKS = "autofill_save_no"; 100 private static final String RESOURCE_STRING_SAVE_BUTTON_YES = "autofill_save_yes"; 101 private static final String RESOURCE_STRING_UPDATE_BUTTON_YES = "autofill_update_yes"; 102 private static final String RESOURCE_STRING_UPDATE_TITLE = "autofill_update_title"; 103 private static final String RESOURCE_STRING_UPDATE_TITLE_WITH_TYPE = 104 "autofill_update_title_with_type"; 105 106 private static final String RESOURCE_STRING_AUTOFILL = "autofill"; 107 private static final String RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE = 108 "autofill_picker_accessibility_title"; 109 private static final String RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE = 110 "autofill_save_accessibility_title"; 111 112 113 static final BySelector DATASET_PICKER_SELECTOR = By.res("android", RESOURCE_ID_DATASET_PICKER); 114 private static final BySelector SAVE_UI_SELECTOR = By.res("android", RESOURCE_ID_SAVE_SNACKBAR); 115 private static final BySelector DATASET_HEADER_SELECTOR = 116 By.res("android", RESOURCE_ID_DATASET_HEADER); 117 118 // TODO: figure out a more reliable solution that does not depend on SystemUI resources. 119 private static final String SPLIT_WINDOW_DIVIDER_ID = 120 "com.android.systemui:id/docked_divider_background"; 121 122 private static final boolean DUMP_ON_ERROR = true; 123 124 /** Pass to {@link #setScreenOrientation(int)} to change the display to portrait mode */ 125 public static int PORTRAIT = 0; 126 127 /** Pass to {@link #setScreenOrientation(int)} to change the display to landscape mode */ 128 public static int LANDSCAPE = 1; 129 130 private final UiDevice mDevice; 131 private final Context mContext; 132 private final String mPackageName; 133 private final UiAutomation mAutoman; 134 private final Timeout mDefaultTimeout; 135 136 private boolean mOkToCallAssertNoDatasets; 137 UiBot()138 public UiBot() { 139 this(UI_TIMEOUT); 140 } 141 UiBot(Timeout defaultTimeout)142 public UiBot(Timeout defaultTimeout) { 143 mDefaultTimeout = defaultTimeout; 144 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 145 mDevice = UiDevice.getInstance(instrumentation); 146 mContext = instrumentation.getContext(); 147 mPackageName = mContext.getPackageName(); 148 mAutoman = instrumentation.getUiAutomation(); 149 } 150 waitForIdle()151 public void waitForIdle() { 152 final long before = SystemClock.elapsedRealtimeNanos(); 153 mDevice.waitForIdle(); 154 final float delta = ((float) (SystemClock.elapsedRealtimeNanos() - before)) / 1_000_000; 155 Log.v(TAG, "device idle in " + delta + "ms"); 156 } 157 reset()158 public void reset() { 159 mOkToCallAssertNoDatasets = false; 160 } 161 162 /** 163 * Assumes the device has a minimum height and width of {@code minSize}, throwing a 164 * {@code AssumptionViolatedException} if it doesn't (so the test is skiped by the JUnit 165 * Runner). 166 */ assumeMinimumResolution(int minSize)167 public void assumeMinimumResolution(int minSize) { 168 final int width = mDevice.getDisplayWidth(); 169 final int heigth = mDevice.getDisplayHeight(); 170 final int min = Math.min(width, heigth); 171 assumeTrue("Screen size is too small (" + width + "x" + heigth + ")", min >= minSize); 172 Log.d(TAG, "assumeMinimumResolution(" + minSize + ") passed: screen size is " 173 + width + "x" + heigth); 174 } 175 176 /** 177 * Sets the screen resolution in a way that the IME doesn't interfere with the Autofill UI 178 * when the device is rotated to landscape. 179 * 180 * When called, test must call <p>{@link #resetScreenResolution()} in a {@code finally} block. 181 * 182 * @deprecated this method should not be necessarily anymore as we're using a MockIme. 183 */ 184 @Deprecated 185 // TODO: remove once we're sure no more OEM is getting failure due to screen size setScreenResolution()186 public void setScreenResolution() { 187 if (true) { 188 Log.w(TAG, "setScreenResolution(): ignored"); 189 return; 190 } 191 assumeMinimumResolution(500); 192 193 runShellCommand("wm size 1080x1920"); 194 runShellCommand("wm density 320"); 195 } 196 197 /** 198 * Resets the screen resolution. 199 * 200 * <p>Should always be called after {@link #setScreenResolution()}. 201 * 202 * @deprecated this method should not be necessarily anymore as we're using a MockIme. 203 */ 204 @Deprecated 205 // TODO: remove once we're sure no more OEM is getting failure due to screen size resetScreenResolution()206 public void resetScreenResolution() { 207 if (true) { 208 Log.w(TAG, "resetScreenResolution(): ignored"); 209 return; 210 } 211 runShellCommand("wm density reset"); 212 runShellCommand("wm size reset"); 213 } 214 215 /** 216 * Asserts the dataset picker is not shown anymore. 217 * 218 * @throws IllegalStateException if called *before* an assertion was made to make sure the 219 * dataset picker is shown - if that's not the case, call 220 * {@link #assertNoDatasetsEver()} instead. 221 */ assertNoDatasets()222 public void assertNoDatasets() throws Exception { 223 if (!mOkToCallAssertNoDatasets) { 224 throw new IllegalStateException( 225 "Cannot call assertNoDatasets() without calling assertDatasets first"); 226 } 227 mDevice.wait(Until.gone(DATASET_PICKER_SELECTOR), UI_DATASET_PICKER_TIMEOUT.ms()); 228 mOkToCallAssertNoDatasets = false; 229 } 230 231 /** 232 * Asserts the dataset picker was never shown. 233 * 234 * <p>This method is slower than {@link #assertNoDatasets()} and should only be called in the 235 * cases where the dataset picker was not previous shown. 236 */ assertNoDatasetsEver()237 public void assertNoDatasetsEver() throws Exception { 238 assertNeverShown("dataset picker", DATASET_PICKER_SELECTOR, 239 DATASET_PICKER_NOT_SHOWN_NAPTIME_MS); 240 } 241 242 /** 243 * Asserts the dataset chooser is shown and contains exactly the given datasets. 244 * 245 * @return the dataset picker object. 246 */ assertDatasets(String...names)247 public UiObject2 assertDatasets(String...names) throws Exception { 248 // TODO: change run() so it can rethrow the original message 249 return UI_DATASET_PICKER_TIMEOUT.run("assertDatasets: " + Arrays.toString(names), () -> { 250 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 251 try { 252 // TODO: use a library to check it contains, instead of asserThat + catch exception 253 assertWithMessage("wrong dataset names").that(getChildrenAsText(picker)) 254 .containsExactlyElementsIn(Arrays.asList(names)).inOrder(); 255 return picker; 256 } catch (AssertionError e) { 257 // Value mismatch - most likely UI didn't change yet, try again 258 Log.w(TAG, "datasets don't match yet: " + e.getMessage()); 259 return null; 260 } 261 }); 262 } 263 264 /** 265 * Asserts the dataset chooser is shown and contains the given datasets. 266 * 267 * @return the dataset picker object. 268 */ assertDatasetsContains(String...names)269 public UiObject2 assertDatasetsContains(String...names) throws Exception { 270 // TODO: change run() so it can rethrow the original message 271 return UI_DATASET_PICKER_TIMEOUT.run("assertDatasets: " + Arrays.toString(names), () -> { 272 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 273 try { 274 // TODO: use a library to check it contains, instead of asserThat + catch exception 275 assertWithMessage("wrong dataset names").that(getChildrenAsText(picker)) 276 .containsAllIn(Arrays.asList(names)).inOrder(); 277 return picker; 278 } catch (AssertionError e) { 279 // Value mismatch - most likely UI didn't change yet, try again 280 Log.w(TAG, "datasets don't match yet: " + e.getMessage()); 281 return null; 282 } 283 }); 284 } 285 286 /** 287 * Asserts the dataset chooser is shown and contains the given datasets, header, and footer. 288 * <p>In fullscreen, header view is not under R.id.autofill_dataset_picker. 289 * 290 * @return the dataset picker object. 291 */ 292 public UiObject2 assertDatasetsWithBorders(String header, String footer, String...names) 293 throws Exception { 294 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 295 final List<String> expectedChild = new ArrayList<>(); 296 if (header != null) { 297 if (Helper.isAutofillWindowFullScreen(mContext)) { 298 final UiObject2 headerView = waitForObject(DATASET_HEADER_SELECTOR, 299 UI_DATASET_PICKER_TIMEOUT); 300 assertWithMessage("fullscreen wrong dataset header") 301 .that(getChildrenAsText(headerView)) 302 .containsExactlyElementsIn(Arrays.asList(header)).inOrder(); 303 } else { 304 expectedChild.add(header); 305 } 306 } 307 expectedChild.addAll(Arrays.asList(names)); 308 if (footer != null) { 309 expectedChild.add(footer); 310 } 311 assertWithMessage("wrong elements on dataset picker").that(getChildrenAsText(picker)) 312 .containsExactlyElementsIn(expectedChild).inOrder(); 313 return picker; 314 } 315 316 /** 317 * Gets the text of this object children. 318 */ 319 public List<String> getChildrenAsText(UiObject2 object) { 320 final List<String> list = new ArrayList<>(); 321 getChildrenAsText(object, list); 322 return list; 323 } 324 325 private static void getChildrenAsText(UiObject2 object, List<String> children) { 326 final String text = object.getText(); 327 if (text != null) { 328 children.add(text); 329 } 330 for (UiObject2 child : object.getChildren()) { 331 getChildrenAsText(child, children); 332 } 333 } 334 335 /** 336 * Selects a dataset that should be visible in the floating UI. 337 */ 338 public void selectDataset(String name) throws Exception { 339 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 340 selectDataset(picker, name); 341 } 342 343 /** 344 * Selects a dataset that should be visible in the floating UI. 345 */ 346 public void selectDataset(UiObject2 picker, String name) { 347 final UiObject2 dataset = picker.findObject(By.text(name)); 348 if (dataset == null) { 349 throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(picker)); 350 } 351 dataset.click(); 352 } 353 354 /** 355 * Selects a view by text. 356 * 357 * <p><b>NOTE:</b> when selecting an option in dataset picker is shown, prefer 358 * {@link #selectDataset(String)}. 359 */ 360 public void selectByText(String name) throws Exception { 361 Log.v(TAG, "selectByText(): " + name); 362 363 final UiObject2 object = waitForObject(By.text(name)); 364 object.click(); 365 } 366 367 /** 368 * Asserts a text is shown. 369 * 370 * <p><b>NOTE:</b> when asserting the dataset picker is shown, prefer 371 * {@link #assertDatasets(String...)}. 372 */ 373 public UiObject2 assertShownByText(String text) throws Exception { 374 return assertShownByText(text, mDefaultTimeout); 375 } 376 377 public UiObject2 assertShownByText(String text, Timeout timeout) throws Exception { 378 final UiObject2 object = waitForObject(By.text(text), timeout); 379 assertWithMessage("No node with text '%s'", text).that(object).isNotNull(); 380 return object; 381 } 382 383 /** 384 * Finds a node by text, without waiting for it to be shown (but failing if it isn't). 385 */ 386 @NonNull 387 public UiObject2 findRightAwayByText(@NonNull String text) throws Exception { 388 final UiObject2 object = mDevice.findObject(By.text(text)); 389 assertWithMessage("no UIObject for text '%s'", text).that(object).isNotNull(); 390 return object; 391 } 392 393 /** 394 * Asserts that the text is not showing for sure in the screen "as is", i.e., without waiting 395 * for it. 396 * 397 * <p>Typically called after another assertion that waits for a condition to be shown. 398 */ 399 public void assertNotShowingForSure(String text) throws Exception { 400 final UiObject2 object = mDevice.findObject(By.text(text)); 401 assertWithMessage("Found node with text '%s'", text).that(object).isNull(); 402 } 403 404 /** 405 * Asserts a node with the given content description is shown. 406 * 407 */ 408 public UiObject2 assertShownByContentDescription(String contentDescription) throws Exception { 409 final UiObject2 object = waitForObject(By.desc(contentDescription)); 410 assertWithMessage("No node with content description '%s'", contentDescription).that(object) 411 .isNotNull(); 412 return object; 413 } 414 415 /** 416 * Checks if a View with a certain text exists. 417 */ 418 public boolean hasViewWithText(String name) { 419 Log.v(TAG, "hasViewWithText(): " + name); 420 421 return mDevice.findObject(By.text(name)) != null; 422 } 423 424 /** 425 * Selects a view by id. 426 */ 427 public UiObject2 selectByRelativeId(String id) throws Exception { 428 Log.v(TAG, "selectByRelativeId(): " + id); 429 UiObject2 object = waitForObject(By.res(mPackageName, id)); 430 object.click(); 431 return object; 432 } 433 434 /** 435 * Asserts the id is shown on the screen. 436 */ 437 public UiObject2 assertShownById(String id) throws Exception { 438 final UiObject2 object = waitForObject(By.res(id)); 439 assertThat(object).isNotNull(); 440 return object; 441 } 442 443 /** 444 * Asserts the id is shown on the screen, using a resource id from the test package. 445 */ 446 public UiObject2 assertShownByRelativeId(String id) throws Exception { 447 return assertShownByRelativeId(id, mDefaultTimeout); 448 } 449 450 public UiObject2 assertShownByRelativeId(String id, Timeout timeout) throws Exception { 451 final UiObject2 obj = waitForObject(By.res(mPackageName, id), timeout); 452 assertThat(obj).isNotNull(); 453 return obj; 454 } 455 456 /** 457 * Asserts the id is not shown on the screen anymore, using a resource id from the test package. 458 * 459 * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise 460 * it might pass without really asserting anything. 461 */ 462 public void assertGoneByRelativeId(@NonNull String id, @NonNull Timeout timeout) { 463 assertGoneByRelativeId(/* parent = */ null, id, timeout); 464 } 465 466 public void assertGoneByRelativeId(int resId, @NonNull Timeout timeout) { 467 assertGoneByRelativeId(/* parent = */ null, getIdName(resId), timeout); 468 } 469 470 private String getIdName(int resId) { 471 return mContext.getResources().getResourceEntryName(resId); 472 } 473 474 /** 475 * Asserts the id is not shown on the parent anymore, using a resource id from the test package. 476 * 477 * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise 478 * it might pass without really asserting anything. 479 */ 480 public void assertGoneByRelativeId(@Nullable UiObject2 parent, @NonNull String id, 481 @NonNull Timeout timeout) { 482 final SearchCondition<Boolean> condition = Until.gone(By.res(mPackageName, id)); 483 final boolean gone = parent != null 484 ? parent.wait(condition, timeout.ms()) 485 : mDevice.wait(condition, timeout.ms()); 486 if (!gone) { 487 final String message = "Object with id '" + id + "' should be gone after " 488 + timeout + " ms"; 489 dumpScreen(message); 490 throw new RetryableException(message); 491 } 492 } 493 494 public UiObject2 assertShownByRelativeId(int resId) throws Exception { 495 return assertShownByRelativeId(getIdName(resId)); 496 } 497 498 public void assertNeverShownByRelativeId(@NonNull String description, int resId, long timeout) 499 throws Exception { 500 final BySelector selector = By.res(Helper.MY_PACKAGE, getIdName(resId)); 501 assertNeverShown(description, selector, timeout); 502 } 503 504 /** 505 * Asserts that a {@code selector} is not showing after {@code timeout} milliseconds. 506 */ 507 private void assertNeverShown(String description, BySelector selector, long timeout) 508 throws Exception { 509 SystemClock.sleep(timeout); 510 final UiObject2 object = mDevice.findObject(selector); 511 if (object != null) { 512 throw new AssertionError( 513 String.format("Should not be showing %s after %dms, but got %s", 514 description, timeout, getChildrenAsText(object))); 515 } 516 } 517 518 /** 519 * Gets the text set on a view. 520 */ 521 public String getTextByRelativeId(String id) throws Exception { 522 return waitForObject(By.res(mPackageName, id)).getText(); 523 } 524 525 /** 526 * Focus in the view with the given resource id. 527 */ 528 public void focusByRelativeId(String id) throws Exception { 529 waitForObject(By.res(mPackageName, id)).click(); 530 } 531 532 /** 533 * Sets a new text on a view. 534 */ 535 public void setTextByRelativeId(String id, String newText) throws Exception { 536 waitForObject(By.res(mPackageName, id)).setText(newText); 537 } 538 539 /** 540 * Asserts the save snackbar is showing and returns it. 541 */ 542 public UiObject2 assertSaveShowing(int type) throws Exception { 543 return assertSaveShowing(SAVE_TIMEOUT, type); 544 } 545 546 /** 547 * Asserts the save snackbar is showing and returns it. 548 */ 549 public UiObject2 assertSaveShowing(Timeout timeout, int type) throws Exception { 550 return assertSaveShowing(null, timeout, type); 551 } 552 553 /** 554 * Asserts the save snackbar is showing with the Update message and returns it. 555 */ 556 public UiObject2 assertUpdateShowing(int... types) throws Exception { 557 return assertSaveOrUpdateShowing(/* update= */ true, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 558 null, SAVE_TIMEOUT, types); 559 } 560 561 /** 562 * Presses the Back button. 563 */ 564 public void pressBack() { 565 Log.d(TAG, "pressBack()"); 566 mDevice.pressBack(); 567 } 568 569 /** 570 * Presses the Home button. 571 */ 572 public void pressHome() { 573 Log.d(TAG, "pressHome()"); 574 mDevice.pressHome(); 575 } 576 577 /** 578 * Asserts the save snackbar is not showing. 579 */ 580 public void assertSaveNotShowing(int type) throws Exception { 581 assertNeverShown("save UI for type " + type, SAVE_UI_SELECTOR, SAVE_NOT_SHOWN_NAPTIME_MS); 582 } 583 584 public void assertSaveNotShowing() throws Exception { 585 assertNeverShown("save UI", SAVE_UI_SELECTOR, SAVE_NOT_SHOWN_NAPTIME_MS); 586 } 587 588 private String getSaveTypeString(int type) { 589 final String typeResourceName; 590 switch (type) { 591 case SAVE_DATA_TYPE_PASSWORD: 592 typeResourceName = RESOURCE_STRING_SAVE_TYPE_PASSWORD; 593 break; 594 case SAVE_DATA_TYPE_ADDRESS: 595 typeResourceName = RESOURCE_STRING_SAVE_TYPE_ADDRESS; 596 break; 597 case SAVE_DATA_TYPE_CREDIT_CARD: 598 typeResourceName = RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD; 599 break; 600 case SAVE_DATA_TYPE_USERNAME: 601 typeResourceName = RESOURCE_STRING_SAVE_TYPE_USERNAME; 602 break; 603 case SAVE_DATA_TYPE_EMAIL_ADDRESS: 604 typeResourceName = RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS; 605 break; 606 default: 607 throw new IllegalArgumentException("Unsupported type: " + type); 608 } 609 return getString(typeResourceName); 610 } 611 612 public UiObject2 assertSaveShowing(String description, int... types) throws Exception { 613 return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 614 description, SAVE_TIMEOUT, types); 615 } 616 617 public UiObject2 assertSaveShowing(String description, Timeout timeout, int... types) 618 throws Exception { 619 return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 620 description, timeout, types); 621 } 622 623 public UiObject2 assertSaveShowing(int negativeButtonStyle, String description, 624 int... types) throws Exception { 625 return assertSaveOrUpdateShowing(/* update= */ false, negativeButtonStyle, description, 626 SAVE_TIMEOUT, types); 627 } 628 629 630 public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, 631 String description, Timeout timeout, int... types) throws Exception { 632 final UiObject2 snackbar = waitForObject(SAVE_UI_SELECTOR, timeout); 633 634 final UiObject2 titleView = 635 waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_TITLE), timeout); 636 assertWithMessage("save title (%s) is not shown", RESOURCE_ID_SAVE_TITLE).that(titleView) 637 .isNotNull(); 638 639 final UiObject2 iconView = 640 waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_ICON), timeout); 641 assertWithMessage("save icon (%s) is not shown", RESOURCE_ID_SAVE_ICON).that(iconView) 642 .isNotNull(); 643 644 final String actualTitle = titleView.getText(); 645 Log.d(TAG, "save title: " + actualTitle); 646 647 final String titleId, titleWithTypeId; 648 if (update) { 649 titleId = RESOURCE_STRING_UPDATE_TITLE; 650 titleWithTypeId = RESOURCE_STRING_UPDATE_TITLE_WITH_TYPE; 651 } else { 652 titleId = RESOURCE_STRING_SAVE_TITLE; 653 titleWithTypeId = RESOURCE_STRING_SAVE_TITLE_WITH_TYPE; 654 } 655 656 final String serviceLabel = InstrumentedAutoFillService.getServiceLabel(); 657 switch (types.length) { 658 case 1: 659 final String expectedTitle = (types[0] == SAVE_DATA_TYPE_GENERIC) 660 ? Html.fromHtml(getString(titleId, serviceLabel), 0).toString() 661 : Html.fromHtml(getString(titleWithTypeId, 662 getSaveTypeString(types[0]), serviceLabel), 0).toString(); 663 assertThat(actualTitle).isEqualTo(expectedTitle); 664 break; 665 case 2: 666 // We cannot predict the order... 667 assertThat(actualTitle).contains(getSaveTypeString(types[0])); 668 assertThat(actualTitle).contains(getSaveTypeString(types[1])); 669 break; 670 case 3: 671 // We cannot predict the order... 672 assertThat(actualTitle).contains(getSaveTypeString(types[0])); 673 assertThat(actualTitle).contains(getSaveTypeString(types[1])); 674 assertThat(actualTitle).contains(getSaveTypeString(types[2])); 675 break; 676 default: 677 throw new IllegalArgumentException("Invalid types: " + Arrays.toString(types)); 678 } 679 680 if (description != null) { 681 final UiObject2 saveSubTitle = snackbar.findObject(By.text(description)); 682 assertWithMessage("save subtitle(%s)", description).that(saveSubTitle).isNotNull(); 683 } 684 685 final String positiveButtonStringId = update ? RESOURCE_STRING_UPDATE_BUTTON_YES 686 : RESOURCE_STRING_SAVE_BUTTON_YES; 687 final String expectedPositiveButtonText = getString(positiveButtonStringId).toUpperCase(); 688 final UiObject2 positiveButton = waitForObject(snackbar, 689 By.res("android", RESOURCE_ID_SAVE_BUTTON_YES), timeout); 690 assertWithMessage("wrong text on positive button") 691 .that(positiveButton.getText().toUpperCase()).isEqualTo(expectedPositiveButtonText); 692 693 final String negativeButtonStringId = 694 (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) 695 ? RESOURCE_STRING_SAVE_BUTTON_NOT_NOW 696 : RESOURCE_STRING_SAVE_BUTTON_NO_THANKS; 697 final String expectedNegativeButtonText = getString(negativeButtonStringId).toUpperCase(); 698 final UiObject2 negativeButton = waitForObject(snackbar, 699 By.res("android", RESOURCE_ID_SAVE_BUTTON_NO), timeout); 700 assertWithMessage("wrong text on negative button") 701 .that(negativeButton.getText().toUpperCase()).isEqualTo(expectedNegativeButtonText); 702 703 final String expectedAccessibilityTitle = 704 getString(RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE); 705 assertAccessibilityTitle(snackbar, expectedAccessibilityTitle); 706 707 return snackbar; 708 } 709 710 /** 711 * Taps an option in the save snackbar. 712 * 713 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 714 * @param types expected types of save info. 715 */ 716 public void saveForAutofill(boolean yesDoIt, int... types) throws Exception { 717 final UiObject2 saveSnackBar = assertSaveShowing( 718 SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, types); 719 saveForAutofill(saveSnackBar, yesDoIt); 720 } 721 722 public void updateForAutofill(boolean yesDoIt, int... types) throws Exception { 723 final UiObject2 saveUi = assertUpdateShowing(types); 724 saveForAutofill(saveUi, yesDoIt); 725 } 726 727 /** 728 * Taps an option in the save snackbar. 729 * 730 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 731 * @param types expected types of save info. 732 */ 733 public void saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types) 734 throws Exception { 735 final UiObject2 saveSnackBar = assertSaveShowing(negativeButtonStyle,null, types); 736 saveForAutofill(saveSnackBar, yesDoIt); 737 } 738 739 /** 740 * Taps an option in the save snackbar. 741 * 742 * @param saveSnackBar Save snackbar, typically obtained through 743 * {@link #assertSaveShowing(int)}. 744 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 745 */ 746 public void saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt) { 747 final String id = yesDoIt ? "autofill_save_yes" : "autofill_save_no"; 748 749 final UiObject2 button = saveSnackBar.findObject(By.res("android", id)); 750 assertWithMessage("save button (%s)", id).that(button).isNotNull(); 751 button.click(); 752 } 753 754 /** 755 * Gets the AUTOFILL contextual menu by long pressing a text field. 756 * 757 * <p><b>NOTE:</b> this method should only be called in scenarios where we explicitly want to 758 * test the overflow menu. For all other scenarios where we want to test manual autofill, it's 759 * better to call {@code AFM.requestAutofill()} directly, because it's less error-prone and 760 * faster. 761 * 762 * @param id resource id of the field. 763 */ 764 public UiObject2 getAutofillMenuOption(String id) throws Exception { 765 final UiObject2 field = waitForObject(By.res(mPackageName, id)); 766 // TODO: figure out why obj.longClick() doesn't always work 767 field.click(3000); 768 769 List<UiObject2> menuItems = waitForObjects( 770 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout); 771 final String expectedText = getAutofillContextualMenuTitle(); 772 773 final StringBuffer menuNames = new StringBuffer(); 774 775 // Check first menu for AUTOFILL 776 for (UiObject2 menuItem : menuItems) { 777 final String menuName = menuItem.getText(); 778 if (menuName.equalsIgnoreCase(expectedText)) { 779 Log.v(TAG, "AUTOFILL found in first menu"); 780 return menuItem; 781 } 782 menuNames.append("'").append(menuName).append("' "); 783 } 784 785 menuNames.append(";"); 786 787 // First menu does not have AUTOFILL, check overflow 788 final BySelector overflowSelector = By.res("android", RESOURCE_ID_OVERFLOW); 789 790 // Click overflow menu button. 791 final UiObject2 overflowMenu = waitForObject(overflowSelector, mDefaultTimeout); 792 overflowMenu.click(); 793 794 // Wait for overflow menu to show. 795 mDevice.wait(Until.gone(overflowSelector), 1000); 796 797 menuItems = waitForObjects( 798 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout); 799 for (UiObject2 menuItem : menuItems) { 800 final String menuName = menuItem.getText(); 801 if (menuName.equalsIgnoreCase(expectedText)) { 802 Log.v(TAG, "AUTOFILL found in overflow menu"); 803 return menuItem; 804 } 805 menuNames.append("'").append(menuName).append("' "); 806 } 807 throw new RetryableException("no '%s' on '%s'", expectedText, menuNames); 808 } 809 810 String getAutofillContextualMenuTitle() { 811 return getString(RESOURCE_STRING_AUTOFILL); 812 } 813 814 /** 815 * Gets a string from the Android resources. 816 */ 817 private String getString(String id) { 818 final Resources resources = mContext.getResources(); 819 final int stringId = resources.getIdentifier(id, "string", "android"); 820 return resources.getString(stringId); 821 } 822 823 /** 824 * Gets a string from the Android resources. 825 */ 826 private String getString(String id, Object... formatArgs) { 827 final Resources resources = mContext.getResources(); 828 final int stringId = resources.getIdentifier(id, "string", "android"); 829 return resources.getString(stringId, formatArgs); 830 } 831 832 /** 833 * Waits for and returns an object. 834 * 835 * @param selector {@link BySelector} that identifies the object. 836 */ 837 private UiObject2 waitForObject(BySelector selector) throws Exception { 838 return waitForObject(selector, mDefaultTimeout); 839 } 840 841 /** 842 * Waits for and returns an object. 843 * 844 * @param parent where to find the object (or {@code null} to use device's root). 845 * @param selector {@link BySelector} that identifies the object. 846 * @param timeout timeout in ms. 847 * @param dumpOnError whether the window hierarchy should be dumped if the object is not found. 848 */ 849 private UiObject2 waitForObject(UiObject2 parent, BySelector selector, Timeout timeout, 850 boolean dumpOnError) throws Exception { 851 // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach. 852 try { 853 return timeout.run("waitForObject(" + selector + ")", () -> { 854 return parent != null 855 ? parent.findObject(selector) 856 : mDevice.findObject(selector); 857 858 }); 859 } catch (RetryableException e) { 860 if (dumpOnError) { 861 dumpScreen("waitForObject() for " + selector + "on " 862 + (parent == null ? "mDevice" : parent) + " failed"); 863 } 864 throw e; 865 } 866 } 867 868 public UiObject2 waitForObject(@Nullable UiObject2 parent, @NonNull BySelector selector, 869 @NonNull Timeout timeout) 870 throws Exception { 871 return waitForObject(parent, selector, timeout, DUMP_ON_ERROR); 872 } 873 874 /** 875 * Waits for and returns an object. 876 * 877 * @param selector {@link BySelector} that identifies the object. 878 * @param timeout timeout in ms 879 */ 880 private UiObject2 waitForObject(@NonNull BySelector selector, @NonNull Timeout timeout) 881 throws Exception { 882 return waitForObject(/* parent= */ null, selector, timeout); 883 } 884 885 /** 886 * Waits for and returns a child from a parent {@link UiObject2}. 887 */ 888 public UiObject2 assertChildText(UiObject2 parent, String resourceId, String expectedText) 889 throws Exception { 890 final UiObject2 child = waitForObject(parent, By.res(mPackageName, resourceId), 891 Timeouts.UI_TIMEOUT); 892 assertWithMessage("wrong text for view '%s'", resourceId).that(child.getText()) 893 .isEqualTo(expectedText); 894 return child; 895 } 896 897 /** 898 * Execute a Runnable and wait for {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} or 899 * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED}. 900 */ 901 public AccessibilityEvent waitForWindowChange(Runnable runnable, long timeoutMillis) { 902 try { 903 return mAutoman.executeAndWaitForEvent(runnable, (AccessibilityEvent event) -> { 904 switch (event.getEventType()) { 905 case AccessibilityEvent.TYPE_WINDOWS_CHANGED: 906 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: 907 return true; 908 default: 909 Log.v(TAG, "waitForWindowChange(): ignoring event " + event); 910 } 911 return false; 912 }, timeoutMillis); 913 } catch (TimeoutException e) { 914 throw new WindowChangeTimeoutException(e, timeoutMillis); 915 } 916 } 917 918 public AccessibilityEvent waitForWindowChange(Runnable runnable) { 919 return waitForWindowChange(runnable, Timeouts.WINDOW_CHANGE_TIMEOUT_MS); 920 } 921 922 /** 923 * Waits for and returns a list of objects. 924 * 925 * @param selector {@link BySelector} that identifies the object. 926 * @param timeout timeout in ms 927 */ 928 private List<UiObject2> waitForObjects(BySelector selector, Timeout timeout) throws Exception { 929 // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach. 930 try { 931 return timeout.run("waitForObject(" + selector + ")", () -> { 932 final List<UiObject2> uiObjects = mDevice.findObjects(selector); 933 if (uiObjects != null && !uiObjects.isEmpty()) { 934 return uiObjects; 935 } 936 return null; 937 938 }); 939 940 } catch (RetryableException e) { 941 dumpScreen("waitForObjects() for " + selector + "failed"); 942 throw e; 943 } 944 } 945 946 private UiObject2 findDatasetPicker(Timeout timeout) throws Exception { 947 final UiObject2 picker = waitForObject(DATASET_PICKER_SELECTOR, timeout); 948 949 final String expectedTitle = getString(RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE); 950 assertAccessibilityTitle(picker, expectedTitle); 951 952 if (picker != null) { 953 mOkToCallAssertNoDatasets = true; 954 } 955 956 return picker; 957 } 958 959 /** 960 * Asserts a given object has the expected accessibility title. 961 */ 962 private void assertAccessibilityTitle(UiObject2 object, String expectedTitle) { 963 // TODO: ideally it should get the AccessibilityWindowInfo from the object, but UiAutomator 964 // does not expose that. 965 for (AccessibilityWindowInfo window : mAutoman.getWindows()) { 966 final CharSequence title = window.getTitle(); 967 if (title != null && title.toString().equals(expectedTitle)) { 968 return; 969 } 970 } 971 throw new RetryableException("Title '%s' not found for %s", expectedTitle, object); 972 } 973 974 /** 975 * Sets the the screen orientation. 976 * 977 * @param orientation typically {@link #LANDSCAPE} or {@link #PORTRAIT}. 978 * 979 * @throws RetryableException if value didn't change. 980 */ 981 public void setScreenOrientation(int orientation) throws Exception { 982 mAutoman.setRotation(orientation); 983 984 UI_SCREEN_ORIENTATION_TIMEOUT.run("setScreenOrientation(" + orientation + ")", () -> { 985 return getScreenOrientation() == orientation ? Boolean.TRUE : null; 986 }); 987 } 988 989 /** 990 * Gets the value of the screen orientation. 991 * 992 * @return typically {@link #LANDSCAPE} or {@link #PORTRAIT}. 993 */ 994 public int getScreenOrientation() { 995 return mDevice.getDisplayRotation(); 996 } 997 998 /** 999 * Dumps the current view hierarchy and take a screenshot and save both locally so they can be 1000 * inspected later. 1001 */ 1002 public void dumpScreen(@NonNull String cause) { 1003 try { 1004 final File file = Helper.createTestFile("hierarchy.xml"); 1005 if (file == null) return; 1006 Log.w(TAG, "Dumping window hierarchy because " + cause + " on " + file); 1007 try (FileInputStream fis = new FileInputStream(file)) { 1008 mDevice.dumpWindowHierarchy(file); 1009 } 1010 } catch (Exception e) { 1011 Log.e(TAG, "error dumping screen on " + cause, e); 1012 } finally { 1013 takeScreenshotAndSave(); 1014 } 1015 } 1016 1017 // TODO(b/74358143): ideally we should take a screenshot limited by the boundaries of the 1018 // activity window, so external elements (such as the clock) are filtered out and don't cause 1019 // test flakiness when the contents are compared. 1020 public Bitmap takeScreenshot() { 1021 final long before = SystemClock.elapsedRealtime(); 1022 final Bitmap bitmap = mAutoman.takeScreenshot(); 1023 final long delta = SystemClock.elapsedRealtime() - before; 1024 Log.v(TAG, "Screenshot taken in " + delta + "ms"); 1025 return bitmap; 1026 } 1027 1028 /** 1029 * Takes a screenshot and save it in the file system for post-mortem analysis. 1030 */ 1031 public void takeScreenshotAndSave() { 1032 File file = null; 1033 try { 1034 file = Helper.createTestFile("screenshot.png"); 1035 if (file != null) { 1036 Log.i(TAG, "Taking screenshot on " + file); 1037 final Bitmap screenshot = takeScreenshot(); 1038 Helper.dumpBitmap(screenshot, file); 1039 } 1040 } catch (Exception e) { 1041 Log.e(TAG, "Error taking screenshot and saving on " + file, e); 1042 } 1043 } 1044 1045 /** 1046 * Asserts the contents of a child element. 1047 * 1048 * @param parent parent object 1049 * @param childId (relative) resource id of the child 1050 * @param assertion if {@code null}, asserts the child does not exist; otherwise, asserts the 1051 * child with it. 1052 */ 1053 public void assertChild(@NonNull UiObject2 parent, @NonNull String childId, 1054 @Nullable Visitor<UiObject2> assertion) { 1055 final UiObject2 child = parent.findObject(By.res(mPackageName, childId)); 1056 try { 1057 if (assertion != null) { 1058 assertWithMessage("Didn't find child with id '%s'", childId).that(child) 1059 .isNotNull(); 1060 try { 1061 assertion.visit(child); 1062 } catch (Throwable t) { 1063 throw new AssertionError("Error on child '" + childId + "'", t); 1064 } 1065 } else { 1066 assertWithMessage("Shouldn't find child with id '%s'", childId).that(child) 1067 .isNull(); 1068 } 1069 } catch (RuntimeException | Error e) { 1070 dumpScreen("assertChild(" + childId + ") failed: " + e); 1071 throw e; 1072 } 1073 } 1074 1075 /** 1076 * Waits until the window was split to show multiple activities. 1077 */ 1078 public void waitForWindowSplit() throws Exception { 1079 try { 1080 assertShownById(SPLIT_WINDOW_DIVIDER_ID); 1081 } catch (Exception e) { 1082 final long timeout = Timeouts.ACTIVITY_RESURRECTION.ms(); 1083 Log.e(TAG, "Did not find window divider " + SPLIT_WINDOW_DIVIDER_ID + "; waiting " 1084 + timeout + "ms instead"); 1085 SystemClock.sleep(timeout); 1086 } 1087 } 1088 } 1089