1 /* 2 * Copyright (C) 2021 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.helpers; 18 19 import android.app.Instrumentation; 20 import android.content.ActivityNotFoundException; 21 import android.os.SystemClock; 22 import android.util.Log; 23 24 import android.graphics.Point; 25 import android.graphics.Rect; 26 27 import android.support.test.uiautomator.By; 28 import android.support.test.uiautomator.BySelector; 29 import android.support.test.uiautomator.Direction; 30 import android.support.test.uiautomator.UiDevice; 31 import android.support.test.uiautomator.UiObject2; 32 import android.support.test.uiautomator.Until; 33 34 import java.io.IOException; 35 import java.util.ArrayList; 36 import java.util.List; 37 import java.util.regex.Pattern; 38 39 public abstract class AbstractAutoStandardAppHelper extends AbstractStandardAppHelper { 40 private static final String LOG_TAG = AbstractAutoStandardAppHelper.class.getSimpleName(); 41 42 protected Instrumentation mInstrumentation; 43 protected UiDevice mDevice; 44 45 private AutoJsonUtility mAutoJsonUtil; 46 47 private static final int UI_RESPONSE_WAIT_MS = 5000; 48 private static final float DEFAULT_SCROLL_PERCENT = 100f; 49 private static final int DEFAULT_SCROLL_TIME_MS = 500; 50 51 private static final int MAX_SCROLLS = 5; 52 53 private enum SwipeDirection { 54 TOP_TO_BOTTOM, 55 BOTTOM_TO_TOP, 56 LEFT_TO_RIGHT, 57 RIGHT_TO_LEFT 58 } 59 AbstractAutoStandardAppHelper(Instrumentation instrumentation)60 public AbstractAutoStandardAppHelper(Instrumentation instrumentation) { 61 super(instrumentation); 62 mInstrumentation = instrumentation; 63 mDevice = UiDevice.getInstance(instrumentation); 64 mAutoJsonUtil = AutoJsonUtility.getInstance(); 65 } 66 addConfigUtility(String appName, IAutoConfigUtility utility)67 protected void addConfigUtility(String appName, IAutoConfigUtility utility) { 68 mAutoJsonUtil.addConfigUtility(appName, utility); 69 } 70 71 /** {@inheritDoc} */ 72 @Override open()73 public void open() { 74 // Launch the application as normal. 75 String pkg = getPackage(); 76 77 String output = null; 78 try { 79 Log.i(LOG_TAG, String.format("Sending command to launch: %s", pkg)); 80 mInstrumentation.getContext().startActivity(getOpenAppIntent()); 81 } catch (ActivityNotFoundException e) { 82 throw new RuntimeException(String.format("Failed to find package: %s", pkg), e); 83 } 84 85 // Ensure the package is in the foreground for success. 86 if (!mDevice.wait(Until.hasObject(By.pkg(pkg).depth(0)), 30000)) { 87 throw new IllegalStateException( 88 String.format("Did not find package, %s, in foreground.", pkg)); 89 } 90 } 91 92 /** {@inheritDoc} */ 93 @Override exit()94 public void exit() { 95 pressHome(); 96 waitForIdle(); 97 } 98 99 /** {@inheritDoc} */ 100 @Override dismissInitialDialogs()101 public void dismissInitialDialogs() { 102 // Nothing to dismiss 103 } 104 105 /** {@inheritDoc} */ 106 @Override getLauncherName()107 public String getLauncherName() { 108 throw new UnsupportedOperationException("Operation not supported."); 109 } 110 111 /** {@inheritDoc} */ 112 @Override getPackage()113 public String getPackage() { 114 throw new UnsupportedOperationException("Operation not supported."); 115 } 116 117 /** 118 * Executes a shell command on device, and return the standard output in string. 119 * 120 * @param command the command to run 121 * @return the standard output of the command, or empty string if failed without throwing an 122 * IOException 123 */ executeShellCommand(String command)124 protected String executeShellCommand(String command) { 125 try { 126 return mDevice.executeShellCommand(command); 127 } catch (IOException e) { 128 // ignore 129 Log.e( 130 LOG_TAG, 131 String.format( 132 "The shell command failed to run: %s exception: %s", 133 command, e.getMessage())); 134 return ""; 135 } 136 } 137 138 /** Press Home Button on the Device */ pressHome()139 protected void pressHome() { 140 mDevice.pressHome(); 141 } 142 143 /** Press Back Button on the Device */ pressBack()144 protected void pressBack() { 145 mDevice.pressBack(); 146 } 147 148 /** Press Enter Button on the Device */ pressEnter()149 protected void pressEnter() { 150 mDevice.pressEnter(); 151 } 152 153 /** Press power button */ pressPowerButton()154 protected void pressPowerButton() { 155 executeShellCommand("input keyevent KEYCODE_POWER"); 156 SystemClock.sleep(UI_RESPONSE_WAIT_MS); 157 } 158 159 /** Wait for the device to be idle */ waitForIdle()160 protected void waitForIdle() { 161 mDevice.waitForIdle(); 162 } 163 164 /** Wait for the given selector to be gone */ waitForGone(BySelector selector)165 protected void waitForGone(BySelector selector) { 166 mDevice.wait(Until.gone(selector), UI_RESPONSE_WAIT_MS); 167 } 168 169 /** Wait for window change on the device */ waitForWindowUpdate(String applicationPackage)170 protected void waitForWindowUpdate(String applicationPackage) { 171 mDevice.waitForWindowUpdate(applicationPackage, UI_RESPONSE_WAIT_MS); 172 } 173 174 /** 175 * Scroll in given direction by specified percent of the whole scrollable region in given time. 176 * 177 * @param direction The direction in which to perform scrolling, it's either up or down. 178 * @param percent The percentage of the whole scrollable region by which to scroll, ranging from 179 * 0 - 100. For instance, percent = 50 would scroll up/down by half of the screen. 180 * @param timeMs The duration in milliseconds to perform the scrolling gesture. 181 * @param index Index required for split screen. 182 */ scroll(Direction direction, float percent, long timeMs, int index)183 private boolean scroll(Direction direction, float percent, long timeMs, int index) { 184 boolean canScrollMoreInGivenDircetion = false; 185 List<UiObject2> upButtons = 186 findUiObjects( 187 getResourceFromConfig( 188 AutoConfigConstants.SETTINGS, 189 AutoConfigConstants.FULL_SETTINGS, 190 AutoConfigConstants.UP_BUTTON)); 191 List<UiObject2> downButtons = 192 findUiObjects( 193 getResourceFromConfig( 194 AutoConfigConstants.SETTINGS, 195 AutoConfigConstants.FULL_SETTINGS, 196 AutoConfigConstants.DOWN_BUTTON)); 197 List<UiObject2> scrollableObjects = findUiObjects(By.scrollable(true)); 198 if (scrollableObjects == null || upButtons == null || scrollableObjects.size() == 0) { 199 return canScrollMoreInGivenDircetion; 200 } 201 if (upButtons.size() == 1 || (scrollableObjects.size() - 1) < index) { 202 // reset index as it is invalid 203 index = 0; 204 } 205 if (upButtons != null) { 206 UiObject2 upButton = upButtons.get(index); 207 UiObject2 downButton = downButtons.get(index); 208 if (direction == Direction.UP) { 209 clickAndWaitForIdleScreen(upButton); 210 } else if (direction == Direction.DOWN) { 211 clickAndWaitForIdleScreen(downButton); 212 } 213 } else { 214 UiObject2 scrollable = scrollableObjects.get(index); 215 if (scrollable != null) { 216 scrollable.setGestureMargins( 217 getScrollableMargin(scrollable, false), // left 218 getScrollableMargin(scrollable, true), // top 219 getScrollableMargin(scrollable, false), // right 220 getScrollableMargin(scrollable, true)); // bottom 221 int scrollSpeed = getScrollSpeed(scrollable, timeMs); 222 canScrollMoreInGivenDircetion = 223 scrollable.scroll(direction, percent / 100, scrollSpeed); 224 } 225 } 226 return canScrollMoreInGivenDircetion; 227 } 228 229 /** 230 * Return the margin for scrolling. 231 * 232 * @param scrollable The given scrollable object to scroll through. 233 * @param isVertical If true, then vertical else horizontal 234 */ getScrollableMargin(UiObject2 scrollable, boolean isVertical)235 private int getScrollableMargin(UiObject2 scrollable, boolean isVertical) { 236 Rect bounds = scrollable.getVisibleBounds(); 237 int margin = (int) (Math.abs(bounds.width()) / 4); 238 if (isVertical) { 239 margin = (int) (Math.abs(bounds.height()) / 4); 240 } 241 return margin; 242 } 243 244 /** 245 * Return the scroll speed such that it takes given time for the device to scroll through the 246 * whole scrollable region(i.e. from the top of the scrollable region to bottom). 247 * 248 * @param scrollable The given scrollable object to scroll through. 249 * @param timeMs The duration in milliseconds to perform the scrolling gesture. 250 */ getScrollSpeed(UiObject2 scrollable, long timeMs)251 private int getScrollSpeed(UiObject2 scrollable, long timeMs) { 252 Rect bounds = scrollable.getVisibleBounds(); 253 double timeSeconds = (double) timeMs / 1000; 254 int scrollSpeed = (int) (bounds.height() / timeSeconds); 255 return scrollSpeed; 256 } 257 258 /** 259 * Scroll down from the top of the scrollable region to bottom of the scrollable region (i.e. by 260 * one page). 261 */ scrollDownOnePage()262 public boolean scrollDownOnePage() { 263 return scrollDownOnePage(0); 264 } 265 266 /** 267 * Scroll down from the top of the scrollable region to bottom of the scrollable region (i.e. by 268 * one page). Index - required for split screen 269 */ scrollDownOnePage(int index)270 protected boolean scrollDownOnePage(int index) { 271 return scroll(Direction.DOWN, DEFAULT_SCROLL_PERCENT, DEFAULT_SCROLL_TIME_MS, index); 272 } 273 274 /** 275 * Scroll up from the bottom of the scrollable region to top of the scrollable region (i.e. by 276 * one page). 277 */ scrollUpOnePage()278 public boolean scrollUpOnePage() { 279 return scrollUpOnePage(0); 280 } 281 282 /** 283 * Scroll up from the bottom of the scrollable region to top of the scrollable region (i.e. by 284 * one page). Index - required for split screen 285 */ scrollUpOnePage(int index)286 protected boolean scrollUpOnePage(int index) { 287 return scroll(Direction.UP, DEFAULT_SCROLL_PERCENT, DEFAULT_SCROLL_TIME_MS, index); 288 } 289 290 /** Find UI Object in a Scrollable view */ scrollAndFindUiObject(BySelector selector)291 protected UiObject2 scrollAndFindUiObject(BySelector selector) { 292 return scrollAndFindUiObject(selector, 0); 293 } 294 295 /** Find UI Object in a Scrollable view with Index ( required for split screen ) */ scrollAndFindUiObject(BySelector selector, int index)296 protected UiObject2 scrollAndFindUiObject(BySelector selector, int index) { 297 if (selector == null) { 298 return null; 299 } 300 // Find the object on current page 301 UiObject2 uiObject = findUiObject(selector); 302 if (uiObject != null) { 303 return uiObject; 304 } 305 // Scroll To Top 306 scrollToTop(index); 307 // Find UI Object on the first page 308 uiObject = findUiObject(selector); 309 // Try finding UI Object until it's found or all the pages are checked 310 int scrollCount = 0; 311 while (uiObject == null && scrollCount < MAX_SCROLLS) { 312 // Scroll down to next page 313 scrollDownOnePage(index); 314 315 // Find UI Object 316 uiObject = findUiObject(selector); 317 318 scrollCount++; 319 } 320 return uiObject; 321 } 322 323 /** Scroll to top of the scrollable region. */ scrollToTop()324 protected void scrollToTop() { 325 scrollToTop(0); 326 } 327 328 /** Scroll to top of the scrollable region with index. ( Required for Split Screen ) */ scrollToTop(int index)329 protected void scrollToTop(int index) { 330 int scrollCount = 0; 331 while (scrollCount < MAX_SCROLLS) { 332 scrollUpOnePage(index); 333 scrollCount++; 334 } 335 } 336 337 /** Find the UI Object that matches the given selector */ findUiObject(BySelector selector)338 protected UiObject2 findUiObject(BySelector selector) { 339 if (selector == null) { 340 return null; 341 } 342 UiObject2 uiObject = mDevice.wait(Until.findObject(selector), UI_RESPONSE_WAIT_MS); 343 return uiObject; 344 } 345 346 /** Find the list of UI object that matches the given selector */ findUiObjects(BySelector selector)347 protected List<UiObject2> findUiObjects(BySelector selector) { 348 if (selector == null) { 349 return null; 350 } 351 List<UiObject2> uiObjects = mDevice.wait(Until.findObjects(selector), UI_RESPONSE_WAIT_MS); 352 return uiObjects; 353 } 354 355 /** Find the list of UI object that matches the given selector for given depth */ findUiObjects(BySelector selector, int depth)356 protected List<UiObject2> findUiObjects(BySelector selector, int depth) { 357 if (selector == null) { 358 return null; 359 } 360 List<UiObject2> uiObjects = 361 mDevice.wait(Until.findObjects(selector.maxDepth(depth)), UI_RESPONSE_WAIT_MS); 362 return uiObjects; 363 } 364 365 /** 366 * This method is used to click on an UiObject2 and wait for device idle after click. 367 * 368 * @param uiObject UiObject2 to click. 369 */ clickAndWaitForIdleScreen(UiObject2 uiObject2)370 protected void clickAndWaitForIdleScreen(UiObject2 uiObject2) { 371 uiObject2.click(); 372 waitForIdle(); 373 } 374 375 /** 376 * This method is used to click on an UiObject2 and wait for window update. 377 * 378 * @param appPackage application package for window update 379 * @param uiObject2 UiObject2 to click. 380 */ clickAndWaitForWindowUpdate(String appPackage, UiObject2 uiObject2)381 protected void clickAndWaitForWindowUpdate(String appPackage, UiObject2 uiObject2) { 382 uiObject2.click(); 383 waitForWindowUpdate(appPackage); 384 waitForIdle(); 385 } 386 387 /** 388 * This method is used to click on an UiObject2 and wait until it is gone 389 * 390 * @param uiObject2 uiObject to be clicked 391 * @param selector BySelector to be gone 392 */ clickAndWaitForGone(UiObject2 uiObject2, BySelector selector)393 protected void clickAndWaitForGone(UiObject2 uiObject2, BySelector selector) { 394 uiObject2.click(); 395 waitForGone(selector); 396 } 397 398 /** 399 * This method is used check if given object is visible on the device screen 400 * 401 * @param selector BySelector to be gone 402 */ hasUiObject(BySelector selector)403 protected boolean hasUiObject(BySelector selector) { 404 return mDevice.hasObject(selector); 405 } 406 407 /** Get path for the given setting */ getSettingPath(String setting)408 protected String[] getSettingPath(String setting) { 409 return mAutoJsonUtil.getSettingPath(setting); 410 } 411 412 /** Get available options for given settings */ getSettingOptions(String setting)413 protected String[] getSettingOptions(String setting) { 414 return mAutoJsonUtil.getSettingOptions(setting); 415 } 416 417 /** Get application config value for given configuration */ getApplicationConfig(String config)418 protected String getApplicationConfig(String config) { 419 return mAutoJsonUtil.getApplicationConfig(config); 420 } 421 422 /** Get resource for given configuration resource in given application */ getResourceFromConfig( String appName, String appConfig, String appResource)423 protected BySelector getResourceFromConfig( 424 String appName, String appConfig, String appResource) { 425 AutoConfigResource configResource = 426 mAutoJsonUtil.getResourceFromConfig(appName, appConfig, appResource); 427 428 // RESOURCE_ID 429 if (configResource != null 430 && AutoConfigConstants.RESOURCE_ID.equals(configResource.getResourceType())) { 431 return By.res(configResource.getResourcePackage(), configResource.getResourceValue()); 432 } 433 434 // TEXT 435 if (configResource != null 436 && AutoConfigConstants.TEXT.equals(configResource.getResourceType())) { 437 return By.text( 438 Pattern.compile(configResource.getResourceValue(), Pattern.CASE_INSENSITIVE)); 439 } 440 441 // TEXT_CONTAINS 442 if (configResource != null 443 && AutoConfigConstants.TEXT_CONTAINS.equals(configResource.getResourceType())) { 444 return By.textContains(configResource.getResourceValue()); 445 } 446 447 // DESCRIPTION 448 if (configResource != null 449 && AutoConfigConstants.DESCRIPTION.equals(configResource.getResourceType())) { 450 return By.desc( 451 Pattern.compile(configResource.getResourceValue(), Pattern.CASE_INSENSITIVE)); 452 } 453 454 // CLASS 455 if (configResource != null 456 && AutoConfigConstants.CLASS.equals(configResource.getResourceType())) { 457 if (configResource.getResourcePackage() != null 458 && !configResource.getResourcePackage().isEmpty()) { 459 return By.clazz( 460 configResource.getResourcePackage(), configResource.getResourceValue()); 461 } 462 return By.clazz(configResource.getResourceValue()); 463 } 464 465 return null; 466 } 467 468 /** Get resource value for given configuration resource in given application */ getResourceValue(String appName, String appConfig, String appResource)469 protected String getResourceValue(String appName, String appConfig, String appResource) { 470 AutoConfigResource configResource = 471 mAutoJsonUtil.getResourceFromConfig(appName, appConfig, appResource); 472 473 if (configResource != null) { 474 return configResource.getResourceValue(); 475 } 476 477 return null; 478 } 479 480 /** Get resource package for given configuration resource in given application */ getResourcePackage(String appName, String appConfig, String appResource)481 protected String getResourcePackage(String appName, String appConfig, String appResource) { 482 AutoConfigResource configResource = 483 mAutoJsonUtil.getResourceFromConfig(appName, appConfig, appResource); 484 485 if (configResource != null) { 486 return configResource.getResourcePackage(); 487 } 488 489 return null; 490 } 491 492 /** Check for Split Screen UI in Settings Application */ hasSplitScreenSettingsUI()493 protected boolean hasSplitScreenSettingsUI() { 494 boolean isSplitScreen = false; 495 if ("TRUE" 496 .equalsIgnoreCase( 497 mAutoJsonUtil.getApplicationConfig(AutoConfigConstants.SPLIT_SCREEN_UI))) { 498 isSplitScreen = true; 499 } 500 return isSplitScreen; 501 } 502 swipeDownFromTop()503 protected void swipeDownFromTop() { 504 swipe(SwipeDirection.TOP_TO_BOTTOM, /*numOfSteps*/ 6); 505 } 506 swipe(SwipeDirection swipeDirection, int numOfSteps)507 protected void swipe(SwipeDirection swipeDirection, int numOfSteps) { 508 Rect bounds = getScreenBounds(); 509 List<Point> swipePoints = getPointsToSwipe(bounds, swipeDirection); 510 511 Point startPoint = swipePoints.get(0); 512 Point finishPoint = swipePoints.get(1); 513 514 // Swipe from start pont to finish point in given number of steps 515 mDevice.swipe(startPoint.x, startPoint.y, finishPoint.x, finishPoint.y, numOfSteps); 516 } 517 getPointsToSwipe(Rect bounds, SwipeDirection swipeDirection)518 protected List<Point> getPointsToSwipe(Rect bounds, SwipeDirection swipeDirection) { 519 Point boundsCenter = new Point(bounds.centerX(), bounds.centerY()); 520 521 int xStart; 522 int yStart; 523 int xFinish; 524 int yFinish; 525 // Set as 5 for default 526 int pad = 5; 527 528 switch (swipeDirection) { 529 // Scroll left = swipe from left to right. 530 case LEFT_TO_RIGHT: 531 xStart = bounds.left + pad; // Pad the edges 532 xFinish = bounds.right - pad; // Pad the edges 533 yStart = boundsCenter.y; 534 yFinish = boundsCenter.y; 535 break; 536 // Scroll right = swipe from right to left. 537 case RIGHT_TO_LEFT: 538 xStart = bounds.right - pad; // Pad the edges 539 xFinish = bounds.left + pad; // Pad the edges 540 yStart = boundsCenter.y; 541 yFinish = boundsCenter.y; 542 break; 543 // Scroll up = swipe from top to bottom. 544 case TOP_TO_BOTTOM: 545 xStart = boundsCenter.x; 546 xFinish = boundsCenter.x; 547 yStart = bounds.top + pad; // Pad the edges 548 yFinish = bounds.bottom - pad; // Pad the edges 549 break; 550 // Scroll down = swipe to bottom to top. 551 case BOTTOM_TO_TOP: 552 default: 553 xStart = boundsCenter.x; 554 xFinish = boundsCenter.x; 555 yStart = bounds.bottom - pad; // Pad the edges 556 yFinish = bounds.top + pad; // Pad the edges 557 break; 558 } 559 560 List<Point> swipePoints = new ArrayList<Point>(); 561 // Start Point 562 swipePoints.add(new Point(xStart, yStart)); 563 // Finish Point 564 swipePoints.add(new Point(xFinish, yFinish)); 565 566 return swipePoints; 567 } 568 getScreenBounds()569 protected Rect getScreenBounds() { 570 Point dimensions = mDevice.getDisplaySizeDp(); 571 return new Rect(0, 0, dimensions.x, dimensions.y); 572 } 573 } 574