1 /* 2 * Copyright (C) 2016 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.support.test.launcherhelper; 18 19 import android.graphics.Point; 20 import android.os.RemoteException; 21 import android.os.SystemClock; 22 import android.support.test.uiautomator.*; 23 import android.util.Log; 24 25 import java.io.ByteArrayOutputStream; 26 import java.io.IOException; 27 28 public class LeanbackLauncherStrategy implements ILeanbackLauncherStrategy { 29 30 private static final String LOG_TAG = LeanbackLauncherStrategy.class.getSimpleName(); 31 private static final String PACKAGE_LAUNCHER = "com.google.android.leanbacklauncher"; 32 private static final String PACKAGE_SEARCH = "com.google.android.katniss"; 33 34 private static final int MAX_SCROLL_ATTEMPTS = 20; 35 private static final int APP_LAUNCH_TIMEOUT = 10000; 36 private static final int SHORT_WAIT_TIME = 5000; // 5 sec 37 38 protected UiDevice mDevice; 39 40 41 /** 42 * {@inheritDoc} 43 */ 44 @Override getSupportedLauncherPackage()45 public String getSupportedLauncherPackage() { 46 return PACKAGE_LAUNCHER; 47 } 48 49 /** 50 * {@inheritDoc} 51 */ 52 @Override setUiDevice(UiDevice uiDevice)53 public void setUiDevice(UiDevice uiDevice) { 54 mDevice = uiDevice; 55 } 56 57 /** 58 * {@inheritDoc} 59 */ 60 @Override open()61 public void open() { 62 // if we see main list view, assume at home screen already 63 if (!mDevice.hasObject(getWorkspaceSelector())) { 64 mDevice.pressHome(); 65 // ensure launcher is shown 66 if (!mDevice.wait(Until.hasObject(getWorkspaceSelector()), SHORT_WAIT_TIME)) { 67 // HACK: dump hierarchy to logcat 68 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 69 try { 70 mDevice.dumpWindowHierarchy(baos); 71 baos.flush(); 72 baos.close(); 73 String[] lines = baos.toString().split("\\r?\\n"); 74 for (String line : lines) { 75 Log.d(LOG_TAG, line.trim()); 76 } 77 } catch (IOException ioe) { 78 Log.e(LOG_TAG, "error dumping XML to logcat", ioe); 79 } 80 throw new RuntimeException("Failed to open leanback launcher"); 81 } 82 mDevice.waitForIdle(); 83 } 84 } 85 86 /** 87 * {@inheritDoc} 88 */ 89 @Override openAllApps(boolean reset)90 public UiObject2 openAllApps(boolean reset) { 91 UiObject2 appsRow = selectAppsRow(); 92 if (appsRow == null) { 93 throw new RuntimeException("Could not find all apps row"); 94 } 95 if (reset) { 96 Log.w(LOG_TAG, "The reset will be ignored on leanback launcher"); 97 } 98 return appsRow; 99 } 100 101 /** 102 * {@inheritDoc} 103 */ 104 @Override getWorkspaceSelector()105 public BySelector getWorkspaceSelector() { 106 return By.res(getSupportedLauncherPackage(), "main_list_view"); 107 } 108 109 /** 110 * {@inheritDoc} 111 */ 112 @Override getSearchRowSelector()113 public BySelector getSearchRowSelector() { 114 return By.res(getSupportedLauncherPackage(), "search_view"); 115 } 116 117 /** 118 * {@inheritDoc} 119 */ 120 @Override getNotificationRowSelector()121 public BySelector getNotificationRowSelector() { 122 return By.res(getSupportedLauncherPackage(), "notification_view"); 123 } 124 125 /** 126 * {@inheritDoc} 127 */ 128 @Override getAppsRowSelector()129 public BySelector getAppsRowSelector() { 130 return By.res(getSupportedLauncherPackage(), "list").desc("Apps"); 131 } 132 133 /** 134 * {@inheritDoc} 135 */ 136 @Override getGamesRowSelector()137 public BySelector getGamesRowSelector() { 138 return By.res(getSupportedLauncherPackage(), "list").desc("Games"); 139 } 140 141 /** 142 * {@inheritDoc} 143 */ 144 @Override getSettingsRowSelector()145 public BySelector getSettingsRowSelector() { 146 return By.res(getSupportedLauncherPackage(), "list").desc("") 147 .hasDescendant(By.res("icon")); 148 } 149 150 /** 151 * {@inheritDoc} 152 */ 153 @Override getAllAppsScrollDirection()154 public Direction getAllAppsScrollDirection() { 155 return Direction.RIGHT; 156 } 157 158 /** 159 * {@inheritDoc} 160 */ 161 @Override getAllAppsSelector()162 public BySelector getAllAppsSelector() { 163 // On Leanback launcher the Apps row corresponds to the All Apps on phone UI 164 return getAppsRowSelector(); 165 } 166 167 /** 168 * {@inheritDoc} 169 */ 170 @Override launch(String appName, String packageName)171 public long launch(String appName, String packageName) { 172 BySelector app = By.res(getSupportedLauncherPackage(), "app_banner").desc(appName); 173 return launchApp(this, app, packageName); 174 } 175 176 /** 177 * {@inheritDoc} 178 */ 179 @Override search(String query)180 public void search(String query) { 181 if (selectSearchRow() == null) { 182 throw new RuntimeException("Could not find search row."); 183 } 184 185 BySelector keyboardOrb = By.res(getSupportedLauncherPackage(), "keyboard_orb"); 186 UiObject2 orbButton = mDevice.wait(Until.findObject(keyboardOrb), SHORT_WAIT_TIME); 187 if (orbButton == null) { 188 throw new RuntimeException("Could not find keyboard orb."); 189 } 190 if (orbButton.isFocused()) { 191 mDevice.pressDPadCenter(); 192 } else { 193 // Move the focus to keyboard orb by DPad button. 194 mDevice.pressDPadRight(); 195 if (orbButton.isFocused()) { 196 mDevice.pressDPadCenter(); 197 } 198 } 199 mDevice.wait(Until.gone(keyboardOrb), SHORT_WAIT_TIME); 200 201 BySelector searchEditor = By.res(PACKAGE_SEARCH, "search_text_editor"); 202 UiObject2 editText = mDevice.wait(Until.findObject(searchEditor), SHORT_WAIT_TIME); 203 if (editText == null) { 204 throw new RuntimeException("Could not find search text input."); 205 } 206 207 editText.setText(query); 208 SystemClock.sleep(SHORT_WAIT_TIME); 209 210 // Note that Enter key is pressed instead of DPad keys to dismiss leanback IME 211 mDevice.pressEnter(); 212 mDevice.wait(Until.gone(searchEditor), SHORT_WAIT_TIME); 213 } 214 215 /** 216 * {@inheritDoc} 217 * 218 * Assume that the rows are sorted in the following order from the top: 219 * Search, Notification(, Partner), Apps, Games, Settings(, and Inputs) 220 */ 221 @Override selectNotificationRow()222 public UiObject2 selectNotificationRow() { 223 if (!isNotificationRowSelected()) { 224 open(); 225 mDevice.pressHome(); // Home key to move to the first card in the Notification row 226 } 227 return mDevice.wait(Until.findObject( 228 getNotificationRowSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME); 229 } 230 231 /** 232 * {@inheritDoc} 233 */ 234 @Override selectSearchRow()235 public UiObject2 selectSearchRow() { 236 if (!isSearchRowSelected()) { 237 selectNotificationRow(); 238 mDevice.pressDPadUp(); 239 } 240 return mDevice.wait(Until.findObject( 241 getSearchRowSelector().hasDescendant(By.focused(true))), SHORT_WAIT_TIME); 242 } 243 244 /** 245 * {@inheritDoc} 246 */ 247 @Override selectAppsRow()248 public UiObject2 selectAppsRow() { 249 // Start finding Apps row from Notification row 250 if (!isAppsRowSelected()) { 251 selectNotificationRow(); 252 mDevice.pressDPadDown(); 253 } 254 return mDevice.wait(Until.findObject( 255 getAllAppsSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME); 256 } 257 258 /** 259 * {@inheritDoc} 260 */ 261 @Override selectGamesRow()262 public UiObject2 selectGamesRow() { 263 if (!isGamesRowSelected()) { 264 selectAppsRow(); 265 mDevice.pressDPadDown(); 266 // If more than or equal to 16 apps are installed, the app banner could be cut off 267 // into two rows at maximum. It needs to scroll down once more. 268 if (!isGamesRowSelected()) { 269 mDevice.pressDPadDown(); 270 } 271 } 272 return mDevice.wait(Until.findObject( 273 getGamesRowSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME); 274 } 275 276 /** 277 * {@inheritDoc} 278 */ 279 @Override selectSettingsRow()280 public UiObject2 selectSettingsRow() { 281 if (!isSettingsRowSelected()) { 282 open(); 283 mDevice.pressHome(); // Home key to move to the first card in the Notification row 284 // The Settings row is at the last position 285 final int MAX_ROW_NUMS = 8; 286 for (int i = 0; i < MAX_ROW_NUMS; ++i) { 287 mDevice.pressDPadDown(); 288 } 289 } 290 return null; 291 } 292 293 @SuppressWarnings("unused") 294 @Override openAllWidgets(boolean reset)295 public UiObject2 openAllWidgets(boolean reset) { 296 throw new UnsupportedOperationException( 297 "All Widgets is not available on Leanback Launcher."); 298 } 299 300 @SuppressWarnings("unused") 301 @Override getAllWidgetsSelector()302 public BySelector getAllWidgetsSelector() { 303 throw new UnsupportedOperationException( 304 "All Widgets is not available on Leanback Launcher."); 305 } 306 307 @SuppressWarnings("unused") 308 @Override getAllWidgetsScrollDirection()309 public Direction getAllWidgetsScrollDirection() { 310 throw new UnsupportedOperationException( 311 "All Widgets is not available on Leanback Launcher."); 312 } 313 314 @SuppressWarnings("unused") 315 @Override getHotSeatSelector()316 public BySelector getHotSeatSelector() { 317 throw new UnsupportedOperationException( 318 "Hot Seat is not available on Leanback Launcher."); 319 } 320 321 @SuppressWarnings("unused") 322 @Override getWorkspaceScrollDirection()323 public Direction getWorkspaceScrollDirection() { 324 throw new UnsupportedOperationException( 325 "Workspace is not available on Leanback Launcher."); 326 } 327 launchApp(ILauncherStrategy launcherStrategy, BySelector app, String packageName)328 protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app, 329 String packageName) { 330 return launchApp(launcherStrategy, app, packageName, MAX_SCROLL_ATTEMPTS); 331 } 332 launchApp(ILauncherStrategy launcherStrategy, BySelector app, String packageName, int maxScrollAttempts)333 protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app, 334 String packageName, int maxScrollAttempts) { 335 unlockDeviceIfAsleep(); 336 337 if (isAppOpen(packageName)) { 338 // Application is already open 339 return 0; 340 } 341 342 // Go to the home page 343 launcherStrategy.open(); 344 // attempt to find the app icon if it's not already on the screen 345 UiObject2 container = launcherStrategy.openAllApps(false); 346 UiObject2 appIcon = container.findObject(app); 347 int attempts = 0; 348 while (attempts++ < maxScrollAttempts) { 349 // Compare the focused icon and the app icon to search for. 350 UiObject2 focusedIcon = container.findObject(By.focused(true)) 351 .findObject(By.res(getSupportedLauncherPackage(), "app_banner")); 352 353 if (appIcon == null) { 354 appIcon = findApp(container, focusedIcon, app); 355 if (appIcon == null) { 356 throw new RuntimeException("Failed to find the app icon on screen: " 357 + packageName); 358 } 359 continue; 360 } else if (focusedIcon.equals(appIcon)) { 361 // The app icon is on the screen, and selected. 362 break; 363 } else { 364 // The app icon is on the screen, but not selected yet 365 // Move one step closer to the app icon 366 Point currentPosition = focusedIcon.getVisibleCenter(); 367 Point targetPosition = appIcon.getVisibleCenter(); 368 int dx = targetPosition.x - currentPosition.x; 369 int dy = targetPosition.y - currentPosition.y; 370 final int MARGIN = 10; 371 // The sequence of moving should be kept in the following order so as not to 372 // be stuck in case that the apps row are not even. 373 if (dx < -MARGIN) { 374 mDevice.pressDPadLeft(); 375 continue; 376 } 377 if (dy < -MARGIN) { 378 mDevice.pressDPadUp(); 379 continue; 380 } 381 if (dx > MARGIN) { 382 mDevice.pressDPadRight(); 383 continue; 384 } 385 if (dy > MARGIN) { 386 mDevice.pressDPadDown(); 387 continue; 388 } 389 throw new RuntimeException( 390 "Failed to navigate to the app icon on screen: " + packageName); 391 } 392 } 393 394 if (attempts == maxScrollAttempts) { 395 throw new RuntimeException( 396 "scrollBackToBeginning: exceeded max attempts: " + maxScrollAttempts); 397 } 398 399 // The app icon is already found and focused. 400 long ready = SystemClock.uptimeMillis(); 401 mDevice.pressDPadCenter(); 402 mDevice.waitForIdle(); 403 if (packageName != null) { 404 Log.w(LOG_TAG, String.format( 405 "No UI element with package name %s detected.", packageName)); 406 boolean success = mDevice.wait(Until.hasObject( 407 By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT); 408 if (success) { 409 return ready; 410 } else { 411 return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP; 412 } 413 } else { 414 return ready; 415 } 416 } 417 isSearchRowSelected()418 protected boolean isSearchRowSelected() { 419 UiObject2 row = mDevice.findObject(getSearchRowSelector()); 420 if (row == null) { 421 return false; 422 } 423 return row.hasObject(By.focused(true)); 424 } 425 isAppsRowSelected()426 protected boolean isAppsRowSelected() { 427 UiObject2 row = mDevice.findObject(getAppsRowSelector()); 428 if (row == null) { 429 return false; 430 } 431 return row.hasObject(By.focused(true)); 432 } 433 isGamesRowSelected()434 protected boolean isGamesRowSelected() { 435 UiObject2 row = mDevice.findObject(getGamesRowSelector()); 436 if (row == null) { 437 return false; 438 } 439 return row.hasObject(By.focused(true)); 440 } 441 isNotificationRowSelected()442 protected boolean isNotificationRowSelected() { 443 UiObject2 row = mDevice.findObject(getNotificationRowSelector()); 444 if (row == null) { 445 return false; 446 } 447 return row.hasObject(By.focused(true)); 448 } 449 isSettingsRowSelected()450 protected boolean isSettingsRowSelected() { 451 // Settings label is only visible if the settings row is selected 452 return mDevice.hasObject(By.res(getSupportedLauncherPackage(), "label").text("Settings")); 453 } 454 isAppOpen(String appPackage)455 protected boolean isAppOpen (String appPackage) { 456 return mDevice.hasObject(By.pkg(appPackage).depth(0)); 457 } 458 unlockDeviceIfAsleep()459 protected void unlockDeviceIfAsleep () { 460 // Turn screen on if necessary 461 try { 462 if (!mDevice.isScreenOn()) { 463 mDevice.wakeUp(); 464 } 465 } catch (RemoteException e) { 466 Log.e(LOG_TAG, "Failed to unlock the screen-off device.", e); 467 } 468 } 469 findApp(UiObject2 container, UiObject2 focusedIcon, BySelector app)470 protected UiObject2 findApp(UiObject2 container, UiObject2 focusedIcon, BySelector app) { 471 UiObject2 appIcon; 472 // The app icon is not on the screen. 473 // Search by going left first until it finds the app icon on the screen 474 String prevText = focusedIcon.getContentDescription(); 475 String nextText; 476 do { 477 mDevice.pressDPadLeft(); 478 appIcon = container.findObject(app); 479 if (appIcon != null) { 480 return appIcon; 481 } 482 nextText = container.findObject(By.focused(true)).findObject( 483 By.res(getSupportedLauncherPackage(), 484 "app_banner")).getContentDescription(); 485 } while (nextText != null && !nextText.equals(prevText)); 486 487 // If we haven't found it yet, search by going right 488 do { 489 mDevice.pressDPadRight(); 490 appIcon = container.findObject(app); 491 if (appIcon != null) { 492 return appIcon; 493 } 494 nextText = container.findObject(By.focused(true)).findObject( 495 By.res(getSupportedLauncherPackage(), 496 "app_banner")).getContentDescription(); 497 } while (nextText != null && !nextText.equals(prevText)); 498 return null; 499 } 500 } 501