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