1 /* 2 * Copyright (C) 2019 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 package android.support.test.launcherhelper; 17 18 import android.app.Instrumentation; 19 import android.os.SystemClock; 20 import android.support.test.uiautomator.By; 21 import android.support.test.uiautomator.BySelector; 22 import android.support.test.uiautomator.Direction; 23 import android.support.test.uiautomator.UiDevice; 24 import android.support.test.uiautomator.UiObject2; 25 import android.support.test.uiautomator.Until; 26 import android.system.helpers.CommandsHelper; 27 28 import java.util.Arrays; 29 import java.util.List; 30 import java.util.Map; 31 import java.util.regex.Pattern; 32 import java.util.stream.Collectors; 33 import java.util.stream.Stream; 34 35 public class AutoLauncherStrategy implements IAutoLauncherStrategy { 36 private static final String LOG_TAG = AutoLauncherStrategy.class.getSimpleName(); 37 private static final String CAR_LENSPICKER = "com.android.car.carlauncher"; 38 private static final String SYSTEM_UI_PACKAGE = "com.android.systemui"; 39 private static final String MAPS_PACKAGE = "com.google.android.apps.maps"; 40 private static final String MEDIA_PACKAGE = "com.android.car.media"; 41 private static final String RADIO_PACKAGE = "com.android.car.radio"; 42 private static final String DIAL_PACKAGE = "com.android.car.dialer"; 43 private static final String ASSISTANT_PACKAGE = "com.google.android.googlequicksearchbox"; 44 private static final String SETTINGS_PACKAGE = "com.android.car.settings"; 45 private static final String APP_SWITCH_ID = "car_ui_toolbar_menu_item_icon_container"; 46 private static final String APP_LIST_ID = "apps_grid"; 47 48 private static final long APP_LAUNCH_TIMEOUT = 30000; 49 private static final long UI_WAIT_TIMEOUT = 5000; 50 private static final long POLL_INTERVAL = 100; 51 52 private static final BySelector UP_BTN = 53 By.res(Pattern.compile(".*:id/car_ui_scrollbar_page_up")); 54 private static final BySelector DOWN_BTN = 55 By.res(Pattern.compile(".*:id/car_ui_scrollbar_page_down")); 56 private static final BySelector APP_SWITCH = By.res(Pattern.compile(".*:id/" + APP_SWITCH_ID)); 57 private static final BySelector APP_LIST = By.res(Pattern.compile(".*:id/" + APP_LIST_ID)); 58 private static final BySelector SCROLLABLE_APP_LIST = 59 By.res(Pattern.compile(".*:id/" + APP_LIST_ID)).scrollable(true); 60 private static final BySelector QUICK_SETTINGS = By.res(SYSTEM_UI_PACKAGE, "qs"); 61 private static final BySelector LEFT_HVAC = By.res(SYSTEM_UI_PACKAGE, "hvacleft"); 62 private static final BySelector RIGHT_HVAC = By.res(SYSTEM_UI_PACKAGE, "hvacright"); 63 64 private static final Map<String, BySelector> FACET_MAP = 65 Stream.of(new Object[][] { 66 { "Home", By.res(SYSTEM_UI_PACKAGE, "home").clickable(true) }, 67 { "Maps", By.res(SYSTEM_UI_PACKAGE, "maps_nav").clickable(true) }, 68 { "Media", By.res(SYSTEM_UI_PACKAGE, "music_nav").clickable(true) }, 69 { "Dial", By.res(SYSTEM_UI_PACKAGE, "phone_nav").clickable(true) }, 70 { "App Grid", By.res(SYSTEM_UI_PACKAGE, "grid_nav").clickable(true) }, 71 { "Notification", By.res(SYSTEM_UI_PACKAGE, "notifications").clickable(true) }, 72 { "Google Assistant", By.res(SYSTEM_UI_PACKAGE, "assist").clickable(true) }, 73 }).collect(Collectors.toMap(data -> (String) data[0], data -> (BySelector) data[1])); 74 75 private static final Map<String, BySelector> APP_OPEN_VERIFIERS = 76 Stream.of(new Object[][] { 77 { "Home", By.hasDescendant(By.res(CAR_LENSPICKER, "maps")) 78 .hasDescendant(By.res(CAR_LENSPICKER, "contextual")) 79 .hasDescendant(By.res(CAR_LENSPICKER, "playback")) 80 }, 81 { "Maps", By.pkg(MAPS_PACKAGE).depth(0) }, 82 { "Media", By.pkg(MEDIA_PACKAGE).depth(0) }, 83 { "Radio", By.pkg(RADIO_PACKAGE).depth(0) }, 84 { "Dial", By.pkg(DIAL_PACKAGE).depth(0) }, 85 { "App Grid", By.res(CAR_LENSPICKER, "apps_grid") }, 86 { "Notification", By.res(SYSTEM_UI_PACKAGE, "notifications") }, 87 { "Google Assistant", By.pkg(ASSISTANT_PACKAGE) }, 88 { "Settings", By.pkg(SETTINGS_PACKAGE).depth(0) }, 89 }).collect(Collectors.toMap(data -> (String) data[0], data -> (BySelector) data[1])); 90 91 protected UiDevice mDevice; 92 private Instrumentation mInstrumentation; 93 protected CommandsHelper mCommandsHelper; 94 95 /** 96 * {@inheritDoc} 97 */ 98 @Override getSupportedLauncherPackage()99 public String getSupportedLauncherPackage() { 100 return CAR_LENSPICKER; 101 } 102 103 /** 104 * {@inheritDoc} 105 */ 106 @Override setUiDevice(UiDevice uiDevice)107 public void setUiDevice(UiDevice uiDevice) { 108 mDevice = uiDevice; 109 } 110 111 /** 112 * {@inheritDoc} 113 */ 114 @Override setInstrumentation(Instrumentation instrumentation)115 public void setInstrumentation(Instrumentation instrumentation) { 116 mInstrumentation = instrumentation; 117 mCommandsHelper = CommandsHelper.getInstance(mInstrumentation); 118 } 119 120 /** 121 * {@inheritDoc} 122 */ 123 @Override open()124 public void open() { 125 openHomeFacet(); 126 } 127 128 /** 129 * {@inheritDoc} 130 */ 131 @Override openHomeFacet()132 public void openHomeFacet() { 133 openFacet("Home"); 134 } 135 136 /** 137 * {@inheritDoc} 138 */ 139 @Override openMapsFacet()140 public void openMapsFacet() { 141 openFacet("Maps"); 142 } 143 144 /** 145 * {@inheritDoc} 146 */ 147 @Override openMediaFacet()148 public void openMediaFacet() { 149 openFacet("Media"); 150 } 151 152 /** 153 * {@inheritDoc} 154 */ 155 @Override openMediaFacet(String appName)156 public void openMediaFacet(String appName) { 157 openMediaFacet(); 158 159 // Click on app switch to open app list. 160 List<UiObject2> buttons = mDevice.wait( 161 Until.findObjects(APP_SWITCH), APP_LAUNCH_TIMEOUT); 162 int lastIndex = buttons.size() - 1; 163 /* 164 * On some media app page, there are two buttons with the same ID, 165 * while on other media app page, only the app switch button presents. 166 * The app switch button is always the last button if not the only button. 167 */ 168 UiObject2 appSwitch = buttons.get(lastIndex); 169 if (appSwitch == null) { 170 throw new RuntimeException("Failed to find app switch."); 171 } 172 appSwitch.clickAndWait(Until.newWindow(), UI_WAIT_TIMEOUT); 173 mDevice.waitForIdle(); 174 175 // Click the targeted app in app list. 176 UiObject2 app = findApplication(appName); 177 if (app == null) { 178 throw new RuntimeException(String.format("Failed to find %s app in media.", appName)); 179 } 180 app.click(); 181 mDevice.wait(Until.gone(APP_LIST), UI_WAIT_TIMEOUT); 182 183 // Verify either a Radio or Media app is in foreground for success. 184 if (appName.equals("Radio")) { 185 waitUntilAppOpen("Radio", APP_LAUNCH_TIMEOUT); 186 } else { 187 waitUntilAppOpen("Media", APP_LAUNCH_TIMEOUT); 188 } 189 190 } 191 192 /** 193 * {@inheritDoc} 194 */ 195 @Override openDialFacet()196 public void openDialFacet() { 197 openFacet("Dial"); 198 } 199 200 /** 201 * {@inheritDoc} 202 */ 203 @Override openAppGridFacet()204 public void openAppGridFacet() { 205 openFacet("App Grid"); 206 } 207 208 /** 209 * {@inheritDoc} 210 */ 211 @Override openNotificationFacet()212 public void openNotificationFacet() { 213 openFacet("Notification"); 214 } 215 216 /** {@inheritDoc} */ 217 @Override openNotifications()218 public void openNotifications() { 219 openNotificationFacet(); 220 } 221 222 /** {@inheritDoc} */ 223 @Override pressHome()224 public void pressHome() { 225 openHomeFacet(); 226 } 227 228 /** {@inheritDoc} */ 229 @Override openAssistantFacet()230 public void openAssistantFacet() { 231 openFacet("Google Assistant"); 232 } 233 openFacet(String facetName)234 private void openFacet(String facetName) { 235 BySelector facetSelector = FACET_MAP.get(facetName); 236 UiObject2 facet = mDevice.findObject(facetSelector); 237 if (facet != null) { 238 facet.click(); 239 if (!facetName.equals("Media")) { 240 // Verify the corresponding app has been open in the foregorund for success. 241 waitUntilAppOpen(facetName, APP_LAUNCH_TIMEOUT); 242 } else { 243 // For Media facet, it could open either radio app or some media app. 244 waitUntilOneOfTheAppOpen(Arrays.asList("Radio", "Media"), APP_LAUNCH_TIMEOUT); 245 } 246 } else { 247 throw new RuntimeException(String.format("Failed to find %s facet.", facetName)); 248 } 249 } 250 251 /** 252 * {@inheritDoc} 253 */ 254 @Override openQuickSettings()255 public void openQuickSettings() { 256 UiObject2 quickSettings = mDevice.findObject(QUICK_SETTINGS); 257 if (quickSettings != null) { 258 quickSettings.click(); 259 waitUntilAppOpen("Settings", APP_LAUNCH_TIMEOUT); 260 } else { 261 throw new RuntimeException("Failed to find quick settings."); 262 } 263 } 264 265 /** 266 * Wait for <code>timeout</code> milliseconds until the BySelector which can verify that the app 267 * corresponding to <code>appName</code> is in foreground has been found. If the BySelector 268 * isn't found after timeout, throw an error. 269 */ waitUntilAppOpen(String appName, long timeout)270 private void waitUntilAppOpen(String appName, long timeout) { 271 waitUntilOneOfTheAppOpen(Arrays.asList(appName), timeout); 272 } 273 274 /** 275 * Wait for <code>time</code> milliseconds until one of the app in <code>appNames</code> has 276 * been found in the foreground. 277 */ waitUntilOneOfTheAppOpen(List<String> appNames, long timeout)278 private void waitUntilOneOfTheAppOpen(List<String> appNames, long timeout) { 279 long startTime = SystemClock.uptimeMillis(); 280 281 boolean isAnyOpen = false; 282 while (SystemClock.uptimeMillis() - startTime < timeout) { 283 isAnyOpen = 284 appNames.stream() 285 .map(appName -> mDevice.hasObject(APP_OPEN_VERIFIERS.get(appName))) 286 .anyMatch(isOpen -> isOpen == true); 287 if (isAnyOpen) { 288 break; 289 } 290 291 SystemClock.sleep(POLL_INTERVAL); 292 } 293 294 if (!isAnyOpen) { 295 throw new IllegalStateException( 296 String.format( 297 "Did not find any app of %s in foreground after %d ms.", 298 String.join(", ", appNames), timeout)); 299 } 300 } 301 302 /** 303 * {@inheritDoc} 304 */ 305 @Override clickLeftHvac()306 public void clickLeftHvac() { 307 clickHvac(LEFT_HVAC); 308 } 309 310 /** 311 * {@inheritDoc} 312 */ 313 @Override clickRightHvac()314 public void clickRightHvac() { 315 clickHvac(RIGHT_HVAC); 316 } 317 clickHvac(BySelector hvacSelector)318 private void clickHvac(BySelector hvacSelector) { 319 UiObject2 hvac = mDevice.findObject(hvacSelector); 320 if (hvac != null) { 321 // Hvac is not verifiable from uiautomator. It does not starts a new window, nor does 322 // it spawn any specific identifiable ui components from uiautomator's perspective. 323 // Therefore, this wait does not verify a new window, but rather it always timeout 324 // after <code>APP_LAUNCH_TIMEOUT</code> amount of time so that hvac has sufficient 325 // time to be opened/closed. 326 hvac.clickAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT); 327 mDevice.waitForIdle(); 328 } else { 329 throw new RuntimeException("Failed to find hvac."); 330 } 331 } 332 333 @Override checkApplicationExists(String appName)334 public boolean checkApplicationExists(String appName) { 335 openAppGridFacet(); 336 UiObject2 app = findApplication(appName); 337 return app != null; 338 } 339 340 @Override openApp(String appName)341 public void openApp(String appName) { 342 if (checkApplicationExists(appName)) { 343 UiObject2 app = mDevice.findObject(By.clickable(true).hasDescendant(By.text(appName))); 344 app.clickAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT); 345 mDevice.waitForIdle(); 346 } else { 347 throw new RuntimeException(String.format("Application %s not found", appName)); 348 } 349 } 350 351 @Override openBluetoothAudioApp()352 public void openBluetoothAudioApp() { 353 String appName = "Bluetooth Audio"; 354 if (checkApplicationExists(appName)) { 355 UiObject2 app = mDevice.findObject(By.clickable(true).hasDescendant(By.text(appName))); 356 app.clickAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT); 357 mDevice.waitForIdle(); 358 } else { 359 throw new RuntimeException(String.format("Application %s not found", appName)); 360 } 361 } 362 363 @Override openGooglePlayStore()364 public void openGooglePlayStore() { 365 openApp("Play Store"); 366 SystemClock.sleep(APP_LAUNCH_TIMEOUT); 367 } 368 findApplication(String appName)369 private UiObject2 findApplication(String appName) { 370 BySelector appSelector = By.clickable(true).hasDescendant(By.text(appName)); 371 if (mDevice.hasObject(SCROLLABLE_APP_LIST)) { 372 // App list has more than 1 page. Scroll if necessary when searching for app. 373 UiObject2 down = mDevice.findObject(DOWN_BTN); 374 UiObject2 up = mDevice.findObject(UP_BTN); 375 376 while (up.isEnabled()) { 377 up.click(); 378 mDevice.waitForIdle(); 379 } 380 381 UiObject2 app = mDevice.wait(Until.findObject(appSelector), UI_WAIT_TIMEOUT); 382 while (app == null && down.isEnabled()) { 383 down.click(); 384 mDevice.waitForIdle(); 385 app = mDevice.wait(Until.findObject(appSelector), UI_WAIT_TIMEOUT); 386 } 387 return app; 388 } else { 389 // App list only has 1 page. 390 return mDevice.findObject(appSelector); 391 } 392 } 393 394 @SuppressWarnings("unused") 395 @Override openAllApps(boolean reset)396 public UiObject2 openAllApps(boolean reset) { 397 throw new UnsupportedOperationException( 398 "The feature not supported on Auto"); 399 } 400 401 @SuppressWarnings("unused") 402 @Override getAllAppsButtonSelector()403 public BySelector getAllAppsButtonSelector() { 404 throw new UnsupportedOperationException( 405 "The feature not supported on Auto"); 406 } 407 408 @SuppressWarnings("unused") 409 @Override getAllAppsSelector()410 public BySelector getAllAppsSelector() { 411 throw new UnsupportedOperationException( 412 "The feature not supported on Auto"); 413 } 414 415 @SuppressWarnings("unused") 416 @Override getAllAppsScrollDirection()417 public Direction getAllAppsScrollDirection() { 418 throw new UnsupportedOperationException( 419 "The feature not supported on Auto"); 420 } 421 422 @SuppressWarnings("unused") 423 @Override openAllWidgets(boolean reset)424 public UiObject2 openAllWidgets(boolean reset) { 425 throw new UnsupportedOperationException( 426 "The feature not supported on Auto"); 427 } 428 429 @SuppressWarnings("unused") 430 @Override getAllWidgetsSelector()431 public BySelector getAllWidgetsSelector() { 432 throw new UnsupportedOperationException( 433 "The feature not supported on Auto"); 434 } 435 436 @SuppressWarnings("unused") 437 @Override getAllWidgetsScrollDirection()438 public Direction getAllWidgetsScrollDirection() { 439 throw new UnsupportedOperationException( 440 "The feature not supported on Auto"); 441 } 442 443 @SuppressWarnings("unused") 444 @Override getWorkspaceSelector()445 public BySelector getWorkspaceSelector() { 446 throw new UnsupportedOperationException( 447 "The feature not supported on Auto"); 448 } 449 450 @SuppressWarnings("unused") 451 @Override getHotSeatSelector()452 public BySelector getHotSeatSelector() { 453 throw new UnsupportedOperationException( 454 "The feature not supported on Auto"); 455 } 456 457 @SuppressWarnings("unused") 458 @Override getWorkspaceScrollDirection()459 public Direction getWorkspaceScrollDirection() { 460 throw new UnsupportedOperationException( 461 "The feature not supported on Auto"); 462 } 463 464 @SuppressWarnings("unused") 465 @Override launch(String appName, String packageName)466 public long launch(String appName, String packageName) { 467 openApp(appName); 468 return 0; 469 } 470 } 471