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