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