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