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 * 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 (false) { 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 (false) { 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 return selectByRelativeId(mPackageName, id); 518 } 519 selectByRelativeId(String packageName, String id)520 public UiObject2 selectByRelativeId(String packageName, String id) throws Exception { 521 Log.v(TAG, "selectByRelativeId(): " + packageName + ":/" + id); 522 UiObject2 object = waitForObject(By.res(packageName, id)); 523 object.click(); 524 return object; 525 } 526 527 /** 528 * Asserts the id is shown on the screen. 529 */ assertShownById(String id)530 public UiObject2 assertShownById(String id) throws Exception { 531 final UiObject2 object = waitForObject(By.res(id)); 532 assertThat(object).isNotNull(); 533 return object; 534 } 535 536 /** 537 * Asserts the id is shown on the screen, using a resource id from the test package. 538 */ assertShownByRelativeId(String id)539 public UiObject2 assertShownByRelativeId(String id) throws Exception { 540 return assertShownByRelativeId(id, mDefaultTimeout); 541 } 542 assertShownByRelativeId(String id, Timeout timeout)543 public UiObject2 assertShownByRelativeId(String id, Timeout timeout) throws Exception { 544 final UiObject2 obj = waitForObject(By.res(mPackageName, id), timeout); 545 assertThat(obj).isNotNull(); 546 return obj; 547 } 548 549 /** 550 * Asserts the id is not shown on the screen anymore, using a resource id from the test package. 551 * 552 * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise 553 * it might pass without really asserting anything. 554 */ assertGoneByRelativeId(@onNull String id, @NonNull Timeout timeout)555 public void assertGoneByRelativeId(@NonNull String id, @NonNull Timeout timeout) { 556 assertGoneByRelativeId(/* parent = */ null, id, timeout); 557 } 558 assertGoneByRelativeId(int resId, @NonNull Timeout timeout)559 public void assertGoneByRelativeId(int resId, @NonNull Timeout timeout) { 560 assertGoneByRelativeId(/* parent = */ null, getIdName(resId), timeout); 561 } 562 getIdName(int resId)563 private String getIdName(int resId) { 564 return mContext.getResources().getResourceEntryName(resId); 565 } 566 567 /** 568 * Asserts the id is not shown on the parent anymore, using a resource id from the test package. 569 * 570 * <p><b>Note:</b> this method should only called AFTER the id was previously shown, otherwise 571 * it might pass without really asserting anything. 572 */ assertGoneByRelativeId(@ullable UiObject2 parent, @NonNull String id, @NonNull Timeout timeout)573 public void assertGoneByRelativeId(@Nullable UiObject2 parent, @NonNull String id, 574 @NonNull Timeout timeout) { 575 final SearchCondition<Boolean> condition = Until.gone(By.res(mPackageName, id)); 576 final boolean gone = parent != null 577 ? parent.wait(condition, timeout.ms()) 578 : mDevice.wait(condition, timeout.ms()); 579 if (!gone) { 580 final String message = "Object with id '" + id + "' should be gone after " 581 + timeout + " ms"; 582 dumpScreen(message); 583 throw new RetryableException(message); 584 } 585 } 586 assertShownByRelativeId(int resId)587 public UiObject2 assertShownByRelativeId(int resId) throws Exception { 588 return assertShownByRelativeId(getIdName(resId)); 589 } 590 assertNeverShownByRelativeId(@onNull String description, int resId, long timeout)591 public void assertNeverShownByRelativeId(@NonNull String description, int resId, long timeout) 592 throws Exception { 593 final BySelector selector = By.res(Helper.MY_PACKAGE, getIdName(resId)); 594 assertNeverShown(description, selector, timeout); 595 } 596 597 /** 598 * Asserts that a {@code selector} is not showing after {@code timeout} milliseconds. 599 */ assertNeverShown(String description, BySelector selector, long timeout)600 protected void assertNeverShown(String description, BySelector selector, long timeout) 601 throws Exception { 602 SystemClock.sleep(timeout); 603 final UiObject2 object = mDevice.findObject(selector); 604 if (object != null) { 605 throw new AssertionError( 606 String.format("Should not be showing %s after %dms, but got %s", 607 description, timeout, getChildrenAsText(object))); 608 } 609 } 610 611 /** 612 * Gets the text set on a view. 613 */ getTextByRelativeId(String id)614 public String getTextByRelativeId(String id) throws Exception { 615 return waitForObject(By.res(mPackageName, id)).getText(); 616 } 617 618 /** 619 * Focus in the view with the given resource id. 620 */ focusByRelativeId(String id)621 public void focusByRelativeId(String id) throws Exception { 622 waitForObject(By.res(mPackageName, id)).click(); 623 } 624 625 /** 626 * Sets a new text on a view. 627 */ setTextByRelativeId(String id, String newText)628 public void setTextByRelativeId(String id, String newText) throws Exception { 629 waitForObject(By.res(mPackageName, id)).setText(newText); 630 } 631 632 /** 633 * Sets a new text on a view. 634 * 635 * <p><b>Note:</b> First clear the view to the first character of the old string, then clear to 636 * empty. This is to accommodate the fix for the bug where views are reset to empty, causing 637 * save dialog to not show. The fix for this bug is to ignore sudden resets to empty, therefore 638 * CTS tests simulating field clearing have to progressively clear the field instead of 639 * resetting to empty at once. 640 */ clearTextByRelativeId(String id)641 public void clearTextByRelativeId(String id) throws Exception { 642 final UiObject2 object = waitForObject(By.res(mPackageName, id)); 643 String oldText = object.getText(); 644 if (!oldText.isEmpty()) { 645 object.setText(String.valueOf(oldText.charAt(0))); 646 object.setText(""); 647 } 648 } 649 650 /** 651 * Asserts the save snackbar is showing and returns it. 652 */ assertSaveShowing(int type)653 public UiObject2 assertSaveShowing(int type) throws Exception { 654 return assertSaveShowing(SAVE_TIMEOUT, type); 655 } 656 657 /** 658 * Asserts the save snackbar is showing with a custom service name and returns it. 659 */ assertSaveShowingWithCustomServiceName(int type, String customServiceName)660 public UiObject2 assertSaveShowingWithCustomServiceName(int type, String customServiceName) 661 throws Exception { 662 return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 663 SaveInfo.POSITIVE_BUTTON_STYLE_SAVE, null, SAVE_TIMEOUT, customServiceName, type); 664 } 665 666 /** 667 * Asserts the save snackbar is showing and returns it. 668 */ assertSaveShowing(Timeout timeout, int type)669 public UiObject2 assertSaveShowing(Timeout timeout, int type) throws Exception { 670 return assertSaveShowing(null, timeout, type); 671 } 672 673 /** 674 * Asserts the save snackbar is showing with the Update message and returns it. 675 */ assertUpdateShowing(int... types)676 public UiObject2 assertUpdateShowing(int... types) throws Exception { 677 return assertSaveOrUpdateShowing(/* update= */ true, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 678 null, SAVE_TIMEOUT, types); 679 } 680 681 /** 682 * Presses the Back button. 683 */ pressBack()684 public void pressBack() { 685 Log.d(TAG, "pressBack()"); 686 mDevice.pressBack(); 687 } 688 689 /** 690 * Presses the Home button. 691 */ pressHome()692 public void pressHome() { 693 Log.d(TAG, "pressHome()"); 694 mDevice.pressHome(); 695 } 696 697 /** 698 * Asserts the save snackbar is not showing. 699 */ assertSaveNotShowing(int type)700 public void assertSaveNotShowing(int type) throws Exception { 701 assertNeverShown("save UI for type " + saveTypeToString(type), SAVE_UI_SELECTOR, 702 SAVE_NOT_SHOWN_NAPTIME_MS); 703 } 704 705 /** 706 * Asserts the save snackbar is not showing, explaining when. 707 */ assertSaveNotShowing(int type, @Nullable String when)708 public void assertSaveNotShowing(int type, @Nullable String when) throws Exception { 709 String suffix = when == null ? "" : " when " + when; 710 assertNeverShown("save UI for type " + saveTypeToString(type) + suffix, SAVE_UI_SELECTOR, 711 SAVE_NOT_SHOWN_NAPTIME_MS); 712 } 713 assertSaveNotShowing()714 public void assertSaveNotShowing() throws Exception { 715 assertNeverShown("save UI", SAVE_UI_SELECTOR, SAVE_NOT_SHOWN_NAPTIME_MS); 716 } 717 getSaveTypeString(int type)718 private String getSaveTypeString(int type) { 719 final String typeResourceName; 720 switch (type) { 721 case SAVE_DATA_TYPE_PASSWORD: 722 typeResourceName = RESOURCE_STRING_SAVE_TYPE_PASSWORD; 723 break; 724 case SAVE_DATA_TYPE_ADDRESS: 725 typeResourceName = RESOURCE_STRING_SAVE_TYPE_ADDRESS; 726 break; 727 case SAVE_DATA_TYPE_CREDIT_CARD: 728 typeResourceName = RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD; 729 break; 730 case SAVE_DATA_TYPE_USERNAME: 731 typeResourceName = RESOURCE_STRING_SAVE_TYPE_USERNAME; 732 break; 733 case SAVE_DATA_TYPE_EMAIL_ADDRESS: 734 typeResourceName = RESOURCE_STRING_SAVE_TYPE_EMAIL_ADDRESS; 735 break; 736 case SAVE_DATA_TYPE_DEBIT_CARD: 737 typeResourceName = RESOURCE_STRING_SAVE_TYPE_DEBIT_CARD; 738 break; 739 case SAVE_DATA_TYPE_PAYMENT_CARD: 740 typeResourceName = RESOURCE_STRING_SAVE_TYPE_PAYMENT_CARD; 741 break; 742 case SAVE_DATA_TYPE_GENERIC_CARD: 743 typeResourceName = RESOURCE_STRING_SAVE_TYPE_GENERIC_CARD; 744 break; 745 default: 746 throw new IllegalArgumentException("Unsupported type: " + type); 747 } 748 return getString(typeResourceName); 749 } 750 saveTypeToString(int type)751 private String saveTypeToString(int type) { 752 // Cannot use DebugUtils, it's @hide 753 switch (type) { 754 case SAVE_DATA_TYPE_PASSWORD: 755 return "PASSWORD"; 756 case SAVE_DATA_TYPE_ADDRESS: 757 return "ADDRESS"; 758 case SAVE_DATA_TYPE_CREDIT_CARD: 759 return "CREDIT_CARD"; 760 case SAVE_DATA_TYPE_USERNAME: 761 return "USERNAME"; 762 case SAVE_DATA_TYPE_EMAIL_ADDRESS: 763 return "EMAIL_ADDRESS"; 764 case SAVE_DATA_TYPE_DEBIT_CARD: 765 return "DEBIT_CARD"; 766 case SAVE_DATA_TYPE_PAYMENT_CARD: 767 return "PAYMENT_CARD"; 768 case SAVE_DATA_TYPE_GENERIC_CARD: 769 return "GENERIC_CARD"; 770 default: 771 return "UNSUPPORT_TYPE_" + type; 772 } 773 } 774 assertSaveShowing(String description, int... types)775 public UiObject2 assertSaveShowing(String description, int... types) throws Exception { 776 return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 777 description, SAVE_TIMEOUT, types); 778 } 779 assertSaveShowing(String description, Timeout timeout, int... types)780 public UiObject2 assertSaveShowing(String description, Timeout timeout, int... types) 781 throws Exception { 782 return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 783 description, timeout, types); 784 } 785 assertSaveShowing(int negativeButtonStyle, String description, int... types)786 public UiObject2 assertSaveShowing(int negativeButtonStyle, String description, 787 int... types) throws Exception { 788 return assertSaveOrUpdateShowing(/* update= */ false, negativeButtonStyle, description, 789 SAVE_TIMEOUT, types); 790 } 791 assertSaveShowing(int positiveButtonStyle, int... types)792 public UiObject2 assertSaveShowing(int positiveButtonStyle, int... types) throws Exception { 793 return assertSaveOrUpdateShowing(/* update= */ false, SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, 794 positiveButtonStyle, /* description= */ null, SAVE_TIMEOUT, types); 795 } 796 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, String description, Timeout timeout, int... types)797 public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, 798 String description, Timeout timeout, int... types) throws Exception { 799 return assertSaveOrUpdateShowing(update, negativeButtonStyle, 800 SaveInfo.POSITIVE_BUTTON_STYLE_SAVE, description, timeout, types); 801 } 802 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, int positiveButtonStyle, String description, Timeout timeout, int... types)803 public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, 804 int positiveButtonStyle, String description, Timeout timeout, int... types) 805 throws Exception { 806 return assertSaveOrUpdateShowing(update, negativeButtonStyle, positiveButtonStyle, 807 description, timeout, InstrumentedAutoFillService.getServiceLabel(), types); 808 } 809 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, int positiveButtonStyle, String description, Timeout timeout, String serviceLabel, int... types)810 public UiObject2 assertSaveOrUpdateShowing(boolean update, int negativeButtonStyle, 811 int positiveButtonStyle, String description, Timeout timeout, String serviceLabel, 812 int... types) throws Exception { 813 814 final UiObject2 snackbar = waitForObject(SAVE_UI_SELECTOR, timeout); 815 816 final UiObject2 titleView = 817 waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_TITLE), timeout); 818 assertWithMessage("save title (%s) is not shown", RESOURCE_ID_SAVE_TITLE).that(titleView) 819 .isNotNull(); 820 821 final UiObject2 iconView = 822 waitForObject(snackbar, By.res("android", RESOURCE_ID_SAVE_ICON), timeout); 823 assertWithMessage("save icon (%s) is not shown", RESOURCE_ID_SAVE_ICON).that(iconView) 824 .isNotNull(); 825 826 final String actualTitle = titleView.getText(); 827 Log.d(TAG, "save title: " + actualTitle); 828 829 final String titleId, titleWithTypeId; 830 if (update) { 831 titleId = RESOURCE_STRING_UPDATE_TITLE; 832 titleWithTypeId = RESOURCE_STRING_UPDATE_TITLE_WITH_TYPE; 833 } else { 834 titleId = RESOURCE_STRING_SAVE_TITLE; 835 titleWithTypeId = RESOURCE_STRING_SAVE_TITLE_WITH_TYPE; 836 } 837 838 switch (types.length) { 839 case 1: 840 final String expectedTitle = (types[0] == SAVE_DATA_TYPE_GENERIC) 841 ? Html.fromHtml(getString(titleId, serviceLabel), 0).toString() 842 : Html.fromHtml(getString(titleWithTypeId, 843 getSaveTypeString(types[0]), serviceLabel), 0).toString(); 844 assertThat(actualTitle).isEqualTo(expectedTitle); 845 break; 846 case 2: 847 // We cannot predict the order... 848 assertThat(actualTitle).contains(getSaveTypeString(types[0])); 849 assertThat(actualTitle).contains(getSaveTypeString(types[1])); 850 break; 851 case 3: 852 // We cannot predict the order... 853 assertThat(actualTitle).contains(getSaveTypeString(types[0])); 854 assertThat(actualTitle).contains(getSaveTypeString(types[1])); 855 assertThat(actualTitle).contains(getSaveTypeString(types[2])); 856 break; 857 default: 858 throw new IllegalArgumentException("Invalid types: " + Arrays.toString(types)); 859 } 860 861 if (description != null) { 862 final UiObject2 saveSubTitle = snackbar.findObject(By.text(description)); 863 assertWithMessage("save subtitle(%s)", description).that(saveSubTitle).isNotNull(); 864 } 865 866 final String positiveButtonStringId; 867 switch (positiveButtonStyle) { 868 case SaveInfo.POSITIVE_BUTTON_STYLE_CONTINUE: 869 positiveButtonStringId = RESOURCE_STRING_CONTINUE_BUTTON_YES; 870 break; 871 default: 872 positiveButtonStringId = update ? RESOURCE_STRING_UPDATE_BUTTON_YES 873 : RESOURCE_STRING_SAVE_BUTTON_YES; 874 } 875 final String expectedPositiveButtonText = getString(positiveButtonStringId).toUpperCase(); 876 final UiObject2 positiveButton = waitForObject(snackbar, 877 By.res("android", RESOURCE_ID_SAVE_BUTTON_YES), timeout); 878 assertWithMessage("wrong text on positive button") 879 .that(positiveButton.getText().toUpperCase()).isEqualTo(expectedPositiveButtonText); 880 881 final String negativeButtonStringId; 882 if (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) { 883 negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NOT_NOW; 884 } else if (negativeButtonStyle == SaveInfo.NEGATIVE_BUTTON_STYLE_NEVER) { 885 negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NEVER; 886 } else { 887 negativeButtonStringId = RESOURCE_STRING_SAVE_BUTTON_NO_THANKS; 888 } 889 final String expectedNegativeButtonText = getString(negativeButtonStringId).toUpperCase(); 890 final UiObject2 negativeButton = waitForObject(snackbar, 891 By.res("android", RESOURCE_ID_SAVE_BUTTON_NO), timeout); 892 assertWithMessage("wrong text on negative button") 893 .that(negativeButton.getText().toUpperCase()).isEqualTo(expectedNegativeButtonText); 894 895 final String expectedAccessibilityTitle = 896 getString(RESOURCE_STRING_SAVE_SNACKBAR_ACCESSIBILITY_TITLE); 897 timeout.run( 898 String.format( 899 "assertAccessibilityTitle(%s, %s)", 900 snackbar, 901 expectedAccessibilityTitle), 902 () -> { 903 try { 904 assertAccessibilityTitle(snackbar, expectedAccessibilityTitle); 905 } catch (RetryableException e) { 906 return null; 907 } 908 return true; 909 }); 910 return snackbar; 911 } 912 913 /** 914 * Taps an option in the save snackbar. 915 * 916 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 917 * @param types expected types of save info. 918 */ saveForAutofill(boolean yesDoIt, int... types)919 public void saveForAutofill(boolean yesDoIt, int... types) throws Exception { 920 final UiObject2 saveSnackBar = assertSaveShowing( 921 SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL, null, types); 922 saveForAutofill(saveSnackBar, yesDoIt); 923 } 924 updateForAutofill(boolean yesDoIt, int... types)925 public void updateForAutofill(boolean yesDoIt, int... types) throws Exception { 926 final UiObject2 saveUi = assertUpdateShowing(types); 927 saveForAutofill(saveUi, yesDoIt); 928 } 929 930 /** 931 * Taps an option in the save snackbar. 932 * 933 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 934 * @param types expected types of save info. 935 */ saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types)936 public void saveForAutofill(int negativeButtonStyle, boolean yesDoIt, int... types) 937 throws Exception { 938 final UiObject2 saveSnackBar = assertSaveShowing(negativeButtonStyle, null, types); 939 saveForAutofill(saveSnackBar, yesDoIt); 940 } 941 942 /** 943 * Taps the positive button in the save snackbar. 944 * 945 * @param types expected types of save info. 946 */ saveForAutofill(int positiveButtonStyle, int... types)947 public void saveForAutofill(int positiveButtonStyle, int... types) throws Exception { 948 final UiObject2 saveSnackBar = assertSaveShowing(positiveButtonStyle, types); 949 saveForAutofill(saveSnackBar, /* yesDoIt= */ true); 950 } 951 952 /** 953 * Taps an option in the save snackbar. 954 * 955 * @param saveSnackBar Save snackbar, typically obtained through 956 * {@link #assertSaveShowing(int)}. 957 * @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'. 958 */ saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt)959 public void saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt) { 960 final String id = yesDoIt ? "autofill_save_yes" : "autofill_save_no"; 961 962 final UiObject2 button = saveSnackBar.findObject(By.res("android", id)); 963 assertWithMessage("save button (%s)", id).that(button).isNotNull(); 964 button.click(); 965 } 966 967 /** 968 * Gets the AUTOFILL contextual menu by long pressing a text field. 969 * 970 * <p><b>NOTE:</b> this method should only be called in scenarios where we explicitly want to 971 * test the overflow menu. For all other scenarios where we want to test manual autofill, it's 972 * better to call {@code AFM.requestAutofill()} directly, because it's less error-prone and 973 * faster. 974 * 975 * @param id resource id of the field. 976 */ getAutofillMenuOption(String id)977 public UiObject2 getAutofillMenuOption(String id) throws Exception { 978 final UiObject2 field = waitForObject(By.res(mPackageName, id)); 979 // TODO: figure out why obj.longClick() doesn't always work 980 field.click(LONG_PRESS_MS); 981 982 List<UiObject2> menuItems = waitForObjects( 983 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout); 984 final String expectedText = getAutofillContextualMenuTitle(); 985 986 final StringBuffer menuNames = new StringBuffer(); 987 988 // Check first menu for AUTOFILL 989 for (UiObject2 menuItem : menuItems) { 990 final String menuName = menuItem.getText(); 991 if (menuName.equalsIgnoreCase(expectedText)) { 992 Log.v(TAG, "AUTOFILL found in first menu"); 993 return menuItem; 994 } 995 menuNames.append("'").append(menuName).append("' "); 996 } 997 998 menuNames.append(";"); 999 1000 // First menu does not have AUTOFILL, check overflow 1001 final BySelector overflowSelector = By.res("android", RESOURCE_ID_OVERFLOW); 1002 1003 // Click overflow menu button. 1004 final UiObject2 overflowMenu = waitForObject(overflowSelector, mDefaultTimeout); 1005 overflowMenu.click(); 1006 1007 // Wait for overflow menu to show. 1008 mDevice.wait(Until.gone(overflowSelector), 1000); 1009 1010 menuItems = waitForObjects( 1011 By.res("android", RESOURCE_ID_CONTEXT_MENUITEM), mDefaultTimeout); 1012 for (UiObject2 menuItem : menuItems) { 1013 final String menuName = menuItem.getText(); 1014 if (menuName.equalsIgnoreCase(expectedText)) { 1015 Log.v(TAG, "AUTOFILL found in overflow menu"); 1016 return menuItem; 1017 } 1018 menuNames.append("'").append(menuName).append("' "); 1019 } 1020 throw new RetryableException("no '%s' on '%s'", expectedText, menuNames); 1021 } 1022 getAutofillContextualMenuTitle()1023 String getAutofillContextualMenuTitle() { 1024 return getString(RESOURCE_STRING_AUTOFILL); 1025 } 1026 1027 /** 1028 * Gets a string from the Android resources. 1029 */ getString(String id)1030 private String getString(String id) { 1031 final Resources resources = mContext.getResources(); 1032 final int stringId = resources.getIdentifier(id, "string", "android"); 1033 try { 1034 return resources.getString(stringId); 1035 } catch (Resources.NotFoundException e) { 1036 throw new IllegalStateException("no internal string for '" + id + "' / res=" + stringId 1037 + ": ", e); 1038 } 1039 } 1040 1041 /** 1042 * Gets a string from the Android resources. 1043 */ getString(String id, Object... formatArgs)1044 private String getString(String id, Object... formatArgs) { 1045 final Resources resources = mContext.getResources(); 1046 final int stringId = resources.getIdentifier(id, "string", "android"); 1047 try { 1048 return resources.getString(stringId, formatArgs); 1049 } catch (Resources.NotFoundException e) { 1050 throw new IllegalStateException("no internal string for '" + id + "' / res=" + stringId 1051 + ": ", e); 1052 } 1053 } 1054 1055 /** 1056 * Waits for and returns an object. 1057 * 1058 * @param selector {@link BySelector} that identifies the object. 1059 */ waitForObject(BySelector selector)1060 private UiObject2 waitForObject(BySelector selector) throws Exception { 1061 return waitForObject(selector, mDefaultTimeout); 1062 } 1063 1064 /** 1065 * Waits for and returns an object. 1066 * 1067 * @param parent where to find the object (or {@code null} to use device's root). 1068 * @param selector {@link BySelector} that identifies the object. 1069 * @param timeout timeout in ms. 1070 * @param dumpOnError whether the window hierarchy should be dumped if the object is not found. 1071 */ waitForObject(UiObject2 parent, BySelector selector, Timeout timeout, boolean dumpOnError)1072 private UiObject2 waitForObject(UiObject2 parent, BySelector selector, Timeout timeout, 1073 boolean dumpOnError) throws Exception { 1074 // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach. 1075 try { 1076 return timeout.run("waitForObject(" + selector + ")", () -> { 1077 return parent != null 1078 ? parent.findObject(selector) 1079 : mDevice.findObject(selector); 1080 1081 }); 1082 } catch (RetryableException e) { 1083 if (dumpOnError) { 1084 dumpScreen("waitForObject() for " + selector + "on " 1085 + (parent == null ? "mDevice" : parent) + " failed"); 1086 } 1087 throw e; 1088 } 1089 } 1090 waitForObject(@ullable UiObject2 parent, @NonNull BySelector selector, @NonNull Timeout timeout)1091 public UiObject2 waitForObject(@Nullable UiObject2 parent, @NonNull BySelector selector, 1092 @NonNull Timeout timeout) 1093 throws Exception { 1094 return waitForObject(parent, selector, timeout, DUMP_ON_ERROR); 1095 } 1096 1097 /** 1098 * Waits for and returns an object. 1099 * 1100 * @param selector {@link BySelector} that identifies the object. 1101 * @param timeout timeout in ms 1102 */ waitForObject(@onNull BySelector selector, @NonNull Timeout timeout)1103 protected UiObject2 waitForObject(@NonNull BySelector selector, @NonNull Timeout timeout) 1104 throws Exception { 1105 return waitForObject(/* parent= */ null, selector, timeout); 1106 } 1107 1108 /** 1109 * Waits for and returns a child from a parent {@link UiObject2}. 1110 */ assertChildText(UiObject2 parent, String resourceId, String expectedText)1111 public UiObject2 assertChildText(UiObject2 parent, String resourceId, String expectedText) 1112 throws Exception { 1113 final UiObject2 child = waitForObject(parent, By.res(mPackageName, resourceId), 1114 Timeouts.UI_TIMEOUT); 1115 assertWithMessage("wrong text for view '%s'", resourceId).that(child.getText()) 1116 .isEqualTo(expectedText); 1117 return child; 1118 } 1119 1120 /** 1121 * Execute a Runnable and wait for {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} or 1122 * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED}. 1123 */ waitForWindowChange(Runnable runnable, long timeoutMillis)1124 public AccessibilityEvent waitForWindowChange(Runnable runnable, long timeoutMillis) { 1125 try { 1126 return mAutoman.executeAndWaitForEvent(runnable, (AccessibilityEvent event) -> { 1127 switch (event.getEventType()) { 1128 case AccessibilityEvent.TYPE_WINDOWS_CHANGED: 1129 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: 1130 return true; 1131 default: 1132 Log.v(TAG, "waitForWindowChange(): ignoring event " + event); 1133 } 1134 return false; 1135 }, timeoutMillis); 1136 } catch (TimeoutException e) { 1137 throw new WindowChangeTimeoutException(e, timeoutMillis); 1138 } 1139 } 1140 1141 public AccessibilityEvent waitForWindowChange(Runnable runnable) { 1142 return waitForWindowChange(runnable, Timeouts.WINDOW_CHANGE_TIMEOUT_MS); 1143 } 1144 1145 /** 1146 * Waits for and returns a list of objects. 1147 * 1148 * @param selector {@link BySelector} that identifies the object. 1149 * @param timeout timeout in ms 1150 */ 1151 private List<UiObject2> waitForObjects(BySelector selector, Timeout timeout) throws Exception { 1152 // NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach. 1153 try { 1154 return timeout.run("waitForObject(" + selector + ")", () -> { 1155 final List<UiObject2> uiObjects = mDevice.findObjects(selector); 1156 if (uiObjects != null && !uiObjects.isEmpty()) { 1157 return uiObjects; 1158 } 1159 return null; 1160 1161 }); 1162 1163 } catch (RetryableException e) { 1164 dumpScreen("waitForObjects() for " + selector + "failed"); 1165 throw e; 1166 } 1167 } 1168 1169 private UiObject2 findDatasetPicker(Timeout timeout) throws Exception { 1170 // The UI element here is flaky. Sometimes the UI automator returns a StateObject. 1171 // Retry is put in place here to make sure that we catch the object. 1172 UiObject2 picker = null; 1173 int retryCount = 0; 1174 final String expectedTitle = getString(RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE); 1175 while (retryCount < MAX_UIOBJECT_RETRY_COUNT) { 1176 try { 1177 picker = waitForObject(DATASET_PICKER_SELECTOR, timeout); 1178 assertAccessibilityTitle(picker, expectedTitle); 1179 break; 1180 } catch (StaleObjectException e) { 1181 Log.d(TAG, "Retry grabbing view class"); 1182 } 1183 retryCount++; 1184 } 1185 assertWithMessage(expectedTitle + " not found").that(retryCount).isLessThan( 1186 MAX_UIOBJECT_RETRY_COUNT); 1187 1188 if (picker != null) { 1189 mOkToCallAssertNoDatasets = true; 1190 } 1191 1192 return picker; 1193 } 1194 1195 /** 1196 * Asserts a given object has the expected accessibility title. 1197 */ 1198 private void assertAccessibilityTitle(UiObject2 object, String expectedTitle) { 1199 // TODO: ideally it should get the AccessibilityWindowInfo from the object, but UiAutomator 1200 // does not expose that. 1201 for (AccessibilityWindowInfo window : mAutoman.getWindows()) { 1202 final CharSequence title = window.getTitle(); 1203 Log.d(TAG, "assertAccessibilityTitle(): found title =" + title + ", expected title=" 1204 + expectedTitle); 1205 if (title != null && title.toString().equals(expectedTitle)) { 1206 return; 1207 } 1208 } 1209 throw new RetryableException("Title '%s' not found for %s", expectedTitle, object); 1210 } 1211 1212 /** 1213 * Trys to set the orientation, if possible. No-op if the device does not support rotation 1214 * 1215 * @return True if the device orientation matches the requested orientation, else false 1216 */ 1217 public boolean maybeSetScreenOrientation(int orientation) { 1218 mAutoman.setRotation(orientation); 1219 final int currentRotation = 1220 InstrumentationRegistry.getInstrumentation() 1221 .getContext() 1222 .getSystemService(DisplayManager.class) 1223 .getDisplay(Display.DEFAULT_DISPLAY) 1224 .getRotation(); 1225 return orientation == currentRotation; 1226 } 1227 1228 /** 1229 * Sets the screen orientation. 1230 * 1231 * @param orientation typically {@link #LANDSCAPE} or {@link #PORTRAIT}. 1232 * @throws org.junit.AssumptionViolatedException if device does not support rotation. 1233 * @throws RetryableException if value didn't change. 1234 */ 1235 public void setScreenOrientation(int orientation) throws Exception { 1236 assumeTrue("Rotation is supported", Helper.isRotationSupported(mContext)); 1237 1238 // Use the platform API instead of mDevice.getDisplayRotation(), which is slow due to 1239 // waitForIdle(). waitForIdle() is not needed here because in AutoFillServiceTestCase we 1240 // always use UiBot#setScreenOrientation() to change the screen rotation, which blocks until 1241 // new rotation is reflected on the device. 1242 final int currentRotation = InstrumentationRegistry.getInstrumentation().getContext() 1243 .getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY) 1244 .getRotation(); 1245 mAutoman.setRotation(orientation); 1246 1247 if (orientation == currentRotation) { 1248 // Just need to freeze the rotation. 1249 return; 1250 } 1251 1252 UI_SCREEN_ORIENTATION_TIMEOUT.run("setScreenOrientation(" + orientation + ")", () -> 1253 mDevice.getDisplayRotation() == orientation ? Boolean.TRUE : null); 1254 } 1255 1256 /** 1257 * Gets the value of the screen orientation. 1258 * 1259 * @return typically {@link #LANDSCAPE} or {@link #PORTRAIT}. 1260 */ 1261 public int getScreenOrientation() { 1262 return mDevice.getDisplayRotation(); 1263 } 1264 1265 /** 1266 * Dumps the current view hierarchy and take a screenshot and save both locally so they can be 1267 * inspected later. 1268 */ 1269 public void dumpScreen(@NonNull String cause) { 1270 try { 1271 final File file = Helper.createTestFile("hierarchy.xml"); 1272 if (file == null) return; 1273 Log.w(TAG, "Dumping window hierarchy because " + cause + " on " + file); 1274 try (FileInputStream fis = new FileInputStream(file)) { 1275 mDevice.dumpWindowHierarchy(file); 1276 } 1277 } catch (Exception e) { 1278 Log.e(TAG, "error dumping screen on " + cause, e); 1279 } finally { 1280 takeScreenshotAndSave(); 1281 } 1282 } 1283 1284 private Rect cropScreenshotWithoutScreenDecoration(Activity activity) { 1285 final WindowInsets[] inset = new WindowInsets[1]; 1286 final View[] rootView = new View[1]; 1287 1288 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 1289 rootView[0] = activity.getWindow().getDecorView(); 1290 inset[0] = rootView[0].getRootWindowInsets(); 1291 }); 1292 final int navBarHeight = inset[0].getStableInsetBottom(); 1293 final int statusBarHeight = inset[0].getStableInsetTop(); 1294 1295 return new Rect(0, statusBarHeight, rootView[0].getWidth(), 1296 rootView[0].getHeight() - navBarHeight - statusBarHeight); 1297 } 1298 1299 // TODO(b/74358143): ideally we should take a screenshot limited by the boundaries of the 1300 // activity window, so external elements (such as the clock) are filtered out and don't cause 1301 // test flakiness when the contents are compared. 1302 public Bitmap takeScreenshot() { 1303 return takeScreenshotWithRect(null); 1304 } 1305 1306 public Bitmap takeScreenshot(@NonNull Activity activity) { 1307 // crop the screenshot without screen decoration to prevent test flakiness. 1308 final Rect rect = cropScreenshotWithoutScreenDecoration(activity); 1309 return takeScreenshotWithRect(rect); 1310 } 1311 1312 private Bitmap takeScreenshotWithRect(@Nullable Rect r) { 1313 final long before = SystemClock.elapsedRealtime(); 1314 final Bitmap bitmap = mAutoman.takeScreenshot(); 1315 final long delta = SystemClock.elapsedRealtime() - before; 1316 Log.v(TAG, "Screenshot taken in " + delta + "ms"); 1317 if (r == null) { 1318 return bitmap; 1319 } 1320 try { 1321 return Bitmap.createBitmap(bitmap, r.left, r.top, r.right, r.bottom); 1322 } finally { 1323 if (bitmap != null) { 1324 bitmap.recycle(); 1325 } 1326 } 1327 } 1328 1329 /** 1330 * Takes a screenshot and save it in the file system for post-mortem analysis. 1331 */ 1332 public void takeScreenshotAndSave() { 1333 File file = null; 1334 try { 1335 file = Helper.createTestFile("screenshot.png"); 1336 if (file != null) { 1337 Log.i(TAG, "Taking screenshot on " + file); 1338 final Bitmap screenshot = takeScreenshot(); 1339 Helper.dumpBitmap(screenshot, file); 1340 } 1341 } catch (Exception e) { 1342 Log.e(TAG, "Error taking screenshot and saving on " + file, e); 1343 } 1344 } 1345 1346 /** 1347 * Asserts the contents of a child element. 1348 * 1349 * @param parent parent object 1350 * @param childId (relative) resource id of the child 1351 * @param assertion if {@code null}, asserts the child does not exist; otherwise, asserts the 1352 * child with it. 1353 */ 1354 public void assertChild(@NonNull UiObject2 parent, @NonNull String childId, 1355 @Nullable Visitor<UiObject2> assertion) { 1356 final UiObject2 child = parent.findObject(By.res(mPackageName, childId)); 1357 try { 1358 if (assertion != null) { 1359 assertWithMessage("Didn't find child with id '%s'", childId).that(child) 1360 .isNotNull(); 1361 try { 1362 assertion.visit(child); 1363 } catch (Throwable t) { 1364 throw new AssertionError("Error on child '" + childId + "'", t); 1365 } 1366 } else { 1367 assertWithMessage("Shouldn't find child with id '%s'", childId).that(child) 1368 .isNull(); 1369 } 1370 } catch (RuntimeException | Error e) { 1371 dumpScreen("assertChild(" + childId + ") failed: " + e); 1372 throw e; 1373 } 1374 } 1375 1376 /** 1377 * Finds the first {@link URLSpan} on the current screen. 1378 */ 1379 public URLSpan findFirstUrlSpanWithText(String str) throws Exception { 1380 final List<AccessibilityNodeInfo> list = mAutoman.getRootInActiveWindow() 1381 .findAccessibilityNodeInfosByText(str); 1382 if (list.isEmpty()) { 1383 throw new AssertionError("Didn't found AccessibilityNodeInfo with " + str); 1384 } 1385 1386 final AccessibilityNodeInfo text = list.get(0); 1387 final CharSequence accessibilityTextWithSpan = text.getText(); 1388 if (!(accessibilityTextWithSpan instanceof Spanned)) { 1389 throw new AssertionError("\"" + text.getViewIdResourceName() + "\" was not a Spanned"); 1390 } 1391 1392 final URLSpan[] spans = ((Spanned) accessibilityTextWithSpan) 1393 .getSpans(0, accessibilityTextWithSpan.length(), URLSpan.class); 1394 return spans[0]; 1395 } 1396 1397 public boolean scrollToTextObject(String text) { 1398 UiScrollable scroller = new UiScrollable(new UiSelector().scrollable(true)); 1399 try { 1400 // Swipe far away from the edges to avoid triggering navigation gestures 1401 scroller.setSwipeDeadZonePercentage(0.25); 1402 return scroller.scrollTextIntoView(text); 1403 } catch (UiObjectNotFoundException e) { 1404 return false; 1405 } 1406 } 1407 1408 /** 1409 * Asserts the header in the fill dialog. 1410 */ 1411 public void assertFillDialogHeader(String expectedHeader) throws Exception { 1412 final UiObject2 header = findFillDialogHeaderPicker(); 1413 1414 assertWithMessage("wrong header for fill dialog") 1415 .that(getChildrenAsText(header)) 1416 .containsExactlyElementsIn(Arrays.asList(expectedHeader)).inOrder(); 1417 } 1418 1419 /** 1420 * Asserts reject button in the fill dialog. 1421 */ 1422 public void assertFillDialogRejectButton() throws Exception { 1423 final UiObject2 picker = findFillDialogPicker(); 1424 1425 // "No thanks" button shown 1426 final UiObject2 rejectButton = picker.findObject( 1427 By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_NO)); 1428 assertWithMessage("No reject button in fill dialog") 1429 .that(rejectButton).isNotNull(); 1430 assertWithMessage("wrong text on reject button") 1431 .that(rejectButton.getText().toUpperCase()).isEqualTo( 1432 getString(RESOURCE_STRING_SAVE_BUTTON_NO_THANKS).toUpperCase()); 1433 } 1434 1435 /** 1436 * Asserts accept button in the fill dialog. 1437 */ 1438 public void assertFillDialogAcceptButton() throws Exception { 1439 final UiObject2 picker = findFillDialogPicker(); 1440 1441 // "Continue" button shown 1442 final UiObject2 acceptButton = picker.findObject( 1443 By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_YES)); 1444 assertWithMessage("No accept button in fill dialog") 1445 .that(acceptButton).isNotNull(); 1446 assertWithMessage("wrong text on accept button") 1447 .that(acceptButton.getText().toUpperCase()).isEqualTo( 1448 getString(RESOURCE_STRING_CONTINUE_BUTTON_YES).toUpperCase()); 1449 } 1450 1451 /** 1452 * Asserts there is no accept button in the fill dialog. 1453 */ 1454 public void assertFillDialogNoAcceptButton() throws Exception { 1455 final UiObject2 picker = findFillDialogPicker(); 1456 1457 // "Continue" button not shown 1458 final UiObject2 acceptButton = picker.findObject( 1459 By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_YES)); 1460 assertWithMessage("wrong accept button in fill dialog") 1461 .that(acceptButton).isNull(); 1462 } 1463 1464 /** 1465 * Asserts the fill dialog is shown and contains the given datasets. 1466 * 1467 * @return the dataset picker object. 1468 */ 1469 public UiObject2 assertFillDialogDatasets(String... datasets) throws Exception { 1470 final UiObject2 picker = findFillDialogDatasetPicker(); 1471 1472 assertWithMessage("wrong elements in fill dialog") 1473 .that(getChildrenAsText(picker)) 1474 .containsExactlyElementsIn(datasets).inOrder(); 1475 return picker; 1476 } 1477 1478 /** 1479 * Asserts the fill dialog is shown and contains the given dataset. And then select the dataset 1480 */ 1481 public void selectFillDialogDataset(String dataset) throws Exception { 1482 final UiObject2 picker = assertFillDialogDatasets(dataset); 1483 selectDataset(picker, dataset); 1484 } 1485 1486 /** 1487 * Touch outside the fill dialog. 1488 */ 1489 public void touchOutsideDialog() throws Exception { 1490 Log.v(TAG, "touchOutsideDialog()"); 1491 final UiObject2 picker = findFillDialogPicker(); 1492 final Rect bounds = picker.getVisibleBounds(); 1493 assertThat(injectClick(new Point(bounds.left, bounds.top / 2))).isTrue(); 1494 } 1495 1496 /** 1497 * Touch outside the fill dialog. 1498 */ 1499 public void touchOutsideSaveDialog() throws Exception { 1500 Log.v(TAG, "touchOutsideSaveDialog()"); 1501 final UiObject2 picker = waitForObject(SAVE_UI_SELECTOR, SAVE_TIMEOUT); 1502 Log.v(TAG, "got picker: " + picker); 1503 final Rect bounds = picker.getVisibleBounds(); 1504 assertThat(injectClick(new Point(bounds.left, bounds.top / 2))).isTrue(); 1505 } 1506 1507 /** 1508 * click dismiss button the fill dialog. 1509 */ 1510 public void clickFillDialogDismiss() throws Exception { 1511 Log.v(TAG, "dismissedFillDialog()"); 1512 final UiObject2 picker = findFillDialogPicker(); 1513 final UiObject2 noButton = 1514 picker.findObject(By.res("android", RESOURCE_ID_FILL_DIALOG_BUTTON_NO)); 1515 noButton.click(); 1516 } 1517 1518 private UiObject2 findFillDialogPicker() throws Exception { 1519 return waitForObject(FILL_DIALOG_SELECTOR, UI_DATASET_PICKER_TIMEOUT); 1520 } 1521 1522 public UiObject2 findFillDialogDatasetPicker() throws Exception { 1523 return waitForObject(FILL_DIALOG_DATASET_SELECTOR, UI_DATASET_PICKER_TIMEOUT); 1524 } 1525 1526 public UiObject2 findFillDialogHeaderPicker() throws Exception { 1527 return waitForObject(FILL_DIALOG_HEADER_SELECTOR, UI_DATASET_PICKER_TIMEOUT); 1528 } 1529 1530 /** 1531 * Asserts the fill dialog is not shown. 1532 */ 1533 public void assertNoFillDialog() throws Exception { 1534 assertNeverShown("Fill dialog", FILL_DIALOG_SELECTOR, DATASET_PICKER_NOT_SHOWN_NAPTIME_MS); 1535 } 1536 1537 /** 1538 * Injects a click input event at the given point in the default display. 1539 * We have this method because {@link UiObject2#click) cannot touch outside the object, and 1540 * {@link UiDevice#click} is broken in multi windowing mode (b/238254060). 1541 */ 1542 private boolean injectClick(Point p) { 1543 final long downTime = SystemClock.uptimeMillis(); 1544 final MotionEvent downEvent = getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, 1545 p); 1546 if (!mAutoman.injectInputEvent(downEvent, true)) { 1547 Log.e(TAG, "Failed to inject down event."); 1548 return false; 1549 } 1550 1551 try { 1552 Thread.sleep(100); 1553 } catch (InterruptedException e) { 1554 Log.e(TAG, "Interrupted while sleep between click", e); 1555 } 1556 1557 final MotionEvent upEvent = getMotionEvent(downTime, SystemClock.uptimeMillis(), 1558 MotionEvent.ACTION_UP, p); 1559 return mAutoman.injectInputEvent(upEvent, true); 1560 } 1561 1562 private MotionEvent getMotionEvent(long downTime, long eventTime, int action, Point p) { 1563 final MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties(); 1564 properties.id = 0; 1565 properties.toolType = Configurator.getInstance().getToolType(); 1566 final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); 1567 coords.pressure = 1.0F; 1568 coords.size = 1.0F; 1569 coords.x = p.x; 1570 coords.y = p.y; 1571 MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, 1, 1572 new MotionEvent.PointerProperties[]{properties}, 1573 new MotionEvent.PointerCoords[]{coords}, 0, 0, 1.0F, 1.0F, 0, 0, 1574 InputDevice.SOURCE_TOUCHSCREEN, 0); 1575 mUserHelper.injectDisplayIdIfNeeded(event); 1576 return event; 1577 } 1578 } 1579