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