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