1 /* 2 * Copyright (C) 2022 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.platform.spectatio.utils; 18 19 import android.app.Instrumentation; 20 import android.graphics.Point; 21 import android.graphics.Rect; 22 import android.os.RemoteException; 23 import android.os.SystemClock; 24 import android.platform.spectatio.exceptions.MissingUiElementException; 25 import android.util.Log; 26 import android.view.KeyEvent; 27 28 import androidx.test.uiautomator.By; 29 import androidx.test.uiautomator.BySelector; 30 import androidx.test.uiautomator.Direction; 31 import androidx.test.uiautomator.UiDevice; 32 import androidx.test.uiautomator.UiObject2; 33 import androidx.test.uiautomator.Until; 34 35 import com.google.common.base.Strings; 36 37 import java.io.ByteArrayOutputStream; 38 import java.io.IOException; 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.Locale; 42 43 public class SpectatioUiUtil { 44 private static final String LOG_TAG = SpectatioUiUtil.class.getSimpleName(); 45 46 private static SpectatioUiUtil sSpectatioUiUtil = null; 47 48 private static final int SHORT_UI_RESPONSE_WAIT_MS = 1000; 49 private static final int LONG_UI_RESPONSE_WAIT_MS = 5000; 50 private static final int EXTRA_LONG_UI_RESPONSE_WAIT_MS = 15000; 51 private static final int LONG_PRESS_DURATION_MS = 5000; 52 private static final int MAX_SCROLL_COUNT = 100; 53 private static final int MAX_SWIPE_STEPS = 10; 54 private static final float SCROLL_PERCENT = 1.0f; 55 private static final float SWIPE_PERCENT = 1.0f; 56 57 private int mWaitTimeAfterScroll = 5; // seconds 58 private int mScrollMargin = 4; 59 60 private UiDevice mDevice; 61 62 public enum SwipeDirection { 63 TOP_TO_BOTTOM, 64 BOTTOM_TO_TOP, 65 LEFT_TO_RIGHT, 66 RIGHT_TO_LEFT 67 } 68 SpectatioUiUtil(UiDevice mDevice)69 private SpectatioUiUtil(UiDevice mDevice) { 70 this.mDevice = mDevice; 71 } 72 getInstance(UiDevice mDevice)73 public static SpectatioUiUtil getInstance(UiDevice mDevice) { 74 if (sSpectatioUiUtil == null) { 75 sSpectatioUiUtil = new SpectatioUiUtil(mDevice); 76 } 77 return sSpectatioUiUtil; 78 } 79 80 /** 81 * Initialize a UiDevice for the given instrumentation, then initialize Spectatio for that 82 * device. If Spectatio has already been initialized, return the previously initialized 83 * instance. 84 */ getInstance(Instrumentation instrumentation)85 public static SpectatioUiUtil getInstance(Instrumentation instrumentation) { 86 return getInstance(UiDevice.getInstance(instrumentation)); 87 } 88 89 /** Sets the scroll margin and wait time after the scroll */ addScrollValues(Integer scrollMargin, Integer waitTime)90 public void addScrollValues(Integer scrollMargin, Integer waitTime) { 91 this.mScrollMargin = scrollMargin; 92 this.mWaitTimeAfterScroll = waitTime; 93 } 94 pressBack()95 public boolean pressBack() { 96 return mDevice.pressBack(); 97 } 98 pressHome()99 public boolean pressHome() { 100 return mDevice.pressHome(); 101 } 102 pressKeyCode(int keyCode)103 public boolean pressKeyCode(int keyCode) { 104 return mDevice.pressKeyCode(keyCode); 105 } 106 pressPower()107 public boolean pressPower() { 108 return pressKeyCode(KeyEvent.KEYCODE_POWER); 109 } 110 longPress(UiObject2 uiObject)111 public boolean longPress(UiObject2 uiObject) { 112 if (!isValidUiObject(uiObject)) { 113 Log.e( 114 LOG_TAG, 115 "Cannot Long Press UI Object; Provide a valid UI Object, currently it is" 116 + " NULL."); 117 return false; 118 } 119 if (!uiObject.isLongClickable()) { 120 Log.e( 121 LOG_TAG, 122 "Cannot Long Press UI Object; Provide a valid UI Object, " 123 + "current UI Object is not long clickable."); 124 return false; 125 } 126 uiObject.longClick(); 127 wait1Second(); 128 return true; 129 } 130 longPressKey(int keyCode)131 public boolean longPressKey(int keyCode) { 132 try { 133 // Use English Locale because ADB Shell command does not depend on Device UI 134 mDevice.executeShellCommand( 135 String.format(Locale.ENGLISH, "input keyevent --longpress %d", keyCode)); 136 wait1Second(); 137 return true; 138 } catch (IOException e) { 139 // Ignore 140 Log.e( 141 LOG_TAG, 142 String.format( 143 "Failed to long press key code: %d, Error: %s", 144 keyCode, e.getMessage())); 145 } 146 return false; 147 } 148 longPressPower()149 public boolean longPressPower() { 150 return longPressKey(KeyEvent.KEYCODE_POWER); 151 } 152 longPressScreenCenter()153 public boolean longPressScreenCenter() { 154 Rect bounds = getScreenBounds(); 155 int xCenter = bounds.centerX(); 156 int yCenter = bounds.centerY(); 157 try { 158 // Click method in UiDevice only takes x and y co-ordintes to tap, 159 // so it can be clicked but cannot be pressed for long time 160 // Use ADB command to Swipe instead (because UiDevice swipe method don't take duration) 161 // i.e. simulate long press by swiping from 162 // center of screen to center of screen (i.e. same points) for long duration 163 // Use English Locale because ADB Shell command does not depend on Device UI 164 mDevice.executeShellCommand( 165 String.format( 166 Locale.ENGLISH, 167 "input swipe %d %d %d %d %d", 168 xCenter, 169 yCenter, 170 xCenter, 171 yCenter, 172 LONG_PRESS_DURATION_MS)); 173 wait1Second(); 174 return true; 175 } catch (IOException e) { 176 // Ignore 177 Log.e( 178 LOG_TAG, 179 String.format( 180 "Failed to long press on screen center. Error: %s", e.getMessage())); 181 } 182 return false; 183 } 184 wakeUp()185 public void wakeUp() { 186 try { 187 mDevice.wakeUp(); 188 } catch (RemoteException ex) { 189 throw new IllegalStateException("Failed to wake up device.", ex); 190 } 191 } 192 clickAndWait(UiObject2 uiObject)193 public void clickAndWait(UiObject2 uiObject) { 194 validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Click"); 195 uiObject.click(); 196 wait1Second(); 197 } 198 199 /** 200 * Click at a specific location in the UI, and wait one second 201 * 202 * @param location Where to click 203 */ clickAndWait(Point location)204 public void clickAndWait(Point location) { 205 mDevice.click(location.x, location.y); 206 wait1Second(); 207 } 208 waitForIdle()209 public void waitForIdle() { 210 mDevice.waitForIdle(); 211 } 212 wait1Second()213 public void wait1Second() { 214 waitNSeconds(SHORT_UI_RESPONSE_WAIT_MS); 215 } 216 wait5Seconds()217 public void wait5Seconds() { 218 waitNSeconds(LONG_UI_RESPONSE_WAIT_MS); 219 } 220 221 /** Waits for 15 seconds */ wait15Seconds()222 public void wait15Seconds() { 223 waitNSeconds(EXTRA_LONG_UI_RESPONSE_WAIT_MS); 224 } 225 waitNSeconds(int waitTime)226 public void waitNSeconds(int waitTime) { 227 SystemClock.sleep(waitTime); 228 } 229 230 /** 231 * Executes a shell command on device, and return the standard output in string. 232 * 233 * @param command the command to run 234 * @return the standard output of the command, or empty string if failed without throwing an 235 * IOException 236 */ executeShellCommand(String command)237 public String executeShellCommand(String command) { 238 validateText(command, /* type= */ "Command"); 239 try { 240 return mDevice.executeShellCommand(command); 241 } catch (IOException e) { 242 // ignore 243 Log.e( 244 LOG_TAG, 245 String.format( 246 "The shell command failed to run: %s, Error: %s", 247 command, e.getMessage())); 248 return ""; 249 } 250 } 251 252 /** Find and return the UI Object that matches the given selector */ findUiObject(BySelector selector)253 public UiObject2 findUiObject(BySelector selector) { 254 validateSelector(selector, /* action= */ "Find UI Object"); 255 UiObject2 uiObject = mDevice.wait(Until.findObject(selector), LONG_UI_RESPONSE_WAIT_MS); 256 return uiObject; 257 } 258 259 /** Find and return the UI Objects that matches the given selector */ findUiObjects(BySelector selector)260 public List<UiObject2> findUiObjects(BySelector selector) { 261 validateSelector(selector, /* action= */ "Find UI Object"); 262 List<UiObject2> uiObjects = 263 mDevice.wait(Until.findObjects(selector), LONG_UI_RESPONSE_WAIT_MS); 264 return uiObjects; 265 } 266 267 /** 268 * Find the UI Object that matches the given text string. 269 * 270 * @param text Text to search on device UI. It should exactly match the text visible on UI. 271 */ findUiObject(String text)272 public UiObject2 findUiObject(String text) { 273 validateText(text, /* type= */ "Text"); 274 return findUiObject(By.text(text)); 275 } 276 277 /** 278 * Find the UI Object in given element. 279 * 280 * @param uiObject Find the ui object(selector) in this element. 281 * @param selector Find this ui object in the given element. 282 */ findUiObjectInGivenElement(UiObject2 uiObject, BySelector selector)283 public UiObject2 findUiObjectInGivenElement(UiObject2 uiObject, BySelector selector) { 284 validateUiObjectAndThrowIllegalArgumentException( 285 uiObject, /* action= */ "Find UI object in given element"); 286 validateSelector(selector, /* action= */ "Find UI object in given element"); 287 return uiObject.findObject(selector); 288 } 289 290 /** 291 * Checks if given text is available on the Device UI. The text should be exactly same as seen 292 * on the screen. 293 * 294 * <p>Given text will be searched on current screen. This method will not scroll on the screen 295 * to check for given text. 296 * 297 * @param text Text to search on device UI 298 * @return Returns True if the text is found, else return False. 299 */ hasUiElement(String text)300 public boolean hasUiElement(String text) { 301 validateText(text, /* type= */ "Text"); 302 return hasUiElement(By.text(text)); 303 } 304 305 /** 306 * Scroll using forward and backward buttons on device screen and check if the given text is 307 * present. 308 * 309 * <p>Method throws {@link MissingUiElementException} if the given button selectors are not 310 * available on the Device UI. 311 * 312 * @param forward {@link BySelector} for the button to use for scrolling forward/down. 313 * @param backward {@link BySelector} for the button to use for scrolling backward/up. 314 * @param text Text to search on device UI 315 * @return Returns True if the text is found, else return False. 316 */ scrollAndCheckIfUiElementExist( BySelector forward, BySelector backward, String text)317 public boolean scrollAndCheckIfUiElementExist( 318 BySelector forward, BySelector backward, String text) throws MissingUiElementException { 319 return scrollAndFindUiObject(forward, backward, text) != null; 320 } 321 322 /** 323 * Scroll by performing forward and backward gestures on device screen and check if the given 324 * text is present on Device UI. 325 * 326 * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code 327 * scrollAndCheckIfUiElementExist(BySelector scrollableSelector, String text, boolean 328 * isVertical)} by passing isVertical = false. 329 * 330 * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not 331 * available on the Device UI. 332 * 333 * @param scrollableSelector {@link BySelector} used for scrolling on device UI 334 * @param text Text to search on device UI 335 * @return Returns True if the text is found, else return False. 336 */ scrollAndCheckIfUiElementExist(BySelector scrollableSelector, String text)337 public boolean scrollAndCheckIfUiElementExist(BySelector scrollableSelector, String text) 338 throws MissingUiElementException { 339 return scrollAndCheckIfUiElementExist(scrollableSelector, text, /* isVertical= */ true); 340 } 341 342 /** 343 * Scroll by performing forward and backward gestures on device screen and check if the given 344 * text is present on Device UI. 345 * 346 * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not 347 * available on the Device UI. 348 * 349 * @param scrollableSelector {@link BySelector} used for scrolling on device UI 350 * @param text Text to search on device UI 351 * @param isVertical For vertical scrolling, use isVertical = true and For Horizontal scrolling, 352 * use isVertical = false. 353 * @return Returns True if the text is found, else return False. 354 */ scrollAndCheckIfUiElementExist( BySelector scrollableSelector, String text, boolean isVertical)355 public boolean scrollAndCheckIfUiElementExist( 356 BySelector scrollableSelector, String text, boolean isVertical) 357 throws MissingUiElementException { 358 return scrollAndFindUiObject(scrollableSelector, text, isVertical) != null; 359 } 360 361 /** 362 * Checks if given target is available on the Device UI. 363 * 364 * <p>Given target will be searched on current screen. This method will not scroll on the screen 365 * to check for given target. 366 * 367 * @param target {@link BySelector} to search on device UI 368 * @return Returns True if the target is found, else return False. 369 */ hasUiElement(BySelector target)370 public boolean hasUiElement(BySelector target) { 371 validateSelector(target, /* action= */ "Check For UI Object"); 372 return mDevice.hasObject(target); 373 } 374 375 /** 376 * Scroll using forward and backward buttons on device screen and check if the given target is 377 * present. 378 * 379 * <p>Method throws {@link MissingUiElementException} if the given button selectors are not 380 * available on the Device UI. 381 * 382 * @param forward {@link BySelector} for the button to use for scrolling forward/down. 383 * @param backward {@link BySelector} for the button to use for scrolling backward/up. 384 * @param target {@link BySelector} to search on device UI 385 * @return Returns True if the target is found, else return False. 386 */ scrollAndCheckIfUiElementExist( BySelector forward, BySelector backward, BySelector target)387 public boolean scrollAndCheckIfUiElementExist( 388 BySelector forward, BySelector backward, BySelector target) 389 throws MissingUiElementException { 390 return scrollAndFindUiObject(forward, backward, target) != null; 391 } 392 393 /** 394 * Scroll by performing forward and backward gestures on device screen and check if the target 395 * UI Element is present. 396 * 397 * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code 398 * scrollAndCheckIfUiElementExist(BySelector scrollableSelector, BySelector target, boolean 399 * isVertical)} by passing isVertical = false. 400 * 401 * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not 402 * available on the Device UI. 403 * 404 * @param scrollableSelector {@link BySelector} used for scrolling on device UI 405 * @param target {@link BySelector} to search on device UI 406 * @return Returns True if the target is found, else return False. 407 */ scrollAndCheckIfUiElementExist(BySelector scrollableSelector, BySelector target)408 public boolean scrollAndCheckIfUiElementExist(BySelector scrollableSelector, BySelector target) 409 throws MissingUiElementException { 410 return scrollAndCheckIfUiElementExist(scrollableSelector, target, /* isVertical= */ true); 411 } 412 413 /** 414 * Scroll by performing forward and backward gestures on device screen and check if the target 415 * UI Element is present. 416 * 417 * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not 418 * available on the Device UI. 419 * 420 * @param scrollableSelector {@link BySelector} used for scrolling on device UI 421 * @param target {@link BySelector} to search on device UI 422 * @param isVertical For vertical scrolling, use isVertical = true and For Horizontal scrolling, 423 * use isVertical = false. 424 * @return Returns True if the target is found, else return False. 425 */ scrollAndCheckIfUiElementExist( BySelector scrollableSelector, BySelector target, boolean isVertical)426 public boolean scrollAndCheckIfUiElementExist( 427 BySelector scrollableSelector, BySelector target, boolean isVertical) 428 throws MissingUiElementException { 429 return scrollAndFindUiObject(scrollableSelector, target, isVertical) != null; 430 } 431 hasPackageInForeground(String packageName)432 public boolean hasPackageInForeground(String packageName) { 433 validateText(packageName, /* type= */ "Package"); 434 return mDevice.hasObject(By.pkg(packageName).depth(0)); 435 } 436 swipeUp()437 public void swipeUp() { 438 // Swipe Up From bottom of screen to the top in one step 439 swipe(SwipeDirection.BOTTOM_TO_TOP, /*numOfSteps*/ MAX_SWIPE_STEPS); 440 } 441 swipeDown()442 public void swipeDown() { 443 // Swipe Down From top of screen to the bottom in one step 444 swipe(SwipeDirection.TOP_TO_BOTTOM, /*numOfSteps*/ MAX_SWIPE_STEPS); 445 } 446 swipeRight()447 public void swipeRight() { 448 // Swipe Right From left of screen to the right in one step 449 swipe(SwipeDirection.LEFT_TO_RIGHT, /*numOfSteps*/ MAX_SWIPE_STEPS); 450 } 451 swipeLeft()452 public void swipeLeft() { 453 // Swipe Left From right of screen to the left in one step 454 swipe(SwipeDirection.RIGHT_TO_LEFT, /*numOfSteps*/ MAX_SWIPE_STEPS); 455 } 456 swipe(SwipeDirection swipeDirection, int numOfSteps)457 public void swipe(SwipeDirection swipeDirection, int numOfSteps) { 458 Rect bounds = getScreenBounds(); 459 460 List<Point> swipePoints = getPointsToSwipe(bounds, swipeDirection); 461 462 Point startPoint = swipePoints.get(0); 463 Point finishPoint = swipePoints.get(1); 464 465 // Swipe from start pont to finish point in given number of steps 466 mDevice.swipe(startPoint.x, startPoint.y, finishPoint.x, finishPoint.y, numOfSteps); 467 } 468 getPointsToSwipe(Rect bounds, SwipeDirection swipeDirection)469 private List<Point> getPointsToSwipe(Rect bounds, SwipeDirection swipeDirection) { 470 Point boundsCenter = new Point(bounds.centerX(), bounds.centerY()); 471 472 int xStart; 473 int yStart; 474 int xFinish; 475 int yFinish; 476 // Set as 5 for default 477 // TODO: Make padding value dynamic based on the screen of device under test 478 int pad = 5; 479 480 switch (swipeDirection) { 481 // Scroll left = swipe from left to right. 482 case LEFT_TO_RIGHT: 483 xStart = bounds.left + pad; // Pad the edges 484 xFinish = bounds.right - pad; // Pad the edges 485 yStart = boundsCenter.y; 486 yFinish = boundsCenter.y; 487 break; 488 // Scroll right = swipe from right to left. 489 case RIGHT_TO_LEFT: 490 xStart = bounds.right - pad; // Pad the edges 491 xFinish = bounds.left + pad; // Pad the edges 492 yStart = boundsCenter.y; 493 yFinish = boundsCenter.y; 494 break; 495 // Scroll up = swipe from top to bottom. 496 case TOP_TO_BOTTOM: 497 xStart = boundsCenter.x; 498 xFinish = boundsCenter.x; 499 yStart = bounds.top + pad; // Pad the edges 500 yFinish = bounds.bottom - pad; // Pad the edges 501 break; 502 // Scroll down = swipe to bottom to top. 503 case BOTTOM_TO_TOP: 504 default: 505 xStart = boundsCenter.x; 506 xFinish = boundsCenter.x; 507 yStart = bounds.bottom - pad; // Pad the edges 508 yFinish = bounds.top + pad; // Pad the edges 509 break; 510 } 511 512 List<Point> swipePoints = new ArrayList<Point>(); 513 // Start Point 514 swipePoints.add(new Point(xStart, yStart)); 515 // Finish Point 516 swipePoints.add(new Point(xFinish, yFinish)); 517 518 return swipePoints; 519 } 520 getScreenBounds()521 private Rect getScreenBounds() { 522 Point dimensions = mDevice.getDisplaySizeDp(); 523 return new Rect(0, 0, dimensions.x, dimensions.y); 524 } 525 swipeRight(UiObject2 uiObject)526 public void swipeRight(UiObject2 uiObject) { 527 validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Swipe Right"); 528 uiObject.swipe(Direction.RIGHT, SWIPE_PERCENT); 529 } 530 swipeLeft(UiObject2 uiObject)531 public void swipeLeft(UiObject2 uiObject) { 532 validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Swipe Left"); 533 uiObject.swipe(Direction.LEFT, SWIPE_PERCENT); 534 } 535 swipeUp(UiObject2 uiObject)536 public void swipeUp(UiObject2 uiObject) { 537 validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Swipe Up"); 538 uiObject.swipe(Direction.UP, SWIPE_PERCENT); 539 } 540 swipeDown(UiObject2 uiObject)541 public void swipeDown(UiObject2 uiObject) { 542 validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Swipe Down"); 543 uiObject.swipe(Direction.DOWN, SWIPE_PERCENT); 544 } 545 setTextForUiElement(UiObject2 uiObject, String text)546 public void setTextForUiElement(UiObject2 uiObject, String text) { 547 validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Set Text"); 548 validateText(text, /* type= */ "Text"); 549 uiObject.setText(text); 550 } 551 getTextForUiElement(UiObject2 uiObject)552 public String getTextForUiElement(UiObject2 uiObject) { 553 validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Get Text"); 554 return uiObject.getText(); 555 } 556 557 /** 558 * Scroll on the device screen using forward or backward buttons. 559 * 560 * <p>Pass Forward/Down Button Selector to scroll forward. Pass Backward/Up Button Selector to 561 * scroll backward. Method throws {@link MissingUiElementException} if the given button is not 562 * available on the Device UI. 563 * 564 * @param scrollButtonSelector {@link BySelector} for the button to use for scrolling. 565 * @return Method returns true for successful scroll else returns false 566 */ scrollUsingButton(BySelector scrollButtonSelector)567 public boolean scrollUsingButton(BySelector scrollButtonSelector) 568 throws MissingUiElementException { 569 validateSelector(scrollButtonSelector, /* action= */ "Scroll Using Button"); 570 UiObject2 scrollButton = findUiObject(scrollButtonSelector); 571 validateUiObjectAndThrowMissingUiElementException( 572 scrollButton, scrollButtonSelector, /* action= */ "Scroll Using Button"); 573 574 String previousView = getViewHierarchy(); 575 if (!scrollButton.isEnabled()) { 576 // Already towards the end, cannot scroll 577 return false; 578 } 579 580 clickAndWait(scrollButton); 581 582 String currentView = getViewHierarchy(); 583 584 // If current view is same as previous view, scroll did not work, so return false 585 return !currentView.equals(previousView); 586 } 587 588 /** 589 * Scroll using forward and backward buttons on device screen and find the text. 590 * 591 * <p>Method throws {@link MissingUiElementException} if the given button selectors are not 592 * available on the Device UI. 593 * 594 * @param forward {@link BySelector} for the button to use for scrolling forward/down. 595 * @param backward {@link BySelector} for the button to use for scrolling backward/up. 596 * @param text Text to search on device UI. It should be exactly same as visible on device UI. 597 * @return {@link UiObject2} for given text will be returned. It returns NULL if given text is 598 * not found on the Device UI. 599 */ scrollAndFindUiObject(BySelector forward, BySelector backward, String text)600 public UiObject2 scrollAndFindUiObject(BySelector forward, BySelector backward, String text) 601 throws MissingUiElementException { 602 validateText(text, /* type= */ "Text"); 603 return scrollAndFindUiObject(forward, backward, By.text(text)); 604 } 605 606 /** 607 * Scroll using forward and backward buttons on device screen and find the target UI Element. 608 * 609 * <p>Method throws {@link MissingUiElementException} if the given button selectors are not 610 * available on the Device UI. 611 * 612 * @param forward {@link BySelector} for the button to use for scrolling forward/down. 613 * @param backward {@link BySelector} for the button to use for scrolling backward/up. 614 * @param target {@link BySelector} for UI Element to search on device UI. 615 * @return {@link UiObject2} for target UI Element will be returned. It returns NULL if given 616 * target is not found on the Device UI. 617 */ scrollAndFindUiObject( BySelector forward, BySelector backward, BySelector target)618 public UiObject2 scrollAndFindUiObject( 619 BySelector forward, BySelector backward, BySelector target) 620 throws MissingUiElementException { 621 validateSelector(forward, /* action= */ "Scroll Forward"); 622 validateSelector(backward, /* action= */ "Scroll Backward"); 623 validateSelector(target, /* action= */ "Find UI Object"); 624 // Find the object on current page 625 UiObject2 uiObject = findUiObject(target); 626 if (isValidUiObject(uiObject)) { 627 return uiObject; 628 } 629 scrollToBeginning(backward); 630 return scrollForwardAndFindUiObject(forward, target); 631 } 632 scrollForwardAndFindUiObject(BySelector forward, BySelector target)633 private UiObject2 scrollForwardAndFindUiObject(BySelector forward, BySelector target) 634 throws MissingUiElementException { 635 UiObject2 uiObject = findUiObject(target); 636 if (isValidUiObject(uiObject)) { 637 return uiObject; 638 } 639 int scrollCount = 0; 640 boolean canScroll = true; 641 while (!isValidUiObject(uiObject) && canScroll && scrollCount < MAX_SCROLL_COUNT) { 642 canScroll = scrollUsingButton(forward); 643 scrollCount++; 644 uiObject = findUiObject(target); 645 } 646 return uiObject; 647 } 648 scrollToBeginning(BySelector backward)649 public void scrollToBeginning(BySelector backward) throws MissingUiElementException { 650 int scrollCount = 0; 651 boolean canScroll = true; 652 while (canScroll && scrollCount < MAX_SCROLL_COUNT) { 653 canScroll = scrollUsingButton(backward); 654 scrollCount++; 655 } 656 } 657 getViewHierarchy()658 private String getViewHierarchy() { 659 try { 660 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 661 mDevice.dumpWindowHierarchy(outputStream); 662 outputStream.close(); 663 return outputStream.toString(); 664 } catch (IOException ex) { 665 throw new IllegalStateException("Unable to get view hierarchy."); 666 } 667 } 668 669 /** 670 * Scroll by performing forward and backward gestures on device screen and find the text. 671 * 672 * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code 673 * scrollAndFindUiObject(BySelector scrollableSelector, String text, boolean isVertical)} by 674 * passing isVertical = false. 675 * 676 * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not 677 * available on the Device UI. 678 * 679 * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI. 680 * @param text Text to search on device UI. It should be exactly same as visible on device UI. 681 * @return {@link UiObject2} for given text will be returned. It returns NULL if given text is 682 * not found on the Device UI. 683 */ scrollAndFindUiObject(BySelector scrollableSelector, String text)684 public UiObject2 scrollAndFindUiObject(BySelector scrollableSelector, String text) 685 throws MissingUiElementException { 686 validateText(text, /* type= */ "Text"); 687 return scrollAndFindUiObject(scrollableSelector, By.text(text)); 688 } 689 690 /** 691 * Scroll by performing forward and backward gestures on device screen and find the text. 692 * 693 * <p>For vertical scrolling, use isVertical = true For Horizontal scrolling, use isVertical = 694 * false. 695 * 696 * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not 697 * available on the Device UI. 698 * 699 * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI. 700 * @param text Text to search on device UI. It should be exactly same as visible on device UI. 701 * @return {@link UiObject2} for given text will be returned. It returns NULL if given text is 702 * not found on the Device UI. 703 */ scrollAndFindUiObject( BySelector scrollableSelector, String text, boolean isVertical)704 public UiObject2 scrollAndFindUiObject( 705 BySelector scrollableSelector, String text, boolean isVertical) 706 throws MissingUiElementException { 707 validateText(text, /* type= */ "Text"); 708 return scrollAndFindUiObject(scrollableSelector, By.text(text), isVertical); 709 } 710 711 /** 712 * Scroll by performing forward and backward gestures on device screen and find the target UI 713 * Element. 714 * 715 * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code 716 * scrollAndFindUiObject(BySelector scrollableSelector, BySelector target, boolean isVertical)} 717 * by passing isVertical = false. 718 * 719 * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not 720 * available on the Device UI. 721 * 722 * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI. 723 * @param target {@link BySelector} for UI Element to search on device UI. 724 * @return {@link UiObject2} for target UI Element will be returned. It returns NULL if given 725 * target is not found on the Device UI. 726 */ scrollAndFindUiObject(BySelector scrollableSelector, BySelector target)727 public UiObject2 scrollAndFindUiObject(BySelector scrollableSelector, BySelector target) 728 throws MissingUiElementException { 729 return scrollAndFindUiObject(scrollableSelector, target, /* isVertical= */ true); 730 } 731 732 /** 733 * Scroll by performing forward and backward gestures on device screen and find the target UI 734 * Element. 735 * 736 * <p>For vertical scrolling, use isVertical = true For Horizontal scrolling, use isVertical = 737 * false. 738 * 739 * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not 740 * available on the Device UI. 741 * 742 * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI. 743 * @param target {@link BySelector} for UI Element to search on device UI. 744 * @param isVertical For vertical scrolling, use isVertical = true and For Horizontal scrolling, 745 * use isVertical = false. 746 * @return {@link UiObject2} for target UI Element will be returned. It returns NULL if given 747 * target is not found on the Device UI. 748 */ scrollAndFindUiObject( BySelector scrollableSelector, BySelector target, boolean isVertical)749 public UiObject2 scrollAndFindUiObject( 750 BySelector scrollableSelector, BySelector target, boolean isVertical) 751 throws MissingUiElementException { 752 validateSelector(scrollableSelector, /* action= */ "Scroll"); 753 validateSelector(target, /* action= */ "Find UI Object"); 754 // Find UI element on current page 755 UiObject2 uiObject = findUiObject(target); 756 if (isValidUiObject(uiObject)) { 757 return uiObject; 758 } 759 scrollToBeginning(scrollableSelector, isVertical); 760 return scrollForwardAndFindUiObject(scrollableSelector, target, isVertical); 761 } 762 scrollForwardAndFindUiObject( BySelector scrollableSelector, BySelector target, boolean isVertical)763 private UiObject2 scrollForwardAndFindUiObject( 764 BySelector scrollableSelector, BySelector target, boolean isVertical) 765 throws MissingUiElementException { 766 UiObject2 uiObject = findUiObject(target); 767 if (isValidUiObject(uiObject)) { 768 return uiObject; 769 } 770 int scrollCount = 0; 771 boolean canScroll = true; 772 while (!isValidUiObject(uiObject) && canScroll && scrollCount < MAX_SCROLL_COUNT) { 773 canScroll = scrollForward(scrollableSelector, isVertical); 774 scrollCount++; 775 uiObject = findUiObject(target); 776 } 777 return uiObject; 778 } 779 scrollToBeginning(BySelector scrollableSelector, boolean isVertical)780 public void scrollToBeginning(BySelector scrollableSelector, boolean isVertical) 781 throws MissingUiElementException { 782 int scrollCount = 0; 783 boolean canScroll = true; 784 while (canScroll && scrollCount < MAX_SCROLL_COUNT) { 785 canScroll = scrollBackward(scrollableSelector, isVertical); 786 scrollCount++; 787 } 788 } 789 getDirection(boolean isVertical, boolean scrollForward)790 private Direction getDirection(boolean isVertical, boolean scrollForward) { 791 // Default Scroll = Vertical and Forward 792 // Go DOWN to scroll forward vertically 793 Direction direction = Direction.DOWN; 794 if (isVertical && !scrollForward) { 795 // Scroll = Vertical and Backward 796 // Go UP to scroll backward vertically 797 direction = Direction.UP; 798 } 799 if (!isVertical && scrollForward) { 800 // Scroll = Horizontal and Forward 801 // Go RIGHT to scroll forward horizontally 802 direction = Direction.RIGHT; 803 } 804 if (!isVertical && !scrollForward) { 805 // Scroll = Horizontal and Backward 806 // Go LEFT to scroll backward horizontally 807 direction = Direction.LEFT; 808 } 809 return direction; 810 } 811 validateAndGetScrollableObject(BySelector scrollableSelector)812 private UiObject2 validateAndGetScrollableObject(BySelector scrollableSelector) 813 throws MissingUiElementException { 814 UiObject2 scrollableObject = findUiObject(scrollableSelector); 815 validateUiObjectAndThrowMissingUiElementException( 816 scrollableObject, scrollableSelector, /* action= */ "Scroll"); 817 if (!scrollableObject.isScrollable()) { 818 scrollableObject = scrollableObject.findObject(By.scrollable(true)); 819 } 820 if ((scrollableObject == null) || !scrollableObject.isScrollable()) { 821 throw new IllegalStateException( 822 String.format( 823 "Cannot scroll; UI Object for selector %s is not scrollable and has no" 824 + " scrollable children.", 825 scrollableSelector)); 826 } 827 return scrollableObject; 828 } 829 830 /** 831 * Scroll forward one page by performing forward gestures on device screen. 832 * 833 * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code 834 * scrollForward(BySelector scrollableSelector, boolean isVertical)} by passing isVertical = 835 * false. 836 * 837 * <p>Method throws {@link MissingUiElementException} if given scrollable selector is not 838 * available on the Device UI. 839 * 840 * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI. 841 * @return Returns true for successful forward scroll, else false. 842 */ scrollForward(BySelector scrollableSelector)843 public boolean scrollForward(BySelector scrollableSelector) throws MissingUiElementException { 844 return scrollForward(scrollableSelector, /* isVertical= */ true); 845 } 846 847 /** 848 * Scroll forward one page by performing forward gestures on device screen. 849 * 850 * <p>For vertical scrolling, use isVertical = true For Horizontal scrolling, use isVertical = 851 * false. 852 * 853 * <p>Method throws {@link MissingUiElementException} if given scrollable selector is not 854 * available on the Device UI. 855 * 856 * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI. 857 * @return Returns true for successful forward scroll, else false. 858 */ scrollForward(BySelector scrollableSelector, boolean isVertical)859 public boolean scrollForward(BySelector scrollableSelector, boolean isVertical) 860 throws MissingUiElementException { 861 return scroll(scrollableSelector, getDirection(isVertical, /* scrollForward= */ true)); 862 } 863 864 /** 865 * Scroll backward one page by performing backward gestures on device screen. 866 * 867 * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code 868 * scrollBackward(BySelector scrollableSelector, boolean isVertical)} by passing isVertical = 869 * false. 870 * 871 * <p>Method throws {@link MissingUiElementException} if given scrollable selector is not 872 * available on the Device UI. 873 * 874 * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI. 875 * @return Returns true for successful backard scroll, else false. 876 */ scrollBackward(BySelector scrollableSelector)877 public boolean scrollBackward(BySelector scrollableSelector) throws MissingUiElementException { 878 return scrollBackward(scrollableSelector, /* isVertical= */ true); 879 } 880 881 /** 882 * Scroll backward one page by performing backward gestures on device screen. 883 * 884 * <p>For vertical scrolling, use isVertical = true For Horizontal scrolling, use isVertical = 885 * false. 886 * 887 * <p>Method throws {@link MissingUiElementException} if given scrollable selector is not 888 * available on the Device UI. 889 * 890 * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI. 891 * @return Returns true for successful backward scroll, else false. 892 */ scrollBackward(BySelector scrollableSelector, boolean isVertical)893 public boolean scrollBackward(BySelector scrollableSelector, boolean isVertical) 894 throws MissingUiElementException { 895 return scroll(scrollableSelector, getDirection(isVertical, /* scrollForward= */ false)); 896 } 897 scroll(BySelector scrollableSelector, Direction direction)898 private boolean scroll(BySelector scrollableSelector, Direction direction) 899 throws MissingUiElementException { 900 901 UiObject2 scrollableObject = validateAndGetScrollableObject(scrollableSelector); 902 903 Rect bounds = scrollableObject.getVisibleBounds(); 904 int horizontalMargin = (int) (Math.abs(bounds.width()) / mScrollMargin); 905 int verticalMargin = (int) (Math.abs(bounds.height()) / mScrollMargin); 906 907 scrollableObject.setGestureMargins( 908 horizontalMargin, // left 909 verticalMargin, // top 910 horizontalMargin, // right 911 verticalMargin); // bottom 912 913 String previousView = getViewHierarchy(); 914 915 scrollableObject.scroll(direction, SCROLL_PERCENT); 916 waitNSeconds(mWaitTimeAfterScroll); 917 918 String currentView = getViewHierarchy(); 919 920 // If current view is same as previous view, scroll did not work, so return false 921 return !currentView.equals(previousView); 922 } 923 validateText(String text, String type)924 private void validateText(String text, String type) { 925 if (Strings.isNullOrEmpty(text)) { 926 throw new IllegalArgumentException( 927 String.format( 928 "Provide a valid %s, current %s value is either NULL or empty.", 929 type, type)); 930 } 931 } 932 validateSelector(BySelector selector, String action)933 private void validateSelector(BySelector selector, String action) { 934 if (selector == null) { 935 throw new IllegalArgumentException( 936 String.format( 937 "Cannot %s; Provide a valid selector to %s, currently it is NULL.", 938 action, action)); 939 } 940 } 941 942 /** 943 * A simple null-check on a single uiObject2 instance 944 * 945 * @param uiObject - The object to be checked. 946 * @param action - The UI action being performed when the object was generated or searched-for. 947 */ validateUiObject(UiObject2 uiObject, String action)948 public void validateUiObject(UiObject2 uiObject, String action) { 949 if (uiObject == null) { 950 throw new MissingUiElementException( 951 String.format("Unable to find UI Element for %s.", action)); 952 } 953 } 954 955 /** 956 * A simple null-check on a list of UIObjects 957 * 958 * @param uiObjects - The list to check 959 * @param action - A string description of the UI action being taken when this list was 960 * generated. 961 */ validateUiObjects(List<UiObject2> uiObjects, String action)962 public void validateUiObjects(List<UiObject2> uiObjects, String action) { 963 if (uiObjects == null) { 964 throw new MissingUiElementException( 965 String.format("Unable to find UI Element for %s.", action)); 966 } 967 } 968 isValidUiObject(UiObject2 uiObject)969 public boolean isValidUiObject(UiObject2 uiObject) { 970 return uiObject != null; 971 } 972 validateUiObjectAndThrowIllegalArgumentException( UiObject2 uiObject, String action)973 private void validateUiObjectAndThrowIllegalArgumentException( 974 UiObject2 uiObject, String action) { 975 if (!isValidUiObject(uiObject)) { 976 throw new IllegalArgumentException( 977 String.format( 978 "Cannot %s; Provide a valid UI Object to %s, currently it is NULL.", 979 action, action)); 980 } 981 } 982 validateUiObjectAndThrowMissingUiElementException( UiObject2 uiObject, BySelector selector, String action)983 private void validateUiObjectAndThrowMissingUiElementException( 984 UiObject2 uiObject, BySelector selector, String action) 985 throws MissingUiElementException { 986 if (!isValidUiObject(uiObject)) { 987 throw new MissingUiElementException( 988 String.format( 989 "Cannot %s; Unable to find UI Object for %s selector.", 990 action, selector)); 991 } 992 } 993 } 994