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