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.testcore; 18 19 import static android.autofillservice.cts.testcore.Timeouts.DATASET_PICKER_NOT_SHOWN_NAPTIME_MS; 20 import static android.autofillservice.cts.testcore.Timeouts.LONG_PRESS_MS; 21 import static android.autofillservice.cts.testcore.Timeouts.SAVE_NOT_SHOWN_NAPTIME_MS; 22 import static android.autofillservice.cts.testcore.Timeouts.SAVE_TIMEOUT; 23 import static android.autofillservice.cts.testcore.Timeouts.UI_DATASET_PICKER_TIMEOUT; 24 import static android.autofillservice.cts.testcore.Timeouts.UI_SCREEN_ORIENTATION_TIMEOUT; 25 import static android.autofillservice.cts.testcore.Timeouts.UI_TIMEOUT; 26 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS; 27 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD; 28 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_DEBIT_CARD; 29 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS; 30 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC; 31 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC_CARD; 32 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD; 33 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PAYMENT_CARD; 34 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_USERNAME; 35 36 import static com.android.compatibility.common.util.ShellUtils.runShellCommand; 37 38 import static com.google.common.truth.Truth.assertThat; 39 import static com.google.common.truth.Truth.assertWithMessage; 40 41 import static org.junit.Assume.assumeTrue; 42 43 import android.app.Activity; 44 import android.app.Instrumentation; 45 import android.app.UiAutomation; 46 import android.content.Context; 47 import android.content.res.Resources; 48 import android.graphics.Bitmap; 49 import android.graphics.Point; 50 import android.graphics.Rect; 51 import android.hardware.display.DisplayManager; 52 import android.os.SystemClock; 53 import android.service.autofill.SaveInfo; 54 import android.text.Html; 55 import android.text.Spanned; 56 import android.text.style.URLSpan; 57 import android.util.Log; 58 import android.view.Display; 59 import android.view.InputDevice; 60 import android.view.MotionEvent; 61 import android.view.Surface; 62 import android.view.View; 63 import android.view.WindowInsets; 64 import android.view.accessibility.AccessibilityEvent; 65 import android.view.accessibility.AccessibilityNodeInfo; 66 import android.view.accessibility.AccessibilityWindowInfo; 67 68 import androidx.annotation.NonNull; 69 import androidx.annotation.Nullable; 70 import androidx.test.platform.app.InstrumentationRegistry; 71 import androidx.test.uiautomator.By; 72 import androidx.test.uiautomator.BySelector; 73 import androidx.test.uiautomator.Configurator; 74 import androidx.test.uiautomator.Direction; 75 import androidx.test.uiautomator.SearchCondition; 76 import androidx.test.uiautomator.StaleObjectException; 77 import androidx.test.uiautomator.UiDevice; 78 import androidx.test.uiautomator.UiObject2; 79 import androidx.test.uiautomator.UiObjectNotFoundException; 80 import androidx.test.uiautomator.UiScrollable; 81 import androidx.test.uiautomator.UiSelector; 82 import androidx.test.uiautomator.Until; 83 84 import com.android.compatibility.common.util.RetryableException; 85 import com.android.compatibility.common.util.Timeout; 86 import com.android.compatibility.common.util.UserHelper; 87 88 import java.io.File; 89 import java.io.FileInputStream; 90 import java.util.ArrayList; 91 import java.util.Arrays; 92 import java.util.List; 93 import java.util.concurrent.TimeoutException; 94 95 /** 96 * Helper for UI-related needs. 97 */ 98 public class UiBot { 99 100 private static final String TAG = "AutoFillCtsUiBot"; 101 102 private static final String RESOURCE_ID_DATASET_PICKER = "autofill_dataset_picker"; 103 private static final String RESOURCE_ID_DATASET_HEADER = "autofill_dataset_header"; 104 private static final String RESOURCE_ID_SAVE_SNACKBAR = "autofill_save"; 105 private static final String RESOURCE_ID_SAVE_ICON = "autofill_save_icon"; 106 private static final String RESOURCE_ID_SAVE_TITLE = "autofill_save_title"; 107 private static final String RESOURCE_ID_CONTEXT_MENUITEM = "floating_toolbar_menu_item_text"; 108 private static final String RESOURCE_ID_SAVE_BUTTON_NO = "autofill_save_no"; 109 private static final String RESOURCE_ID_SAVE_BUTTON_YES = "autofill_save_yes"; 110 private static final String RESOURCE_ID_OVERFLOW = "overflow"; 111 112 private static final String RESOURCE_STRING_SAVE_TITLE = "autofill_save_title"; 113 private static final String RESOURCE_STRING_SAVE_TITLE_WITH_TYPE = 114 "autofill_save_title_with_type"; 115 private static final String RESOURCE_STRING_SAVE_TYPE_PASSWORD = "autofill_save_type_password"; 116 private static final String RESOURCE_STRING_SAVE_TYPE_ADDRESS = "autofill_save_type_address"; 117 private static final String RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD = 118 "autofill_save_type_credit_card"; 119 private static final String RESOURCE_STRING_SAVE_TYPE_USERNAME = "autofill_save_type_username"; 120 private static final String RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS = 121 "autofill_save_type_email_address"; 122 private static final String RESOURCE_STRING_SAVE_TYPE_DEBIT_CARD = 123 "autofill_save_type_debit_card"; 124 private static final String RESOURCE_STRING_SAVE_TYPE_PAYMENT_CARD = 125 "autofill_save_type_payment_card"; 126 private static final String RESOURCE_STRING_SAVE_TYPE_GENERIC_CARD = 127 "autofill_save_type_generic_card"; 128 private static final String RESOURCE_STRING_SAVE_BUTTON_NEVER = "autofill_save_never"; 129 private static final String RESOURCE_STRING_SAVE_BUTTON_NOT_NOW = "autofill_save_notnow"; 130 private static final String RESOURCE_STRING_SAVE_BUTTON_NO_THANKS = "autofill_save_no"; 131 private static final String RESOURCE_STRING_SAVE_BUTTON_YES = "autofill_save_yes"; 132 private static final String RESOURCE_STRING_UPDATE_BUTTON_YES = "autofill_update_yes"; 133 private static final String RESOURCE_STRING_CONTINUE_BUTTON_YES = "autofill_continue_yes"; 134 private static final String RESOURCE_STRING_UPDATE_TITLE = "autofill_update_title"; 135 private static final String RESOURCE_STRING_UPDATE_TITLE_WITH_TYPE = 136 "autofill_update_title_with_type"; 137 138 private static final String RESOURCE_STRING_AUTOFILL = "autofill"; 139 private static final String RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE = 140 "autofill_picker_accessibility_title"; 141 private static final String RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE = 142 "autofill_save_accessibility_title"; 143 144 private static final String RESOURCE_ID_FILL_DIALOG_PICKER = "autofill_dialog_picker"; 145 private static final String RESOURCE_ID_FILL_DIALOG_HEADER = "autofill_dialog_header"; 146 private static final String RESOURCE_ID_FILL_DIALOG_DATASET = "autofill_dialog_list"; 147 private static final String RESOURCE_ID_FILL_DIALOG_BUTTON_NO = "autofill_dialog_no"; 148 private static final String RESOURCE_ID_FILL_DIALOG_BUTTON_YES = "autofill_dialog_yes"; 149 150 static final BySelector DATASET_PICKER_SELECTOR = By.res("android", RESOURCE_ID_DATASET_PICKER); 151 private static final BySelector SAVE_UI_SELECTOR = By.res("android", RESOURCE_ID_SAVE_SNACKBAR); 152 private static final BySelector DATASET_HEADER_SELECTOR = 153 By.res("android", RESOURCE_ID_DATASET_HEADER); 154 private static final BySelector FILL_DIALOG_SELECTOR = 155 By.res("android", RESOURCE_ID_FILL_DIALOG_PICKER); 156 private static final BySelector FILL_DIALOG_HEADER_SELECTOR = 157 By.res("android", RESOURCE_ID_FILL_DIALOG_HEADER); 158 private static final BySelector FILL_DIALOG_DATASET_SELECTOR = 159 By.res("android", RESOURCE_ID_FILL_DIALOG_DATASET); 160 161 162 // TODO: figure out a more reliable solution that does not depend on SystemUI resources. 163 private static final String SPLIT_WINDOW_DIVIDER_ID = 164 "com.android.systemui:id/docked_divider_background"; 165 166 private static final boolean DUMP_ON_ERROR = true; 167 168 protected static final int MAX_UIOBJECT_RETRY_COUNT = 3; 169 170 /** 171 * Pass to {@link #setScreenOrientation(int)} to change the display to portrait mode. 172 * This is an alias of Surface.ROTATION_0 though it's named as PORTRAIT for historical reasons. 173 */ 174 public static final int PORTRAIT = Surface.ROTATION_0; 175 176 /** 177 * Pass to {@link #setScreenOrientation(int)} to change the display to landscape mode. 178 * This is an alias of Surface.ROTATION_90 though it's named as LANDSCAPE for historical 179 * reasons. 180 */ 181 public static final int LANDSCAPE = Surface.ROTATION_90; 182 183 private final UiDevice mDevice; 184 private final Context mContext; 185 private final UserHelper mUserHelper; 186 private final String mPackageName; 187 private final UiAutomation mAutoman; 188 private final Timeout mDefaultTimeout; 189 190 private boolean mOkToCallAssertNoDatasets; 191 UiBot()192 public UiBot() { 193 this(UI_TIMEOUT); 194 } 195 UiBot(Timeout defaultTimeout)196 public UiBot(Timeout defaultTimeout) { 197 mDefaultTimeout = defaultTimeout; 198 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 199 mDevice = UiDevice.getInstance(instrumentation); 200 mContext = instrumentation.getContext(); 201 mUserHelper = new UserHelper(mContext); 202 mPackageName = mContext.getPackageName(); 203 mAutoman = instrumentation.getUiAutomation(); 204 } 205 waitForIdle()206 public void waitForIdle() { 207 final long before = SystemClock.elapsedRealtimeNanos(); 208 mDevice.waitForIdle(); 209 final float delta = ((float) (SystemClock.elapsedRealtimeNanos() - before)) / 1_000_000; 210 Log.v(TAG, "device idle in " + delta + "ms"); 211 } 212 waitForIdleSync()213 public void waitForIdleSync() { 214 final long before = SystemClock.elapsedRealtimeNanos(); 215 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 216 final float delta = ((float) (SystemClock.elapsedRealtimeNanos() - before)) / 1_000_000; 217 Log.v(TAG, "device idle sync in " + delta + "ms"); 218 } 219 reset()220 public void reset() { 221 mOkToCallAssertNoDatasets = false; 222 } 223 224 /** 225 * Assumes the device has a minimum height and width of {@code minSize}, throwing a 226 * {@code AssumptionViolatedException} if it doesn't (so the test is skiped by the JUnit 227 * Runner). 228 */ assumeMinimumResolution(int minSize)229 public void assumeMinimumResolution(int minSize) { 230 final int width = mDevice.getDisplayWidth(); 231 final int heigth = mDevice.getDisplayHeight(); 232 final int min = Math.min(width, heigth); 233 assumeTrue("Screen size is too small (" + width + "x" + heigth + ")", min >= minSize); 234 Log.d(TAG, "assumeMinimumResolution(" + minSize + ") passed: screen size is " 235 + width + "x" + heigth); 236 } 237 238 /** 239 * Assumes the device has a minimum height and width of {@code minSizeDp}, throwing a 240 * {@code AssumptionViolatedException} if it doesn't (so the test is skipped by the JUnit 241 * Runner). 242 */ assumeMinimumResolutionInDp(int minSizeDp)243 public void assumeMinimumResolutionInDp(int minSizeDp) { 244 final int widthInDp = mDevice.getDisplaySizeDp().x; 245 final int heightInDp = mDevice.getDisplaySizeDp().y; 246 final int min = Math.min(widthInDp, heightInDp); 247 assumeTrue("Screen size is too small (" + widthInDp + "x" + heightInDp + ")", 248 min >= minSizeDp); 249 Log.d(TAG, "assumeMinimumResolutionInDp(" + minSizeDp + ") passed: screen size is " 250 + widthInDp + "x" + heightInDp); 251 } 252 253 /** 254 * Sets the screen resolution in a way that the IME doesn't interfere with the Autofill UI 255 * when the device is rotated to landscape. 256 * 257 * When called, test must call <p>{@link #resetScreenResolution()} in a {@code finally} block. 258 * 259 * @deprecated this method should not be necessarily anymore as we're using a MockIme. 260 */ 261 @Deprecated 262 // TODO: remove once we're sure no more OEM is getting failure due to screen size setScreenResolution()263 public void setScreenResolution() { 264 if (false) { 265 Log.w(TAG, "setScreenResolution(): ignored"); 266 return; 267 } 268 assumeMinimumResolution(500); 269 270 runShellCommand("wm size 1080x1920"); 271 runShellCommand("wm density 320"); 272 } 273 274 /** 275 * Resets the screen resolution. 276 * 277 * <p>Should always be called after {@link #setScreenResolution()}. 278 * 279 * @deprecated this method should not be necessarily anymore as we're using a MockIme. 280 */ 281 @Deprecated 282 // TODO: remove once we're sure no more OEM is getting failure due to screen size resetScreenResolution()283 public void resetScreenResolution() { 284 if (false) { 285 Log.w(TAG, "resetScreenResolution(): ignored"); 286 return; 287 } 288 runShellCommand("wm density reset"); 289 runShellCommand("wm size reset"); 290 } 291 292 /** 293 * Asserts the dataset picker is not shown anymore. 294 * 295 * @throws IllegalStateException if called *before* an assertion was made to make sure the 296 * dataset picker is shown - if that's not the case, call 297 * {@link #assertNoDatasetsEver()} instead. 298 */ assertNoDatasets()299 public void assertNoDatasets() throws Exception { 300 if (!mOkToCallAssertNoDatasets) { 301 throw new IllegalStateException( 302 "Cannot call assertNoDatasets() without calling assertDatasets first"); 303 } 304 mDevice.wait(Until.gone(DATASET_PICKER_SELECTOR), UI_DATASET_PICKER_TIMEOUT.ms()); 305 mOkToCallAssertNoDatasets = false; 306 } 307 308 /** 309 * Asserts the dataset picker was never shown. 310 * 311 * <p>This method is slower than {@link #assertNoDatasets()} and should only be called in the 312 * cases where the dataset picker was not previous shown. 313 */ assertNoDatasetsEver()314 public void assertNoDatasetsEver() throws Exception { 315 assertNeverShown("dataset picker", DATASET_PICKER_SELECTOR, 316 DATASET_PICKER_NOT_SHOWN_NAPTIME_MS); 317 } 318 319 /** 320 * Asserts the dataset chooser is shown and contains exactly the given datasets. 321 * 322 * @return the dataset picker object. 323 */ assertDatasets(String...names)324 public UiObject2 assertDatasets(String...names) throws Exception { 325 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 326 return assertDatasets(picker, names); 327 } 328 assertDatasets(UiObject2 picker, String...names)329 protected UiObject2 assertDatasets(UiObject2 picker, String...names) { 330 assertWithMessage("wrong dataset names").that(getChildrenAsText(picker)) 331 .containsExactlyElementsIn(Arrays.asList(names)).inOrder(); 332 return picker; 333 } 334 335 /** 336 * Asserts the dataset chooser is shown and contains the given datasets. 337 * 338 * @return the dataset picker object. 339 */ assertDatasetsContains(String...names)340 public UiObject2 assertDatasetsContains(String...names) throws Exception { 341 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 342 assertWithMessage("wrong dataset names").that(getChildrenAsText(picker)) 343 .containsAtLeastElementsIn(Arrays.asList(names)).inOrder(); 344 return picker; 345 } 346 347 /** 348 * Asserts the dataset chooser is shown and contains the given datasets, header, and footer. 349 * <p>In fullscreen, header view is not under R.id.autofill_dataset_picker. 350 * 351 * @return the dataset picker object. 352 */ assertDatasetsWithBorders(String header, String footer, String...names)353 public UiObject2 assertDatasetsWithBorders(String header, String footer, String...names) 354 throws Exception { 355 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 356 final List<String> expectedChild = new ArrayList<>(); 357 if (header != null) { 358 if (Helper.isAutofillWindowFullScreen(mContext)) { 359 final UiObject2 headerView = waitForObject(DATASET_HEADER_SELECTOR, 360 UI_DATASET_PICKER_TIMEOUT); 361 assertWithMessage("fullscreen wrong dataset header") 362 .that(getChildrenAsText(headerView)) 363 .containsExactlyElementsIn(Arrays.asList(header)).inOrder(); 364 } else { 365 expectedChild.add(header); 366 } 367 } 368 expectedChild.addAll(Arrays.asList(names)); 369 if (footer != null) { 370 expectedChild.add(footer); 371 } 372 assertWithMessage("wrong elements on dataset picker").that(getChildrenAsText(picker)) 373 .containsExactlyElementsIn(expectedChild).inOrder(); 374 return picker; 375 } 376 377 /** 378 * Gets the text of this object children. 379 */ getChildrenAsText(UiObject2 object)380 public List<String> getChildrenAsText(UiObject2 object) { 381 final List<String> list = new ArrayList<>(); 382 getChildrenAsText(object, list); 383 return list; 384 } 385 getChildrenAsText(UiObject2 object, List<String> children)386 private static void getChildrenAsText(UiObject2 object, List<String> children) { 387 final String text = object.getText(); 388 if (text != null) { 389 children.add(text); 390 } 391 for (UiObject2 child : object.getChildren()) { 392 getChildrenAsText(child, children); 393 } 394 } 395 396 /** 397 * Selects a dataset that should be visible in the floating UI and does not need to wait for 398 * application become idle. 399 */ selectDataset(String name)400 public void selectDataset(String name) throws Exception { 401 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 402 selectDataset(picker, name); 403 } 404 405 /** 406 * Selects a dataset that should be visible in the floating UI and waits for application become 407 * idle if needed. 408 */ selectDatasetSync(String name)409 public void selectDatasetSync(String name) throws Exception { 410 final UiObject2 picker = findDatasetPicker(UI_DATASET_PICKER_TIMEOUT); 411 selectDataset(picker, name); 412 mDevice.waitForIdle(); 413 } 414 415 /** 416 * Selects a dataset that should be visible in the floating UI. 417 */ selectDataset(UiObject2 picker, String name)418 public void selectDataset(UiObject2 picker, String name) { 419 final UiObject2 dataset = picker.findObject(By.text(name)); 420 if (dataset == null) { 421 throw new AssertionError("no dataset " + name + " in " + getChildrenAsText(picker)); 422 } 423 dataset.click(); 424 } 425 426 /** 427 * Finds the suggestion by name and perform long click on suggestion to trigger attribution 428 * intent. 429 */ longPressSuggestion(String name)430 public void longPressSuggestion(String name) throws Exception { 431 throw new UnsupportedOperationException(); 432 } 433 434 /** 435 * Asserts the suggestion chooser is shown in the suggestion view. 436 */ assertSuggestion(String name)437 public void assertSuggestion(String name) throws Exception { 438 throw new UnsupportedOperationException(); 439 } 440 441 /** 442 * Asserts the suggestion chooser is not shown in the suggestion view. 443 */ assertNoSuggestion(String name)444 public void assertNoSuggestion(String name) throws Exception { 445 throw new UnsupportedOperationException(); 446 } 447 448 /** 449 * Scrolls the suggestion view. 450 * 451 * @param direction The direction to scroll. 452 * @param speed The speed to scroll per second. 453 */ scrollSuggestionView(Direction direction, int speed)454 public void scrollSuggestionView(Direction direction, int speed) throws Exception { 455 throw new UnsupportedOperationException(); 456 } 457 458 /** 459 * Selects a view by text. 460 * 461 * <p><b>NOTE:</b> Not recommended - prefer other options such as {@link 462 * #selectDataset(String)} when selecting an option in dataset picker is shown, or {@link 463 * #selectByRelativeId(String)} when selecting things inside of the activities. 464 */ selectByText(String name)465 public void selectByText(String name) throws Exception { 466 Log.v(TAG, "selectByText(): " + name); 467 468 final UiObject2 object = waitForObject(By.text(name)); 469 object.click(); 470 } 471 472 /** 473 * Asserts a text is shown. 474 * 475 * <p><b>NOTE:</b> when asserting the dataset picker is shown, prefer 476 * {@link #assertDatasets(String...)}. 477 */ assertShownByText(String text)478 public UiObject2 assertShownByText(String text) throws Exception { 479 return assertShownByText(text, mDefaultTimeout); 480 } 481 assertShownByText(String text, Timeout timeout)482 public UiObject2 assertShownByText(String text, Timeout timeout) throws Exception { 483 final UiObject2 object = waitForObject(By.text(text), timeout); 484 assertWithMessage("No node with text '%s'", text).that(object).isNotNull(); 485 return object; 486 } 487 488 /** 489 * Finds a node by text, without waiting for it to be shown (but failing if it isn't). 490 */ 491 @NonNull findRightAwayByText(@onNull String text)492 public UiObject2 findRightAwayByText(@NonNull String text) throws Exception { 493 final UiObject2 object = mDevice.findObject(By.text(text)); 494 assertWithMessage("no UIObject for text '%s'", text).that(object).isNotNull(); 495 return object; 496 } 497 498 /** 499 * Asserts that the text is not showing for sure in the screen "as is", i.e., without waiting 500 * for it. 501 * 502 * <p>Typically called after another assertion that waits for a condition to be shown. 503 */ assertNotShowingForSure(String text)504 public void assertNotShowingForSure(String text) throws Exception { 505 final UiObject2 object = mDevice.findObject(By.text(text)); 506 assertWithMessage("Found node with text '%s'", text).that(object).isNull(); 507 } 508 509 /** 510 * Asserts a node with the given content description is shown. 511 * 512 */ assertShownByContentDescription(String contentDescription)513 public UiObject2 assertShownByContentDescription(String contentDescription) throws Exception { 514 final UiObject2 object = waitForObject(By.desc(contentDescription)); 515 assertWithMessage("No node with content description '%s'", contentDescription).that(object) 516 .isNotNull(); 517 return object; 518 } 519 520 /** 521 * Checks if a View with a certain text exists. 522 */ hasViewWithText(String name)523 public boolean hasViewWithText(String name) { 524 Log.v(TAG, "hasViewWithText(): " + name); 525 526 return mDevice.findObject(By.text(name)) != null; 527 } 528 529 /** 530 * Selects a view by id. 531 */ selectByRelativeId(String id)532 public UiObject2 selectByRelativeId(String id) throws Exception { 533 return selectByRelativeId(mPackageName, id); 534 } 535 selectByRelativeId(String packageName, String id)536 public UiObject2 selectByRelativeId(String packageName, String id) throws Exception { 537 Log.v(TAG, "selectByRelativeId(): " + packageName + ":/" + id); 538 UiObject2 object = waitForObject(By.res(packageName, id)); 539 object.click(); 540 return object; 541 } 542 543 /** 544 * Asserts the id is shown on the screen. 545 */ assertShownById(String id)546 public UiObject2 assertShownById(String id) throws Exception { 547 final UiObject2 object = waitForObject(By.res(id)); 548 assertThat(object).isNotNull(); 549 return object; 550 } 551 552 /** 553 * Asserts the id is shown on the screen, using a resource id from the test package. 554 */ assertShownByRelativeId(String id)555 public UiObject2 assertShownByRelativeId(String id) throws Exception { 556 return assertShownByRelativeId(id, mDefaultTimeout); 557 } 558 assertShownByRelativeId(String id, Timeout timeout)559 public UiObject2 assertShownByRelativeId(String id, Timeout timeout) throws Exception { 560 final UiObject2 obj = waitForObject(By.res(mPackageName, id), timeout); 561 assertThat(obj).isNotNull(); 562 return obj; 563 } 564 565 /** 566 * Asserts the id is not shown on the screen anymore, using a resource id from the test package. 567 * 568 * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise 569 * it might pass without really asserting anything. 570 */ assertGoneByRelativeId(@onNull String id, @NonNull Timeout timeout)571 public void assertGoneByRelativeId(@NonNull String id, @NonNull Timeout timeout) { 572 assertGoneByRelativeId(/* parent = */ null, id, timeout); 573 } 574 assertGoneByRelativeId(int resId, @NonNull Timeout timeout)575 public void assertGoneByRelativeId(int resId, @NonNull Timeout timeout) { 576 assertGoneByRelativeId(/* parent = */ null, getIdName(resId), timeout); 577 } 578 getIdName(int resId)579 private String getIdName(int resId) { 580 return mContext.getResources().getResourceEntryName(resId); 581 } 582 583 /** 584 * Asserts the id is not shown on the parent anymore, using a resource id from the test package. 585 * 586 * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise 587 * it might pass without really asserting anything. 588 */ assertGoneByRelativeId(@ullable UiObject2 parent, @NonNull String id, @NonNull Timeout timeout)589 public void assertGoneByRelativeId(@Nullable UiObject2 parent, @NonNull String id, 590 @NonNull Timeout timeout) { 591 final SearchCondition<Boolean> condition = Until.gone(By.res(mPackageName, id)); 592 final boolean gone = parent != null 593 ? parent.wait(condition, timeout.ms()) 594 : mDevice.wait(condition, timeout.ms()); 595 if (!gone) { 596 final String message = "Object with id '" + id + "' should be gone after " 597 + timeout + " ms"; 598 dumpScreen(message); 599 throw new RetryableException(message); 600 } 601 } 602 assertShownByRelativeId(int resId)603 public UiObject2 assertShownByRelativeId(int resId) throws Exception { 604 return assertShownByRelativeId(getIdName(resId)); 605 } 606 assertNeverShownByRelativeId(@onNull String description, int resId, long timeout)607 public void assertNeverShownByRelativeId(@NonNull String description, int resId, long timeout) 608 throws Exception { 609 final BySelector selector = By.res(Helper.MY_PACKAGE, getIdName(resId)); 610 assertNeverShown(description, selector, timeout); 611 } 612 613 /** 614 * Asserts that a {@code selector} is not showing after {@code timeout} milliseconds. 615 */ assertNeverShown(String description, BySelector selector, long timeout)616 protected void assertNeverShown(String description, BySelector selector, long timeout) 617 throws Exception { 618 SystemClock.sleep(timeout); 619 final UiObject2 object = mDevice.findObject(selector); 620 if (object != null) { 621 throw new AssertionError( 622 String.format("Should not be showing %s after %dms, but got %s", 623 description, timeout, getChildrenAsText(object))); 624 } 625 } 626 627 /** 628 * Gets the text set on a view. 629 */ getTextByRelativeId(String id)630 public String getTextByRelativeId(String id) throws Exception { 631 return waitForObject(By.res(mPackageName, id)).getText(); 632 } 633 634 /** 635 * Focus in the view with the given resource id. 636 */ focusByRelativeId(String id)637 public void focusByRelativeId(String id) throws Exception { 638 waitForObject(By.res(mPackageName, id)).click(); 639 } 640 641 /** 642 * Sets a new text on a view. 643 */ setTextByRelativeId(String id, String newText)644 public void setTextByRelativeId(String id, String newText) throws Exception { 645 waitForObject(By.res(mPackageName, id)).setText(newText); 646 } 647 648 /** 649 * Sets a new text on a view. 650 * 651 * <p><b>Note:</b> First clear the view to the first character of the old string, then clear to 652 * empty. This is to accommodate the fix for the bug where views are reset to empty, causing 653 * save dialog to not show. The fix for this bug is to ignore sudden resets to empty, therefore 654 * CTS tests simulating field clearing have to progressively clear the field instead of 655 * resetting to empty at once. 656 */ clearTextByRelativeId(String id)657 public void clearTextByRelativeId(String id) throws Exception { 658 final UiObject2 object = waitForObject(By.res(mPackageName, id)); 659 String oldText = object.getText(); 660 if (!oldText.isEmpty()) { 661 object.setText(String.valueOf(oldText.charAt(0))); 662 object.setText(""); 663 } 664 } 665 666 /** 667 * Asserts the save snackbar is showing and returns it. 668 */ assertSaveShowing(int type)669 public UiObject2 assertSaveShowing(int type) throws Exception { 670 return assertSaveShowing(SAVE_TIMEOUT, type); 671 } 672 673 /** 674 * Asserts the save snackbar is showing with a custom service name and returns it. 675 */ assertSaveShowingWithCustomServiceName(int type, String customServiceName)676 public UiObject2 assertSaveShowingWithCustomServiceName(int type, String customServiceName) 677 throws Exception { 678 return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 679 SaveInfo.POSITIVE_BUTTON_STYLE_SAVE, null, SAVE_TIMEOUT, customServiceName, type); 680 } 681 682 /** 683 * Asserts the save snackbar is showing and returns it. 684 */ assertSaveShowing(Timeout timeout, int type)685 public UiObject2 assertSaveShowing(Timeout timeout, int type) throws Exception { 686 return assertSaveShowing(null, timeout, type); 687 } 688 689 /** 690 * Asserts the save snackbar is showing with the Update message and returns it. 691 */ assertUpdateShowing(int... types)692 public UiObject2 assertUpdateShowing(int... types) throws Exception { 693 return assertSaveOrUpdateShowing(/* update= */ true, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 694 null, SAVE_TIMEOUT, types); 695 } 696 697 /** 698 * Presses the Back button. 699 */ pressBack()700 public void pressBack() { 701 Log.d(TAG, "pressBack()"); 702 mDevice.pressBack(); 703 } 704 705 /** 706 * Presses the Home button. 707 */ pressHome()708 public void pressHome() { 709 Log.d(TAG, "pressHome()"); 710 mDevice.pressHome(); 711 } 712 713 /** 714 * Asserts the save snackbar is not showing. 715 */ assertSaveNotShowing(int type)716 public void assertSaveNotShowing(int type) throws Exception { 717 assertNeverShown("save UI for type " + saveTypeToString(type), SAVE_UI_SELECTOR, 718 SAVE_NOT_SHOWN_NAPTIME_MS); 719 } 720 721 /** 722 * Asserts the save snackbar is not showing, explaining when. 723 */ assertSaveNotShowing(int type, @Nullable String when)724 public void assertSaveNotShowing(int type, @Nullable String when) throws Exception { 725 String suffix = when == null ? "" : " when " + when; 726 assertNeverShown("save UI for type " + saveTypeToString(type) + suffix, SAVE_UI_SELECTOR, 727 SAVE_NOT_SHOWN_NAPTIME_MS); 728 } 729 assertSaveNotShowing()730 public void assertSaveNotShowing() throws Exception { 731 assertNeverShown("save UI", SAVE_UI_SELECTOR, SAVE_NOT_SHOWN_NAPTIME_MS); 732 } 733 getSaveTypeString(int type)734 private String getSaveTypeString(int type) { 735 final String typeResourceName; 736 switch (type) { 737 case SAVE_DATA_TYPE_PASSWORD: 738 typeResourceName = RESOURCE_STRING_SAVE_TYPE_PASSWORD; 739 break; 740 case SAVE_DATA_TYPE_ADDRESS: 741 typeResourceName = RESOURCE_STRING_SAVE_TYPE_ADDRESS; 742 break; 743 case SAVE_DATA_TYPE_CREDIT_CARD: 744 typeResourceName = RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD; 745 break; 746 case SAVE_DATA_TYPE_USERNAME: 747 typeResourceName = RESOURCE_STRING_SAVE_TYPE_USERNAME; 748 break; 749 case SAVE_DATA_TYPE_EMAIL_ADDRESS: 750 typeResourceName = RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS; 751 break; 752 case SAVE_DATA_TYPE_DEBIT_CARD: 753 typeResourceName = RESOURCE_STRING_SAVE_TYPE_DEBIT_CARD; 754 break; 755 case SAVE_DATA_TYPE_PAYMENT_CARD: 756 typeResourceName = RESOURCE_STRING_SAVE_TYPE_PAYMENT_CARD; 757 break; 758 case SAVE_DATA_TYPE_GENERIC_CARD: 759 typeResourceName = RESOURCE_STRING_SAVE_TYPE_GENERIC_CARD; 760 break; 761 default: 762 throw new IllegalArgumentException("Unsupported type: " + type); 763 } 764 return getString(typeResourceName); 765 } 766 saveTypeToString(int type)767 private String saveTypeToString(int type) { 768 // Cannot use DebugUtils, it's @hide 769 switch (type) { 770 case SAVE_DATA_TYPE_PASSWORD: 771 return "PASSWORD"; 772 case SAVE_DATA_TYPE_ADDRESS: 773 return "ADDRESS"; 774 case SAVE_DATA_TYPE_CREDIT_CARD: 775 return "CREDIT_CARD"; 776 case SAVE_DATA_TYPE_USERNAME: 777 return "USERNAME"; 778 case SAVE_DATA_TYPE_EMAIL_ADDRESS: 779 return "EMAIL_ADDRESS"; 780 case SAVE_DATA_TYPE_DEBIT_CARD: 781 return "DEBIT_CARD"; 782 case SAVE_DATA_TYPE_PAYMENT_CARD: 783 return "PAYMENT_CARD"; 784 case SAVE_DATA_TYPE_GENERIC_CARD: 785 return "GENERIC_CARD"; 786 default: 787 return "UNSUPPORT_TYPE_" + type; 788 } 789 } 790 assertSaveShowing(String description, int... types)791 public UiObject2 assertSaveShowing(String description, int... types) throws Exception { 792 return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 793 description, SAVE_TIMEOUT, types); 794 } 795 assertSaveShowing(String description, Timeout timeout, int... types)796 public UiObject2 assertSaveShowing(String description, Timeout timeout, int... types) 797 throws Exception { 798 return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 799 description, timeout, types); 800 } 801 assertSaveShowing(int negativeButtonStyle, String description, int... types)802 public UiObject2 assertSaveShowing(int negativeButtonStyle, String description, 803 int... types) throws Exception { 804 return assertSaveOrUpdateShowing(/* update= */ false, negativeButtonStyle, description, 805 SAVE_TIMEOUT, types); 806 } 807 assertSaveShowing(int positiveButtonStyle, int... types)808 public UiObject2 assertSaveShowing(int positiveButtonStyle, int... types) throws Exception { 809 return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 810 positiveButtonStyle, /* description= */ null, SAVE_TIMEOUT, types); 811 } 812 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, String description, Timeout timeout, int... types)813 public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, 814 String description, Timeout timeout, int... types) throws Exception { 815 return assertSaveOrUpdateShowing(update, negativeButtonStyle, 816 SaveInfo.POSITIVE_BUTTON_STYLE_SAVE, description, timeout, types); 817 } 818 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, int positiveButtonStyle, String description, Timeout timeout, int... types)819 public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, 820 int positiveButtonStyle, String description, Timeout timeout, int... types) 821 throws Exception { 822 return assertSaveOrUpdateShowing(update, negativeButtonStyle, positiveButtonStyle, 823 description, timeout, InstrumentedAutoFillService.getServiceLabel(), types); 824 } 825 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, int positiveButtonStyle, String description, Timeout timeout, String serviceLabel, int... types)826 public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, 827 int positiveButtonStyle, String description, Timeout timeout, String serviceLabel, 828 int... types) throws Exception { 829 830 final UiObject2 snackbar = waitForObject(SAVE_UI_SELECTOR, timeout); 831 832 final UiObject2 titleView = 833 waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_TITLE), timeout); 834 assertWithMessage("save title (%s) is not shown", RESOURCE_ID_SAVE_TITLE).that(titleView) 835 .isNotNull(); 836 837 final UiObject2 iconView = 838 waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_ICON), timeout); 839 assertWithMessage("save icon (%s) is not shown", RESOURCE_ID_SAVE_ICON).that(iconView) 840 .isNotNull(); 841 842 final String actualTitle = titleView.getText(); 843 Log.d(TAG, "save title: " + actualTitle); 844 845 final String titleId, titleWithTypeId; 846 if (update) { 847 titleId = RESOURCE_STRING_UPDATE_TITLE; 848 titleWithTypeId = RESOURCE_STRING_UPDATE_TITLE_WITH_TYPE; 849 } else { 850 titleId = RESOURCE_STRING_SAVE_TITLE; 851 titleWithTypeId = RESOURCE_STRING_SAVE_TITLE_WITH_TYPE; 852 } 853 854 switch (types.length) { 855 case 1: 856 final String expectedTitle = (types[0] == SAVE_DATA_TYPE_GENERIC) 857 ? Html.fromHtml(getString(titleId, serviceLabel), 0).toString() 858 : Html.fromHtml(getString(titleWithTypeId, 859 getSaveTypeString(types[0]), serviceLabel), 0).toString(); 860 assertThat(actualTitle).isEqualTo(expectedTitle); 861 break; 862 case 2: 863 // We cannot predict the order... 864 assertThat(actualTitle).contains(getSaveTypeString(types[0])); 865 assertThat(actualTitle).contains(getSaveTypeString(types[1])); 866 break; 867 case 3: 868 // We cannot predict the order... 869 assertThat(actualTitle).contains(getSaveTypeString(types[0])); 870 assertThat(actualTitle).contains(getSaveTypeString(types[1])); 871 assertThat(actualTitle).contains(getSaveTypeString(types[2])); 872 break; 873 default: 874 throw new IllegalArgumentException("Invalid types: " + Arrays.toString(types)); 875 } 876 877 if (description != null) { 878 final UiObject2 saveSubTitle = snackbar.findObject(By.text(description)); 879 assertWithMessage("save subtitle(%s)", description).that(saveSubTitle).isNotNull(); 880 } 881 882 final String positiveButtonStringId; 883 switch (positiveButtonStyle) { 884 case SaveInfo.POSITIVE_BUTTON_STYLE_CONTINUE: 885 positiveButtonStringId = RESOURCE_STRING_CONTINUE_BUTTON_YES; 886 break; 887 default: 888 positiveButtonStringId = update ? RESOURCE_STRING_UPDATE_BUTTON_YES 889 : RESOURCE_STRING_SAVE_BUTTON_YES; 890 } 891 final String expectedPositiveButtonText = getString(positiveButtonStringId).toUpperCase(); 892 final UiObject2 positiveButton = waitForObject(snackbar, 893 By.res("android", RESOURCE_ID_SAVE_BUTTON_YES), timeout); 894 assertWithMessage("wrong text on positive button") 895 .that(positiveButton.getText().toUpperCase()).isEqualTo(expectedPositiveButtonText); 896 897 final String negativeButtonStringId; 898 if (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) { 899 negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NOT_NOW; 900 } else if (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_NEVER) { 901 negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NEVER; 902 } else { 903 negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NO_THANKS; 904 } 905 final String expectedNegativeButtonText = getString(negativeButtonStringId).toUpperCase(); 906 final UiObject2 negativeButton = waitForObject(snackbar, 907 By.res("android", RESOURCE_ID_SAVE_BUTTON_NO), timeout); 908 assertWithMessage("wrong text on negative button") 909 .that(negativeButton.getText().toUpperCase()).isEqualTo(expectedNegativeButtonText); 910 911 final String expectedAccessibilityTitle = 912 getString(RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE); 913 timeout.run( 914 String.format( 915 "assertAccessibilityTitle(%s, %s)", 916 snackbar, 917 expectedAccessibilityTitle), 918 () -> { 919 try { 920 assertAccessibilityTitle(snackbar, expectedAccessibilityTitle); 921 } catch (RetryableException e) { 922 return null; 923 } 924 return true; 925 }); 926 return snackbar; 927 } 928 929 /** 930 * Taps an option in the save snackbar. 931 * 932 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 933 * @param types expected types of save info. 934 */ saveForAutofill(boolean yesDoIt, int... types)935 public void saveForAutofill(boolean yesDoIt, int... types) throws Exception { 936 final UiObject2 saveSnackBar = assertSaveShowing( 937 SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, types); 938 saveForAutofill(saveSnackBar, yesDoIt); 939 } 940 updateForAutofill(boolean yesDoIt, int... types)941 public void updateForAutofill(boolean yesDoIt, int... types) throws Exception { 942 final UiObject2 saveUi = assertUpdateShowing(types); 943 saveForAutofill(saveUi, yesDoIt); 944 } 945 946 /** 947 * Taps an option in the save snackbar. 948 * 949 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 950 * @param types expected types of save info. 951 */ saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types)952 public void saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types) 953 throws Exception { 954 final UiObject2 saveSnackBar = assertSaveShowing(negativeButtonStyle, null, types); 955 saveForAutofill(saveSnackBar, yesDoIt); 956 } 957 958 /** 959 * Taps the positive button in the save snackbar. 960 * 961 * @param types expected types of save info. 962 */ saveForAutofill(int positiveButtonStyle, int... types)963 public void saveForAutofill(int positiveButtonStyle, int... types) throws Exception { 964 final UiObject2 saveSnackBar = assertSaveShowing(positiveButtonStyle, types); 965 saveForAutofill(saveSnackBar, /* yesDoIt= */ true); 966 } 967 968 /** 969 * Taps an option in the save snackbar. 970 * 971 * @param saveSnackBar Save snackbar, typically obtained through 972 * {@link #assertSaveShowing(int)}. 973 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 974 */ saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt)975 public void saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt) { 976 final String id = yesDoIt ? "autofill_save_yes" : "autofill_save_no"; 977 978 final UiObject2 button = saveSnackBar.findObject(By.res("android", id)); 979 assertWithMessage("save button (%s)", id).that(button).isNotNull(); 980 button.click(); 981 } 982 983 /** 984 * Gets the AUTOFILL contextual menu by long pressing a text field. 985 * 986 * <p><b>NOTE:</b> this method should only be called in scenarios where we explicitly want to 987 * test the overflow menu. For all other scenarios where we want to test manual autofill, it's 988 * better to call {@code AFM.requestAutofill()} directly, because it's less error-prone and 989 * faster. 990 * 991 * @param id resource id of the field. 992 */ getAutofillMenuOption(String id)993 public UiObject2 getAutofillMenuOption(String id) throws Exception { 994 final UiObject2 field = waitForObject(By.res(mPackageName, id)); 995 // TODO: figure out why obj.longClick() doesn't always work 996 field.click(LONG_PRESS_MS); 997 998 List<UiObject2> menuItems = waitForObjects( 999 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout); 1000 final String expectedText = getAutofillContextualMenuTitle(); 1001 1002 final StringBuffer menuNames = new StringBuffer(); 1003 1004 // Check first menu for AUTOFILL 1005 for (UiObject2 menuItem : menuItems) { 1006 final String menuName = menuItem.getText(); 1007 if (menuName.equalsIgnoreCase(expectedText)) { 1008 Log.v(TAG, "AUTOFILL found in first menu"); 1009 return menuItem; 1010 } 1011 menuNames.append("'").append(menuName).append("' "); 1012 } 1013 1014 menuNames.append(";"); 1015 1016 // First menu does not have AUTOFILL, check overflow 1017 final BySelector overflowSelector = By.res("android", RESOURCE_ID_OVERFLOW); 1018 1019 // Click overflow menu button. 1020 final UiObject2 overflowMenu = waitForObject(overflowSelector, mDefaultTimeout); 1021 overflowMenu.click(); 1022 1023 // Wait for overflow menu to show. 1024 mDevice.wait(Until.gone(overflowSelector), 1000); 1025 1026 menuItems = waitForObjects( 1027 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout); 1028 for (UiObject2 menuItem : menuItems) { 1029 final String menuName = menuItem.getText(); 1030 if (menuName.equalsIgnoreCase(expectedText)) { 1031 Log.v(TAG, "AUTOFILL found in overflow menu"); 1032 return menuItem; 1033 } 1034 menuNames.append("'").append(menuName).append("' "); 1035 } 1036 throw new RetryableException("no '%s' on '%s'", expectedText, menuNames); 1037 } 1038 getAutofillContextualMenuTitle()1039 String getAutofillContextualMenuTitle() { 1040 return getString(RESOURCE_STRING_AUTOFILL); 1041 } 1042 1043 /** 1044 * Gets a string from the Android resources. 1045 */ getString(String id)1046 private String getString(String id) { 1047 final Resources resources = mContext.getResources(); 1048 final int stringId = resources.getIdentifier(id, "string", "android"); 1049 try { 1050 return resources.getString(stringId); 1051 } catch (Resources.NotFoundException e) { 1052 throw new IllegalStateException("no internal string for '" + id + "' / res=" + stringId 1053 + ": ", e); 1054 } 1055 } 1056 1057 /** 1058 * Gets a string from the Android resources. 1059 */ getString(String id, Object... formatArgs)1060 private String getString(String id, Object... formatArgs) { 1061 final Resources resources = mContext.getResources(); 1062 final int stringId = resources.getIdentifier(id, "string", "android"); 1063 try { 1064 return resources.getString(stringId, formatArgs); 1065 } catch (Resources.NotFoundException e) { 1066 throw new IllegalStateException("no internal string for '" + id + "' / res=" + stringId 1067 + ": ", e); 1068 } 1069 } 1070 1071 /** 1072 * Waits for and returns an object. 1073 * 1074 * @param selector {@link BySelector} that identifies the object. 1075 */ waitForObject(BySelector selector)1076 private UiObject2 waitForObject(BySelector selector) throws Exception { 1077 return waitForObject(selector, mDefaultTimeout); 1078 } 1079 1080 /** 1081 * Waits for and returns an object. 1082 * 1083 * @param parent where to find the object (or {@code null} to use device's root). 1084 * @param selector {@link BySelector} that identifies the object. 1085 * @param timeout timeout in ms. 1086 * @param dumpOnError whether the window hierarchy should be dumped if the object is not found. 1087 */ waitForObject(UiObject2 parent, BySelector selector, Timeout timeout, boolean dumpOnError)1088 private UiObject2 waitForObject(UiObject2 parent, BySelector selector, Timeout timeout, 1089 boolean dumpOnError) throws Exception { 1090 // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach. 1091 try { 1092 return timeout.run("waitForObject(" + selector + ")", () -> { 1093 return parent != null 1094 ? parent.findObject(selector) 1095 : mDevice.findObject(selector); 1096 1097 }); 1098 } catch (RetryableException e) { 1099 if (dumpOnError) { 1100 dumpScreen("waitForObject() for " + selector + "on " 1101 + (parent == null ? "mDevice" : parent) + " failed"); 1102 } 1103 throw e; 1104 } 1105 } 1106 waitForObject(@ullable UiObject2 parent, @NonNull BySelector selector, @NonNull Timeout timeout)1107 public UiObject2 waitForObject(@Nullable UiObject2 parent, @NonNull BySelector selector, 1108 @NonNull Timeout timeout) 1109 throws Exception { 1110 return waitForObject(parent, selector, timeout, DUMP_ON_ERROR); 1111 } 1112 1113 /** 1114 * Waits and returns an object that is a child of a parent {@link UiObject2} base on string 1115 * resource id. 1116 * 1117 * @param parent where to find the object (or {@code null} to use device's root). 1118 * @param resourceId resource id of the child. 1119 * @param timeout specifies the timeout for retries 1120 * @return the object that is found 1121 * @throws Exception if the object is not found after the timeout 1122 */ waitForObject( @ullable UiObject2 parent, @NonNull String resourceId, @NonNull Timeout timeout)1123 public UiObject2 waitForObject( 1124 @Nullable UiObject2 parent, @NonNull String resourceId, @NonNull Timeout timeout) 1125 throws Exception { 1126 return waitForObject(parent, By.res(mPackageName, resourceId), timeout); 1127 } 1128 1129 /** 1130 * Waits for and returns an object. 1131 * 1132 * @param selector {@link BySelector} that identifies the object. 1133 * @param timeout timeout in ms 1134 */ waitForObject(@onNull BySelector selector, @NonNull Timeout timeout)1135 protected UiObject2 waitForObject(@NonNull BySelector selector, @NonNull Timeout timeout) 1136 throws Exception { 1137 return waitForObject(/* parent= */ null, selector, timeout); 1138 } 1139 1140 /** Waits for and returns a child from a parent {@link UiObject2}. */ assertChildText(UiObject2 parent, String resourceId, String expectedText)1141 public UiObject2 assertChildText(UiObject2 parent, String resourceId, String expectedText) 1142 throws Exception { 1143 final UiObject2 child = waitForObject(parent, By.res(mPackageName, resourceId), 1144 Timeouts.UI_TIMEOUT); 1145 assertWithMessage("wrong text for view '%s'", resourceId).that(child.getText()) 1146 .isEqualTo(expectedText); 1147 return child; 1148 } 1149 1150 /** 1151 * Execute a Runnable and wait for {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} or 1152 * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED}. 1153 */ waitForWindowChange(Runnable runnable, long timeoutMillis)1154 public AccessibilityEvent waitForWindowChange(Runnable runnable, long timeoutMillis) { 1155 try { 1156 return mAutoman.executeAndWaitForEvent(runnable, (AccessibilityEvent event) -> { 1157 switch (event.getEventType()) { 1158 case AccessibilityEvent.TYPE_WINDOWS_CHANGED: 1159 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: 1160 return true; 1161 default: 1162 Log.v(TAG, "waitForWindowChange(): ignoring event " + event); 1163 } 1164 return false; 1165 }, timeoutMillis); 1166 } catch (TimeoutException e) { 1167 throw new WindowChangeTimeoutException(e, timeoutMillis); 1168 } 1169 } 1170 1171 public AccessibilityEvent waitForWindowChange(Runnable runnable) { 1172 return waitForWindowChange(runnable, Timeouts.WINDOW_CHANGE_TIMEOUT_MS); 1173 } 1174 1175 /** 1176 * Waits for and returns a list of objects. 1177 * 1178 * @param selector {@link BySelector} that identifies the object. 1179 * @param timeout timeout in ms 1180 */ 1181 private List<UiObject2> waitForObjects(BySelector selector, Timeout timeout) throws Exception { 1182 // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach. 1183 try { 1184 return timeout.run("waitForObject(" + selector + ")", () -> { 1185 final List<UiObject2> uiObjects = mDevice.findObjects(selector); 1186 if (uiObjects != null && !uiObjects.isEmpty()) { 1187 return uiObjects; 1188 } 1189 return null; 1190 1191 }); 1192 1193 } catch (RetryableException e) { 1194 dumpScreen("waitForObjects() for " + selector + "failed"); 1195 throw e; 1196 } 1197 } 1198 1199 private UiObject2 findDatasetPicker(Timeout timeout) throws Exception { 1200 // The UI element here is flaky. Sometimes the UI automator returns a StateObject. 1201 // Retry is put in place here to make sure that we catch the object. 1202 UiObject2 picker = null; 1203 int retryCount = 0; 1204 final String expectedTitle = getString(RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE); 1205 while (retryCount < MAX_UIOBJECT_RETRY_COUNT) { 1206 try { 1207 picker = waitForObject(DATASET_PICKER_SELECTOR, timeout); 1208 assertAccessibilityTitle(picker, expectedTitle); 1209 break; 1210 } catch (StaleObjectException e) { 1211 Log.d(TAG, "Retry grabbing view class"); 1212 } 1213 retryCount++; 1214 } 1215 assertWithMessage(expectedTitle + " not found").that(retryCount).isLessThan( 1216 MAX_UIOBJECT_RETRY_COUNT); 1217 1218 if (picker != null) { 1219 mOkToCallAssertNoDatasets = true; 1220 } 1221 1222 return picker; 1223 } 1224 1225 /** 1226 * Asserts a given object has the expected accessibility title. 1227 */ 1228 private void assertAccessibilityTitle(UiObject2 object, String expectedTitle) { 1229 // TODO: ideally it should get the AccessibilityWindowInfo from the object, but UiAutomator 1230 // does not expose that. 1231 for (AccessibilityWindowInfo window : mAutoman.getWindows()) { 1232 final CharSequence title = window.getTitle(); 1233 Log.d(TAG, "assertAccessibilityTitle(): found title =" + title + ", expected title=" 1234 + expectedTitle); 1235 if (title != null && title.toString().equals(expectedTitle)) { 1236 return; 1237 } 1238 } 1239 throw new RetryableException("Title '%s' not found for %s", expectedTitle, object); 1240 } 1241 1242 /** 1243 * Trys to set the orientation, if possible. No-op if the device does not support rotation 1244 * 1245 * @return True if the device orientation matches the requested orientation, else false 1246 */ 1247 public boolean maybeSetScreenOrientation(int orientation) { 1248 mAutoman.setRotation(orientation); 1249 final int currentRotation = 1250 InstrumentationRegistry.getInstrumentation() 1251 .getContext() 1252 .getSystemService(DisplayManager.class) 1253 .getDisplay(Display.DEFAULT_DISPLAY) 1254 .getRotation(); 1255 return orientation == currentRotation; 1256 } 1257 1258 /** 1259 * Sets the screen orientation. 1260 * 1261 * @param orientation typically {@link #LANDSCAPE} or {@link #PORTRAIT}. 1262 * @throws org.junit.AssumptionViolatedException if device does not support rotation. 1263 * @throws RetryableException if value didn't change. 1264 */ 1265 public void setScreenOrientation(int orientation) throws Exception { 1266 assumeTrue("Rotation is supported", Helper.isRotationSupported(mContext)); 1267 1268 // Use the platform API instead of mDevice.getDisplayRotation(), which is slow due to 1269 // waitForIdle(). waitForIdle() is not needed here because in AutoFillServiceTestCase we 1270 // always use UiBot#setScreenOrientation() to change the screen rotation, which blocks until 1271 // new rotation is reflected on the device. 1272 final int currentRotation = InstrumentationRegistry.getInstrumentation().getContext() 1273 .getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY) 1274 .getRotation(); 1275 mAutoman.setRotation(orientation); 1276 1277 if (orientation == currentRotation) { 1278 // Just need to freeze the rotation. 1279 return; 1280 } 1281 1282 UI_SCREEN_ORIENTATION_TIMEOUT.run("setScreenOrientation(" + orientation + ")", () -> 1283 mDevice.getDisplayRotation() == orientation ? Boolean.TRUE : null); 1284 } 1285 1286 /** 1287 * Gets the value of the screen orientation. 1288 * 1289 * @return typically {@link #LANDSCAPE} or {@link #PORTRAIT}. 1290 */ 1291 public int getScreenOrientation() { 1292 return mDevice.getDisplayRotation(); 1293 } 1294 1295 /** 1296 * Dumps the current view hierarchy and take a screenshot and save both locally so they can be 1297 * inspected later. 1298 */ 1299 public void dumpScreen(@NonNull String cause) { 1300 try { 1301 final File file = Helper.createTestFile("hierarchy.xml"); 1302 if (file == null) return; 1303 Log.w(TAG, "Dumping window hierarchy because " + cause + " on " + file); 1304 try (FileInputStream fis = new FileInputStream(file)) { 1305 mDevice.dumpWindowHierarchy(file); 1306 } 1307 } catch (Exception e) { 1308 Log.e(TAG, "error dumping screen on " + cause, e); 1309 } finally { 1310 takeScreenshotAndSave(); 1311 } 1312 } 1313 1314 private Rect cropScreenshotWithoutScreenDecoration(Activity activity) { 1315 final WindowInsets[] inset = new WindowInsets[1]; 1316 final View[] rootView = new View[1]; 1317 1318 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 1319 rootView[0] = activity.getWindow().getDecorView(); 1320 inset[0] = rootView[0].getRootWindowInsets(); 1321 }); 1322 final int navBarHeight = inset[0].getStableInsetBottom(); 1323 final int statusBarHeight = inset[0].getStableInsetTop(); 1324 1325 return new Rect(0, statusBarHeight, rootView[0].getWidth(), 1326 rootView[0].getHeight() - navBarHeight - statusBarHeight); 1327 } 1328 1329 // TODO(b/74358143): ideally we should take a screenshot limited by the boundaries of the 1330 // activity window, so external elements (such as the clock) are filtered out and don't cause 1331 // test flakiness when the contents are compared. 1332 public Bitmap takeScreenshot() { 1333 return takeScreenshotWithRect(null); 1334 } 1335 1336 public Bitmap takeScreenshot(@NonNull Activity activity) { 1337 // crop the screenshot without screen decoration to prevent test flakiness. 1338 final Rect rect = cropScreenshotWithoutScreenDecoration(activity); 1339 return takeScreenshotWithRect(rect); 1340 } 1341 1342 private Bitmap takeScreenshotWithRect(@Nullable Rect r) { 1343 final long before = SystemClock.elapsedRealtime(); 1344 final Bitmap bitmap = mAutoman.takeScreenshot(); 1345 final long delta = SystemClock.elapsedRealtime() - before; 1346 Log.v(TAG, "Screenshot taken in " + delta + "ms"); 1347 if (r == null) { 1348 return bitmap; 1349 } 1350 try { 1351 return Bitmap.createBitmap(bitmap, r.left, r.top, r.right, r.bottom); 1352 } finally { 1353 if (bitmap != null) { 1354 bitmap.recycle(); 1355 } 1356 } 1357 } 1358 1359 /** 1360 * Takes a screenshot and save it in the file system for post-mortem analysis. 1361 */ 1362 public void takeScreenshotAndSave() { 1363 File file = null; 1364 try { 1365 file = Helper.createTestFile("screenshot.png"); 1366 if (file != null) { 1367 Log.i(TAG, "Taking screenshot on " + file); 1368 final Bitmap screenshot = takeScreenshot(); 1369 Helper.dumpBitmap(screenshot, file); 1370 } 1371 } catch (Exception e) { 1372 Log.e(TAG, "Error taking screenshot and saving on " + file, e); 1373 } 1374 } 1375 1376 /** 1377 * Asserts the contents of a child element. 1378 * 1379 * @param parent parent object 1380 * @param childId (relative) resource id of the child 1381 * @param assertion if {@code null}, asserts the child does not exist; otherwise, asserts the 1382 * child with it. 1383 */ 1384 public void assertChild(@NonNull UiObject2 parent, @NonNull String childId, 1385 @Nullable Visitor<UiObject2> assertion) { 1386 final UiObject2 child = parent.findObject(By.res(mPackageName, childId)); 1387 try { 1388 if (assertion != null) { 1389 assertWithMessage("Didn't find child with id '%s'", childId).that(child) 1390 .isNotNull(); 1391 try { 1392 assertion.visit(child); 1393 } catch (Throwable t) { 1394 throw new AssertionError("Error on child '" + childId + "'", t); 1395 } 1396 } else { 1397 assertWithMessage("Shouldn't find child with id '%s'", childId).that(child) 1398 .isNull(); 1399 } 1400 } catch (RuntimeException | Error e) { 1401 dumpScreen("assertChild(" + childId + ") failed: " + e); 1402 throw e; 1403 } 1404 } 1405 1406 /** 1407 * Finds the first {@link URLSpan} on the current screen. 1408 */ 1409 public URLSpan findFirstUrlSpanWithText(String str) throws Exception { 1410 final List<AccessibilityNodeInfo> list = mAutoman.getRootInActiveWindow() 1411 .findAccessibilityNodeInfosByText(str); 1412 if (list.isEmpty()) { 1413 throw new AssertionError("Didn't found AccessibilityNodeInfo with " + str); 1414 } 1415 1416 final AccessibilityNodeInfo text = list.get(0); 1417 final CharSequence accessibilityTextWithSpan = text.getText(); 1418 if (!(accessibilityTextWithSpan instanceof Spanned)) { 1419 throw new AssertionError("\"" + text.getViewIdResourceName() + "\" was not a Spanned"); 1420 } 1421 1422 final URLSpan[] spans = ((Spanned) accessibilityTextWithSpan) 1423 .getSpans(0, accessibilityTextWithSpan.length(), URLSpan.class); 1424 return spans[0]; 1425 } 1426 1427 public boolean scrollToTextObject(String text) { 1428 UiScrollable scroller = new UiScrollable(new UiSelector().scrollable(true)); 1429 try { 1430 // Swipe far away from the edges to avoid triggering navigation gestures 1431 scroller.setSwipeDeadZonePercentage(0.25); 1432 return scroller.scrollTextIntoView(text); 1433 } catch (UiObjectNotFoundException e) { 1434 return false; 1435 } 1436 } 1437 1438 /** 1439 * Asserts the header in the fill dialog. 1440 */ 1441 public void assertFillDialogHeader(String expectedHeader) throws Exception { 1442 final UiObject2 header = findFillDialogHeaderPicker(); 1443 1444 assertWithMessage("wrong header for fill dialog") 1445 .that(getChildrenAsText(header)) 1446 .containsExactlyElementsIn(Arrays.asList(expectedHeader)).inOrder(); 1447 } 1448 1449 /** 1450 * Asserts reject button in the fill dialog. 1451 */ 1452 public void assertFillDialogRejectButton() throws Exception { 1453 final UiObject2 picker = findFillDialogPicker(); 1454 1455 // "No thanks" button shown 1456 final UiObject2 rejectButton = picker.findObject( 1457 By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_NO)); 1458 assertWithMessage("No reject button in fill dialog") 1459 .that(rejectButton).isNotNull(); 1460 assertWithMessage("wrong text on reject button") 1461 .that(rejectButton.getText().toUpperCase()).isEqualTo( 1462 getString(RESOURCE_STRING_SAVE_BUTTON_NO_THANKS).toUpperCase()); 1463 } 1464 1465 /** 1466 * Asserts accept button in the fill dialog. 1467 */ 1468 public void assertFillDialogAcceptButton() throws Exception { 1469 final UiObject2 picker = findFillDialogPicker(); 1470 1471 // "Continue" button shown 1472 final UiObject2 acceptButton = picker.findObject( 1473 By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_YES)); 1474 assertWithMessage("No accept button in fill dialog") 1475 .that(acceptButton).isNotNull(); 1476 assertWithMessage("wrong text on accept button") 1477 .that(acceptButton.getText().toUpperCase()).isEqualTo( 1478 getString(RESOURCE_STRING_CONTINUE_BUTTON_YES).toUpperCase()); 1479 } 1480 1481 /** 1482 * Asserts there is no accept button in the fill dialog. 1483 */ 1484 public void assertFillDialogNoAcceptButton() throws Exception { 1485 final UiObject2 picker = findFillDialogPicker(); 1486 1487 // "Continue" button not shown 1488 final UiObject2 acceptButton = picker.findObject( 1489 By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_YES)); 1490 assertWithMessage("wrong accept button in fill dialog") 1491 .that(acceptButton).isNull(); 1492 } 1493 1494 /** 1495 * Asserts the fill dialog is shown and contains the given datasets. 1496 * 1497 * @return the dataset picker object. 1498 */ 1499 public UiObject2 assertFillDialogDatasets(String... datasets) throws Exception { 1500 final UiObject2 picker = findFillDialogDatasetPicker(); 1501 1502 assertWithMessage("wrong elements in fill dialog") 1503 .that(getChildrenAsText(picker)) 1504 .containsExactlyElementsIn(datasets).inOrder(); 1505 return picker; 1506 } 1507 1508 /** 1509 * Asserts the fill dialog is shown and contains the given dataset. And then select the dataset 1510 */ 1511 public void selectFillDialogDataset(String dataset) throws Exception { 1512 final UiObject2 picker = assertFillDialogDatasets(dataset); 1513 selectDataset(picker, dataset); 1514 } 1515 1516 /** 1517 * Touch outside the fill dialog. 1518 */ 1519 public void touchOutsideDialog() throws Exception { 1520 Log.v(TAG, "touchOutsideDialog()"); 1521 final UiObject2 picker = findFillDialogPicker(); 1522 final Rect bounds = picker.getVisibleBounds(); 1523 assertThat(injectClick(new Point(bounds.left, bounds.top / 2))).isTrue(); 1524 } 1525 1526 /** 1527 * Touch outside the fill dialog. 1528 */ 1529 public void touchOutsideSaveDialog() throws Exception { 1530 Log.v(TAG, "touchOutsideSaveDialog()"); 1531 final UiObject2 picker = waitForObject(SAVE_UI_SELECTOR, SAVE_TIMEOUT); 1532 Log.v(TAG, "got picker: " + picker); 1533 final Rect bounds = picker.getVisibleBounds(); 1534 assertThat(injectClick(new Point(bounds.left, bounds.top / 2))).isTrue(); 1535 } 1536 1537 /** 1538 * click dismiss button the fill dialog. 1539 */ 1540 public void clickFillDialogDismiss() throws Exception { 1541 Log.v(TAG, "dismissedFillDialog()"); 1542 final UiObject2 picker = findFillDialogPicker(); 1543 final UiObject2 noButton = 1544 picker.findObject(By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_NO)); 1545 noButton.click(); 1546 } 1547 1548 private UiObject2 findFillDialogPicker() throws Exception { 1549 return waitForObject(FILL_DIALOG_SELECTOR, UI_DATASET_PICKER_TIMEOUT); 1550 } 1551 1552 public UiObject2 findFillDialogDatasetPicker() throws Exception { 1553 return waitForObject(FILL_DIALOG_DATASET_SELECTOR, UI_DATASET_PICKER_TIMEOUT); 1554 } 1555 1556 public UiObject2 findFillDialogHeaderPicker() throws Exception { 1557 return waitForObject(FILL_DIALOG_HEADER_SELECTOR, UI_DATASET_PICKER_TIMEOUT); 1558 } 1559 1560 /** 1561 * Asserts the fill dialog is not shown. 1562 */ 1563 public void assertNoFillDialog() throws Exception { 1564 assertNeverShown("Fill dialog", FILL_DIALOG_SELECTOR, DATASET_PICKER_NOT_SHOWN_NAPTIME_MS); 1565 } 1566 1567 /** 1568 * Injects a click input event at the given point in the default display. 1569 * We have this method because {@link UiObject2#click) cannot touch outside the object, and 1570 * {@link UiDevice#click} is broken in multi windowing mode (b/238254060). 1571 */ 1572 private boolean injectClick(Point p) { 1573 final long downTime = SystemClock.uptimeMillis(); 1574 final MotionEvent downEvent = getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, 1575 p); 1576 if (!mAutoman.injectInputEvent(downEvent, true)) { 1577 Log.e(TAG, "Failed to inject down event."); 1578 return false; 1579 } 1580 1581 try { 1582 Thread.sleep(100); 1583 } catch (InterruptedException e) { 1584 Log.e(TAG, "Interrupted while sleep between click", e); 1585 } 1586 1587 final MotionEvent upEvent = getMotionEvent(downTime, SystemClock.uptimeMillis(), 1588 MotionEvent.ACTION_UP, p); 1589 return mAutoman.injectInputEvent(upEvent, true); 1590 } 1591 1592 private MotionEvent getMotionEvent(long downTime, long eventTime, int action, Point p) { 1593 final MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties(); 1594 properties.id = 0; 1595 properties.toolType = Configurator.getInstance().getToolType(); 1596 final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); 1597 coords.pressure = 1.0F; 1598 coords.size = 1.0F; 1599 coords.x = p.x; 1600 coords.y = p.y; 1601 MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, 1, 1602 new MotionEvent.PointerProperties[]{properties}, 1603 new MotionEvent.PointerCoords[]{coords}, 0, 0, 1.0F, 1.0F, 0, 0, 1604 InputDevice.SOURCE_TOUCHSCREEN, 0); 1605 mUserHelper.injectDisplayIdIfNeeded(event); 1606 return event; 1607 } 1608 } 1609