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