1 /* 2 * Copyright (C) 2017 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.app.Instrumentation; 20 import android.content.pm.ApplicationInfo; 21 import android.content.pm.PackageInfo; 22 import android.content.pm.PackageManager; 23 import android.content.pm.PackageManager.NameNotFoundException; 24 import android.graphics.Point; 25 import android.os.RemoteException; 26 import android.os.SystemClock; 27 import android.platform.test.utils.DPadUtil; 28 import android.support.test.uiautomator.By; 29 import android.support.test.uiautomator.BySelector; 30 import android.support.test.uiautomator.Direction; 31 import android.support.test.uiautomator.UiDevice; 32 import android.support.test.uiautomator.UiObject2; 33 import android.support.test.uiautomator.Until; 34 import android.util.Log; 35 import android.view.KeyEvent; 36 37 import org.junit.Assert; 38 39 import java.io.ByteArrayOutputStream; 40 import java.io.IOException; 41 42 43 public class TvLauncherStrategy implements ILeanbackLauncherStrategy { 44 45 private static final String LOG_TAG = TvLauncherStrategy.class.getSimpleName(); 46 private static final String PACKAGE_LAUNCHER = "com.google.android.tvlauncher"; 47 private static final String PACKAGE_SETTINGS = "com.android.tv.settings"; 48 private static final String CHANNEL_TITLE_WATCH_NEXT = "Watch Next"; 49 50 // Build version 51 private static final int BUILD_INT_BANDGAP = 1010100000; 52 53 // Wait time 54 private static final int UI_APP_LAUNCH_WAIT_TIME_MS = 10000; 55 private static final int UI_WAIT_TIME_MS = 5000; 56 private static final int UI_TRANSITION_WAIT_TIME_MS = 1000; 57 private static final int NO_WAIT = 0; 58 59 // Note that the selector specifies criteria for matching an UI element from/to a focused item 60 private static final BySelector SELECTOR_TOP_ROW = By.res(PACKAGE_LAUNCHER, "top_row"); 61 private static final BySelector SELECTOR_APPS_ROW = By.res(PACKAGE_LAUNCHER, "apps_row"); 62 private static final BySelector SELECTOR_ALL_APPS_VIEW = 63 By.res(PACKAGE_LAUNCHER, "row_list_view"); 64 private static final BySelector SELECTOR_ALL_APPS_LOGO = 65 By.res(PACKAGE_LAUNCHER, "channel_logo").focused(true).descContains("Apps"); 66 private static final BySelector SELECTOR_CONFIG_CHANNELS_ROW = 67 By.res(PACKAGE_LAUNCHER, "configure_channels_row"); 68 private static final BySelector SELECTOR_CONTROLLER_MOVE = By.res(PACKAGE_LAUNCHER, "move"); 69 private static final BySelector SELECTOR_CONTROLLER_REMOVE = By.res(PACKAGE_LAUNCHER, "remove"); 70 private static final BySelector SELECTOR_NOTIFICATIONS_ROW = By.res(PACKAGE_LAUNCHER, 71 "notifications_row"); 72 73 protected UiDevice mDevice; 74 protected DPadUtil mDPadUtil; 75 private Instrumentation mInstrumentation; 76 77 /** A {@link UiCondition} is a condition to be satisfied by BaseView or UI actions. */ 78 public interface UiCondition { apply(UiObject2 focus)79 boolean apply(UiObject2 focus); 80 } 81 82 /** 83 * State of an item in Apps row or channel row on the Home Screen. 84 */ 85 public enum HomeRowState { 86 /** 87 * State of a row when this or some other items in Apps row or channel row is not selected 88 */ 89 DEFAULT, 90 /** 91 * State of a row when this or some other items in Apps row or channel row is selected. 92 */ 93 SELECTED, 94 /** 95 * State of an item when one of the zoomed out states is focused: 96 * zoomed_out, channel_actions, move 97 */ 98 ZOOMED_OUT 99 } 100 101 /** 102 * State of an item in the HomeAppState.ZOOMED_OUT mode 103 */ 104 public enum HomeControllerState { 105 /** 106 * Default state of an app. one of the program cards or non-channel rows is selected 107 */ 108 DEFAULT, 109 /** 110 * One of the channel logos is selected, the channel title is zoomed out 111 */ 112 CHANNEL_LOGO, 113 /** 114 * State when a channel is selected and showing channel actions (remove and move). 115 */ 116 CHANNEL_ACTIONS, 117 /** 118 * State when a channel is being moved. 119 */ 120 MOVE_CHANNEL 121 } 122 123 /** 124 * A TvLauncherUnsupportedOperationException is an exception specific to TV Launcher. This will 125 * be thrown when the feature/method is not available on the TV Launcher. 126 */ 127 class TvLauncherUnsupportedOperationException extends UnsupportedOperationException { TvLauncherUnsupportedOperationException()128 TvLauncherUnsupportedOperationException() { 129 super(); 130 } TvLauncherUnsupportedOperationException(String msg)131 TvLauncherUnsupportedOperationException(String msg) { 132 super(msg); 133 } 134 } 135 136 /** 137 * {@inheritDoc} 138 */ 139 @Override getSupportedLauncherPackage()140 public String getSupportedLauncherPackage() { 141 return PACKAGE_LAUNCHER; 142 } 143 144 /** 145 * {@inheritDoc} 146 */ 147 @Override setUiDevice(UiDevice uiDevice)148 public void setUiDevice(UiDevice uiDevice) { 149 mDevice = uiDevice; 150 mDPadUtil = new DPadUtil(mDevice); 151 } 152 153 /** 154 * {@inheritDoc} 155 */ 156 @Override open()157 public void open() { 158 // if we see main list view, assume at home screen already 159 if (!mDevice.hasObject(getWorkspaceSelector())) { 160 mDPadUtil.pressHome(); 161 // ensure launcher is shown 162 if (!mDevice.wait(Until.hasObject(getWorkspaceSelector()), UI_WAIT_TIME_MS)) { 163 // HACK: dump hierarchy to logcat 164 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 165 try { 166 mDevice.dumpWindowHierarchy(baos); 167 baos.flush(); 168 baos.close(); 169 String[] lines = baos.toString().split("\\r?\\n"); 170 for (String line : lines) { 171 Log.d(LOG_TAG, line.trim()); 172 } 173 } catch (IOException ioe) { 174 Log.e(LOG_TAG, "error dumping XML to logcat", ioe); 175 } 176 throw new RuntimeException("Failed to open TV launcher"); 177 } 178 mDevice.waitForIdle(); 179 } 180 } 181 182 /** 183 * {@inheritDoc} 184 * There are two different ways to open All Apps view. If longpress is true, it will long press 185 * the HOME key to open it. Otherwise it will navigate to the "APPS" logo on the Apps row. 186 */ 187 @Override openAllApps(boolean longpress)188 public UiObject2 openAllApps(boolean longpress) { 189 if (!mDevice.hasObject(getAllAppsSelector())) { 190 if (longpress) { 191 mDPadUtil.longPressKeyCode(KeyEvent.KEYCODE_HOME); 192 } else { 193 Assert.assertNotNull("Could not find all apps logo", selectAllAppsLogo()); 194 mDPadUtil.pressDPadCenter(); 195 } 196 } 197 return mDevice.wait(Until.findObject(getAllAppsSelector()), UI_WAIT_TIME_MS); 198 } 199 openSettings()200 public boolean openSettings() { 201 Assert.assertNotNull(selectTopRow()); 202 Assert.assertNotNull(selectBidirect(By.res(getSupportedLauncherPackage(), "settings"), 203 Direction.RIGHT)); 204 mDPadUtil.pressDPadCenter(); 205 return mDevice.wait( 206 Until.hasObject(By.res(PACKAGE_SETTINGS, "decor_title").text("Settings")), 207 UI_WAIT_TIME_MS); 208 } 209 openCustomizeChannels()210 public boolean openCustomizeChannels() { 211 Assert.assertNotNull(selectCustomizeChannelsRow()); 212 Assert.assertNotNull( 213 select(By.res(getSupportedLauncherPackage(), "button"), Direction.RIGHT, 214 UI_WAIT_TIME_MS)); 215 mDPadUtil.pressDPadCenter(); 216 return mDevice.wait( 217 Until.hasObject(By.res(PACKAGE_LAUNCHER, "decor_title").text("Customize channels")), 218 UI_WAIT_TIME_MS); 219 } 220 221 /** 222 * Get the launcher's version code. 223 * @return the version code. -1 if the launcher package is not found. 224 */ getVersionCode()225 public int getVersionCode() { 226 String pkg = getSupportedLauncherPackage(); 227 if (null == pkg || pkg.isEmpty()) { 228 throw new RuntimeException("Can't find version of empty package"); 229 } 230 if (mInstrumentation == null) { 231 Log.w(LOG_TAG, "Instrumentation is null. setInstrumentation should be called " 232 + "to get the version code"); 233 return -1; 234 } 235 PackageManager pm = mInstrumentation.getContext().getPackageManager(); 236 PackageInfo pInfo = null; 237 try { 238 pInfo = pm.getPackageInfo(pkg, 0); 239 return pInfo.versionCode; 240 } catch (NameNotFoundException e) { 241 Log.w(LOG_TAG, String.format("package name is not found: %s", pkg)); 242 return -1; 243 } 244 } 245 246 /** 247 * {@inheritDoc} 248 */ 249 @Override getWorkspaceSelector()250 public BySelector getWorkspaceSelector() { 251 return By.res(getSupportedLauncherPackage(), "home_view_container"); 252 } 253 254 /** 255 * {@inheritDoc} 256 */ 257 @Override getSearchRowSelector()258 public BySelector getSearchRowSelector() { 259 return SELECTOR_TOP_ROW; 260 } 261 262 /** 263 * {@inheritDoc} 264 */ 265 @Override getAppsRowSelector()266 public BySelector getAppsRowSelector() { 267 return SELECTOR_APPS_ROW; 268 } 269 270 /** 271 * {@inheritDoc} 272 */ 273 @Override getGamesRowSelector()274 public BySelector getGamesRowSelector() { 275 // Note that the apps and games are now in the same row on new TV Launcher. 276 return getAppsRowSelector(); 277 } 278 279 /** 280 * {@inheritDoc} 281 */ 282 @Override getAllAppsScrollDirection()283 public Direction getAllAppsScrollDirection() { 284 return Direction.DOWN; 285 } 286 287 /** 288 * {@inheritDoc} 289 */ 290 @Override getAllAppsSelector()291 public BySelector getAllAppsSelector() { 292 return SELECTOR_ALL_APPS_VIEW; 293 } 294 getAllAppsLogoSelector()295 public BySelector getAllAppsLogoSelector() { 296 return SELECTOR_ALL_APPS_LOGO; 297 } 298 299 /** 300 * Returns a {@link BySelector} describing a given favorite app 301 */ getFavoriteAppSelector(String appName)302 public BySelector getFavoriteAppSelector(String appName) { 303 return By.res(getSupportedLauncherPackage(), "favorite_app_banner").desc(appName); 304 } 305 306 /** 307 * Returns a {@link BySelector} describing a given app in Apps View 308 */ getAppInAppsViewSelector(String appName)309 public BySelector getAppInAppsViewSelector(String appName) { 310 if (getVersionCode() > BUILD_INT_BANDGAP) { 311 // bandgap or higher 312 return By.res(getSupportedLauncherPackage(), "banner_image").desc(appName); 313 } 314 return By.res(getSupportedLauncherPackage(), "app_title").text(appName); 315 } 316 317 // Return a {@link BySelector} indicating a channel logo (in either zoom-in or default mode) getChannelLogoSelector()318 public BySelector getChannelLogoSelector() { 319 return By.res(getSupportedLauncherPackage(), "channel_logo"); 320 } getChannelLogoSelector(String channelTitle)321 public BySelector getChannelLogoSelector(String channelTitle) { 322 return getChannelLogoSelector().desc(channelTitle); 323 } 324 325 // Return the list of rows including "top_row", "apps_row", "channel" 326 // and "configure_channels_row" getRowListSelector()327 public BySelector getRowListSelector() { 328 return By.res(getSupportedLauncherPackage(), "home_row_list"); 329 } 330 getHomeRowState()331 public HomeRowState getHomeRowState() { 332 HomeRowState state = HomeRowState.DEFAULT; 333 if (isAppsRowSelected() || isChannelRowSelected()) { 334 if (getHomeControllerState() != HomeControllerState.DEFAULT) { 335 state = HomeRowState.ZOOMED_OUT; 336 } else { 337 state = HomeRowState.SELECTED; 338 } 339 } 340 Log.d(LOG_TAG, String.format("[HomeRowState]%s", state)); 341 return state; 342 } 343 getHomeControllerState()344 public HomeControllerState getHomeControllerState() { 345 HomeControllerState state = HomeControllerState.DEFAULT; 346 UiObject2 focus = findFocus(); 347 if (focus.hasObject(getChannelLogoSelector())) { 348 state = HomeControllerState.CHANNEL_LOGO; 349 } else if (focus.hasObject(SELECTOR_CONTROLLER_MOVE)) { 350 state = HomeControllerState.MOVE_CHANNEL; 351 } else if (focus.hasObject(SELECTOR_CONTROLLER_REMOVE)) { 352 state = HomeControllerState.CHANNEL_ACTIONS; 353 } 354 Log.d(LOG_TAG, String.format("[HomeControllerState]%s", state)); 355 return state; 356 } 357 358 // Return an index of a focused app or program in the Row. 0-based. getFocusedItemIndexInRow()359 public int getFocusedItemIndexInRow() { 360 UiObject2 focusedChannel = mDevice.wait(Until.findObject( 361 By.res(getSupportedLauncherPackage(), "items_list") 362 .hasDescendant(By.focused(true))), UI_WAIT_TIME_MS); 363 if (focusedChannel == null) { 364 Log.w(LOG_TAG, "getFocusedItemIndexInRow: no channel has a focused item. " 365 + "A focus may be at a logo or the top row."); 366 return -1; 367 } 368 int index = 0; 369 for (UiObject2 program : focusedChannel.getChildren()) { 370 if (findFocus(program, NO_WAIT) != null) { 371 break; 372 } 373 ++index; 374 } 375 Log.d(LOG_TAG, String.format("getFocusedItemIndexInRow [index]%d", index)); 376 return index; 377 } 378 379 /** 380 * Return true if any item in Channel row is selected. eg, program, zoomed out, channel actions 381 */ isChannelRowSelected(String channelTitle)382 public boolean isChannelRowSelected(String channelTitle) { 383 return isChannelRowSelected(getChannelLogoSelector(channelTitle)); 384 } isChannelRowSelected()385 public boolean isChannelRowSelected() { 386 return isChannelRowSelected(getChannelLogoSelector()); 387 } isChannelRowSelected(final BySelector channelSelector)388 protected boolean isChannelRowSelected(final BySelector channelSelector) { 389 UiObject2 rowList = mDevice.findObject(getRowListSelector()); 390 for (UiObject2 row : rowList.getChildren()) { 391 if (findFocus(row, NO_WAIT) != null) { 392 return row.hasObject(channelSelector); 393 } 394 } 395 return false; 396 } 397 isOnHomeScreen()398 public boolean isOnHomeScreen() { 399 if (!isAppOpen(getSupportedLauncherPackage())) { 400 Log.w(LOG_TAG, "This launcher is not in foreground"); 401 } 402 return mDevice.hasObject(getWorkspaceSelector()); 403 } 404 isFirstAppSelected()405 public boolean isFirstAppSelected() { 406 if (!isAppsRowSelected()) { 407 return false; 408 } 409 return (getFocusedItemIndexInRow() == 0); 410 } 411 412 /** 413 * {@inheritDoc} 414 */ 415 @Override launch(String appName, String packageName)416 public long launch(String appName, String packageName) { 417 Log.d(LOG_TAG, String.format("launching [name]%s [package]%s", appName, packageName)); 418 return launchApp(this, appName, packageName, isGame(packageName)); 419 } 420 421 /** 422 * {@inheritDoc} 423 * <p> 424 * This function must be called before any UI test runs on TV. 425 * </p> 426 */ 427 @Override setInstrumentation(Instrumentation instrumentation)428 public void setInstrumentation(Instrumentation instrumentation) { 429 mInstrumentation = instrumentation; 430 } 431 432 /** 433 * {@inheritDoc} 434 */ 435 @Override selectSearchRow()436 public UiObject2 selectSearchRow() { 437 // The Search orb is now on top row on TV Launcher 438 return selectTopRow(); 439 } 440 441 /** 442 * {@inheritDoc} 443 */ 444 @Override selectAppsRow()445 public UiObject2 selectAppsRow() { 446 return selectAppsRow(false); 447 } 448 selectAppsRow(boolean useHomeKey)449 public UiObject2 selectAppsRow(boolean useHomeKey) { 450 Log.d(LOG_TAG, "selectAppsRow"); 451 if (!isOnHomeScreen()) { 452 Log.w(LOG_TAG, "selectAppsRow should be called on Home screen"); 453 open(); 454 } 455 456 if (useHomeKey) { 457 // Press the HOME key to move a focus to the first app in the Apps row. 458 mDPadUtil.pressHome(); 459 } else { 460 selectBidirect(getAppsRowSelector().hasDescendant(By.focused(true)), 461 Direction.DOWN); 462 } 463 return isAppsRowSelected() ? findFocus() : null; 464 } 465 466 /** 467 * Select a channel row that matches a given name. 468 */ selectChannelRow(final String channelTitle)469 public UiObject2 selectChannelRow(final String channelTitle) { 470 Log.d(LOG_TAG, String.format("selectChannelRow [channel]%s", channelTitle)); 471 472 // Move out if any channel action button (eg, remove, move) is focused, so that 473 // it can scroll vertically to find a given row. 474 selectBidirect( 475 new UiCondition() { 476 @Override 477 public boolean apply(UiObject2 focus) { 478 HomeControllerState state = getHomeControllerState(); 479 return !(state == HomeControllerState.CHANNEL_ACTIONS 480 || state == HomeControllerState.MOVE_CHANNEL); 481 } 482 }, Direction.RIGHT); 483 484 // Then scroll vertically to find a given row 485 UiObject2 focused = selectBidirect( 486 new UiCondition() { 487 @Override 488 public boolean apply(UiObject2 focus) { 489 return isChannelRowSelected(channelTitle); 490 } 491 }, Direction.DOWN); 492 return focused; 493 } 494 495 /** 496 * Select the All Apps logo (or icon). 497 */ selectAllAppsLogo()498 public UiObject2 selectAllAppsLogo() { 499 Log.d(LOG_TAG, "selectAllAppsLogo"); 500 return selectChannelLogo("Apps"); 501 } 502 selectChannelLogo(final String channelTitle)503 public UiObject2 selectChannelLogo(final String channelTitle) { 504 Log.d(LOG_TAG, String.format("selectChannelLogo [channel]%s", channelTitle)); 505 506 if (!isChannelRowSelected(channelTitle)) { 507 Assert.assertNotNull(selectChannelRow(channelTitle)); 508 } 509 return selectBidirect( 510 new UiCondition() { 511 @Override 512 public boolean apply(UiObject2 focus) { 513 return getHomeControllerState() == HomeControllerState.CHANNEL_LOGO; 514 } 515 }, 516 Direction.LEFT); 517 } 518 519 /** 520 * Returns a {@link UiObject2} describing the Top row on TV Launcher 521 * @return 522 */ 523 public UiObject2 selectTopRow() { 524 open(); 525 mDPadUtil.pressHome(); 526 // Move up until it reaches the top. 527 int maxAttempts = 3; 528 while (maxAttempts-- > 0 && move(Direction.UP)) { 529 SystemClock.sleep(UI_TRANSITION_WAIT_TIME_MS); 530 } 531 return mDevice.wait( 532 Until.findObject(getSearchRowSelector().hasDescendant(By.focused(true))), 533 UI_TRANSITION_WAIT_TIME_MS); 534 } 535 536 /** 537 * Returns a {@link UiObject2} describing the Notification row on TV Launcher 538 * @return 539 */ 540 public UiObject2 selectNotificationRow() { 541 return selectBidirect(By.copy(SELECTOR_NOTIFICATIONS_ROW).hasDescendant(By.focused(true)), 542 Direction.UP); 543 } 544 545 /** 546 * Returns a {@link UiObject2} describing the customize channel row on TV Launcher 547 * @return 548 */ 549 public UiObject2 selectCustomizeChannelsRow() { 550 return select(By.copy(SELECTOR_CONFIG_CHANNELS_ROW).hasDescendant(By.focused(true)), 551 Direction.DOWN, UI_TRANSITION_WAIT_TIME_MS); 552 } 553 554 public UiObject2 selectWatchNextRow() { 555 return selectChannelRow(CHANNEL_TITLE_WATCH_NEXT); 556 } 557 558 /** 559 * Select the first app icon in the Apps row 560 */ 561 public UiObject2 selectFirstAppIcon() { 562 if (!isFirstAppSelected()) { 563 Assert.assertNotNull("The Apps row must be selected.", 564 selectAppsRow(/*useHomeKey*/ true)); 565 mDPadUtil.pressBack(); 566 if (getHomeRowState() == HomeRowState.ZOOMED_OUT) { 567 mDPadUtil.pressDPadRight(); 568 } 569 } 570 Assert.assertTrue("The first app in Apps row must be selected.", isFirstAppSelected()); 571 return findFocus(); 572 } 573 574 /** 575 * {@inheritDoc} 576 */ 577 @Override 578 public UiObject2 selectGamesRow() { 579 return selectAppsRow(); 580 } 581 582 /** 583 * Select the given app in All Apps activity. 584 * When the All Apps opens, the focus is always at the top right. 585 * Search from left to right, and down to the next row, from right to left, and 586 * down to the next row like a zigzag pattern until the app is found. 587 */ 588 protected UiObject2 selectAppInAllApps(BySelector appSelector, String packageName) { 589 Assert.assertTrue(mDevice.hasObject(getAllAppsSelector())); 590 591 // Assume that the focus always starts at the top left of the Apps view. 592 final int maxScrollAttempts = 20; 593 final int margin = 30; 594 int attempts = 0; 595 UiObject2 focused = null; 596 UiObject2 expected = null; 597 while (attempts++ < maxScrollAttempts) { 598 focused = mDevice.wait(Until.findObject(By.focused(true)), UI_WAIT_TIME_MS); 599 expected = mDevice.wait(Until.findObject(appSelector), UI_WAIT_TIME_MS); 600 601 if (expected == null) { 602 mDPadUtil.pressDPadDown(); 603 continue; 604 } else if (focused.hasObject(appSelector)) { 605 // The app icon is selected. 606 Log.i(LOG_TAG, String.format("The app %s is selected", packageName)); 607 break; 608 } else { 609 // The app icon is on the screen, but not selected yet 610 // Move one step closer to the app icon 611 Point currentPosition = focused.getVisibleCenter(); 612 Point targetPosition = expected.getVisibleCenter(); 613 int dx = targetPosition.x - currentPosition.x; 614 int dy = targetPosition.y - currentPosition.y; 615 Log.d(LOG_TAG, String.format("selectAppInAllApps: [dx,dx][%d,%d]", dx, dy)); 616 if (dy > margin) { 617 mDPadUtil.pressDPadDown(); 618 continue; 619 } 620 if (dx > margin) { 621 mDPadUtil.pressDPadRight(); 622 continue; 623 } 624 if (dy < -margin) { 625 mDPadUtil.pressDPadUp(); 626 continue; 627 } 628 if (dx < -margin) { 629 mDPadUtil.pressDPadLeft(); 630 continue; 631 } 632 throw new RuntimeException( 633 "Failed to navigate to the app icon on screen: " + packageName); 634 } 635 } 636 return expected; 637 } 638 639 /** 640 * Select the given app in All Apps activity in zigzag manner. 641 * When the All Apps opens, the focus is always at the top left. 642 * Search from left to right, and down to the next row, from right to left, and 643 * down to the next row like a zigzag pattern until it founds a given app. 644 */ 645 protected UiObject2 selectAppInAllAppsZigZag(BySelector appSelector, String packageName) { 646 Assert.assertTrue(mDevice.hasObject(getAllAppsSelector())); 647 Direction direction = Direction.RIGHT; 648 UiObject2 app = select(appSelector, direction, UI_TRANSITION_WAIT_TIME_MS); 649 while (app == null && move(Direction.DOWN)) { 650 direction = Direction.reverse(direction); 651 app = select(appSelector, direction, UI_TRANSITION_WAIT_TIME_MS); 652 } 653 if (app != null) { 654 Log.i(LOG_TAG, String.format("The app %s is selected", packageName)); 655 } 656 return app; 657 } 658 659 /** 660 * Select the given app in All Apps using the versioned BySelector for the app 661 */ 662 public UiObject2 selectAppInAllApps(String appName, String packageName) { 663 UiObject2 app = null; 664 int versionCode = getVersionCode(); 665 if (versionCode > BUILD_INT_BANDGAP) { 666 // bandgap or higher 667 Log.i(LOG_TAG, 668 String.format("selectAppInAllApps: app banner has app name [versionCode]%d", 669 versionCode)); 670 app = selectAppInAllApps(getAppInAppsViewSelector(appName), packageName); 671 } else { 672 app = selectAppInAllAppsZigZag(getAppInAppsViewSelector(appName), packageName); 673 } 674 return app; 675 } 676 677 /** 678 * Launch the given app in the Apps view. 679 */ 680 public boolean launchAppInAppsView(String appName, String packageName) { 681 Log.d(LOG_TAG, String.format("launching in apps view [appName]%s [packageName]%s", 682 appName, packageName)); 683 openAllApps(true); 684 UiObject2 app = selectAppInAllApps(appName, packageName); 685 if (app == null) { 686 throw new RuntimeException( 687 "Failed to navigate to the app icon in the Apps view: " + packageName); 688 } 689 690 // The app icon is already found and focused. Then wait for it to open. 691 BySelector appMain = By.pkg(packageName).depth(0); 692 mDPadUtil.pressDPadCenter(); 693 if (!mDevice.wait(Until.hasObject(appMain), UI_APP_LAUNCH_WAIT_TIME_MS)) { 694 Log.w(LOG_TAG, String.format( 695 "No UI element with package name %s detected.", packageName)); 696 return false; 697 } 698 return true; 699 } 700 701 protected long launchApp(ILauncherStrategy launcherStrategy, String appName, 702 String packageName, boolean isGame) { 703 unlockDeviceIfAsleep(); 704 705 if (isAppOpen(packageName)) { 706 // Application is already open 707 return 0; 708 } 709 710 // Go to the home page, and select the Apps row 711 launcherStrategy.open(); 712 selectAppsRow(); 713 714 // Search for the app in the Favorite Apps row first. 715 // If not exists, open the 'All Apps' and search for the app there 716 UiObject2 app = null; 717 BySelector favAppSelector = getFavoriteAppSelector(appName); 718 if (mDevice.hasObject(favAppSelector)) { 719 app = selectBidirect(By.copy(favAppSelector).focused(true), Direction.RIGHT); 720 } else { 721 openAllApps(true); 722 app = selectAppInAllApps(appName, packageName); 723 } 724 if (app == null) { 725 throw new RuntimeException( 726 "Failed to navigate to the app icon on screen: " + packageName); 727 } 728 729 // The app icon is already found and focused. Then wait for it to open. 730 long ready = SystemClock.uptimeMillis(); 731 BySelector appMain = By.pkg(packageName).depth(0); 732 mDPadUtil.pressDPadCenter(); 733 if (packageName != null) { 734 if (!mDevice.wait(Until.hasObject(appMain), UI_APP_LAUNCH_WAIT_TIME_MS)) { 735 Log.w(LOG_TAG, String.format( 736 "No UI element with package name %s detected.", packageName)); 737 return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP; 738 } 739 } 740 return ready; 741 } 742 743 protected boolean isTopRowSelected() { 744 UiObject2 row = mDevice.findObject(getSearchRowSelector()); 745 if (row == null) { 746 return false; 747 } 748 return row.hasObject(By.focused(true)); 749 } 750 751 protected boolean isAppsRowSelected() { 752 UiObject2 row = mDevice.findObject(getAppsRowSelector()); 753 if (row == null) { 754 return false; 755 } 756 return row.hasObject(By.focused(true)); 757 } 758 759 protected boolean isGamesRowSelected() { 760 return isAppsRowSelected(); 761 } 762 763 // TODO(hyungtaekim): Move in the common helper 764 protected boolean isAppOpen(String appPackage) { 765 return mDevice.hasObject(By.pkg(appPackage).depth(0)); 766 } 767 768 // TODO(hyungtaekim): Move in the common helper 769 protected void unlockDeviceIfAsleep() { 770 // Turn screen on if necessary 771 try { 772 if (!mDevice.isScreenOn()) { 773 mDevice.wakeUp(); 774 } 775 } catch (RemoteException e) { 776 Log.e(LOG_TAG, "Failed to unlock the screen-off device.", e); 777 } 778 } 779 780 private boolean isGame(String packageName) { 781 boolean isGame = false; 782 if (mInstrumentation != null) { 783 try { 784 ApplicationInfo appInfo = 785 mInstrumentation.getTargetContext().getPackageManager().getApplicationInfo( 786 packageName, 0); 787 // TV game apps should use the "isGame" tag added since the L release. They are 788 // listed on the Games row on the TV Launcher. 789 isGame = (appInfo.metaData != null && appInfo.metaData.getBoolean("isGame", false)) 790 || ((appInfo.flags & ApplicationInfo.FLAG_IS_GAME) != 0); 791 Log.i(LOG_TAG, String.format("The package %s isGame: %b", packageName, isGame)); 792 } catch (NameNotFoundException e) { 793 Log.w(LOG_TAG, 794 String.format("No package found: %s, error:%s", packageName, e.toString())); 795 return false; 796 } 797 } 798 return isGame; 799 } 800 801 /** 802 * {@inheritDoc} 803 */ 804 @Override 805 public void search(String query) { 806 // TODO: Implement this method when the feature is available 807 throw new UnsupportedOperationException("search is not yet implemented"); 808 } 809 810 public void selectRestrictedProfile() { 811 // TODO: Implement this method when the feature is available 812 throw new UnsupportedOperationException( 813 "The Restricted Profile is not yet available on TV Launcher."); 814 } 815 816 817 // Convenient methods for UI actions 818 819 /** 820 * Select an UI element with given {@link BySelector}. This action keeps moving a focus 821 * in a given {@link Direction} until it finds a matched element. 822 * @param selector the search criteria to match an element 823 * @param direction the direction to find 824 * @param timeoutMs timeout in milliseconds to select 825 * @return a UiObject2 which represents the matched element 826 */ 827 public UiObject2 select(final BySelector selector, Direction direction, long timeoutMs) { 828 return select(new UiCondition() { 829 @Override 830 public boolean apply(UiObject2 focus) { 831 return mDevice.hasObject(selector); 832 } 833 }, direction, timeoutMs); 834 } 835 836 public UiObject2 select(UiCondition condition, Direction direction, long timeoutMs) { 837 UiObject2 focus = findFocus(null, timeoutMs); 838 while (!condition.apply(focus)) { 839 Log.d(LOG_TAG, String.format("conditional select: moving a focus from %s to %s", 840 focus, direction)); 841 UiObject2 focused = focus; 842 mDPadUtil.pressDPad(direction); 843 focus = findFocus(); 844 // Hack: A focus might be lost in some UI. Take one more step forward. 845 if (focus == null) { 846 mDPadUtil.pressDPad(direction); 847 focus = findFocus(null, timeoutMs); 848 } 849 // Check if it reaches to an end where it no longer moves a focus to next element 850 if (focused.equals(focus)) { 851 Log.d(LOG_TAG, "conditional select: not found until it reaches to an end."); 852 return null; 853 } 854 } 855 Log.i(LOG_TAG, String.format("conditional select: selected, %s", focus)); 856 return focus; 857 } 858 859 /** 860 * Select an element with a given {@link BySelector} in both given direction and reverse. 861 */ 862 public UiObject2 selectBidirect(BySelector selector, Direction direction) { 863 Log.d(LOG_TAG, String.format("selectBidirect [direction]%s", direction)); 864 UiObject2 object = select(selector, direction, UI_TRANSITION_WAIT_TIME_MS); 865 if (object == null) { 866 object = select(selector, Direction.reverse(direction), UI_TRANSITION_WAIT_TIME_MS); 867 } 868 return object; 869 } 870 871 public UiObject2 selectBidirect(UiCondition condition, Direction direction) { 872 UiObject2 object = select(condition, direction, UI_WAIT_TIME_MS); 873 if (object == null) { 874 object = select(condition, Direction.reverse(direction), UI_WAIT_TIME_MS); 875 } 876 return object; 877 } 878 879 /** 880 * Simulate a move pressing a key code. 881 * Return true if a focus is shifted on TV UI, otherwise false. 882 */ 883 public boolean move(Direction direction) { 884 int keyCode = KeyEvent.KEYCODE_UNKNOWN; 885 switch (direction) { 886 case LEFT: 887 keyCode = KeyEvent.KEYCODE_DPAD_LEFT; 888 break; 889 case RIGHT: 890 keyCode = KeyEvent.KEYCODE_DPAD_RIGHT; 891 break; 892 case UP: 893 keyCode = KeyEvent.KEYCODE_DPAD_UP; 894 break; 895 case DOWN: 896 keyCode = KeyEvent.KEYCODE_DPAD_DOWN; 897 break; 898 default: 899 throw new RuntimeException(String.format("This direction %s is not supported.", 900 direction)); 901 } 902 UiObject2 focus = mDevice.wait(Until.findObject(By.focused(true)), 903 UI_TRANSITION_WAIT_TIME_MS); 904 mDPadUtil.pressKeyCodeAndWait(keyCode); 905 return !focus.equals(mDevice.wait(Until.findObject(By.focused(true)), 906 UI_TRANSITION_WAIT_TIME_MS)); 907 } 908 909 /** 910 * Return the {@link UiObject2} that has a focused element searching through the entire view 911 * hierarchy. 912 */ 913 public UiObject2 findFocus(UiObject2 fromObject, long timeoutMs) { 914 UiObject2 focused; 915 if (fromObject == null) { 916 focused = mDevice.wait(Until.findObject(By.focused(true)), timeoutMs); 917 } else { 918 focused = fromObject.wait(Until.findObject(By.focused(true)), timeoutMs); 919 } 920 return focused; 921 } 922 923 public UiObject2 findFocus() { 924 return findFocus(null, UI_WAIT_TIME_MS); 925 } 926 927 // Unsupported methods 928 929 @SuppressWarnings("unused") 930 @Override 931 public BySelector getNotificationRowSelector() { 932 throw new TvLauncherUnsupportedOperationException("No Notification row"); 933 } 934 935 @SuppressWarnings("unused") 936 @Override 937 public BySelector getSettingsRowSelector() { 938 throw new TvLauncherUnsupportedOperationException("No Settings row"); 939 } 940 941 @SuppressWarnings("unused") 942 @Override 943 public BySelector getAppWidgetSelector() { 944 throw new TvLauncherUnsupportedOperationException(); 945 } 946 947 @SuppressWarnings("unused") 948 @Override 949 public BySelector getNowPlayingCardSelector() { 950 throw new TvLauncherUnsupportedOperationException("No Now Playing Card"); 951 } 952 953 @SuppressWarnings("unused") 954 @Override 955 public UiObject2 selectSettingsRow() { 956 throw new TvLauncherUnsupportedOperationException("No Settings row"); 957 } 958 959 @SuppressWarnings("unused") 960 @Override 961 public boolean hasAppWidgetSelector() { 962 throw new TvLauncherUnsupportedOperationException(); 963 } 964 965 @SuppressWarnings("unused") 966 @Override 967 public boolean hasNowPlayingCard() { 968 throw new TvLauncherUnsupportedOperationException("No Now Playing Card"); 969 } 970 971 @SuppressWarnings("unused") 972 @Override 973 public BySelector getAllAppsButtonSelector() { 974 throw new TvLauncherUnsupportedOperationException("No All Apps button"); 975 } 976 977 @SuppressWarnings("unused") 978 @Override 979 public UiObject2 openAllWidgets(boolean reset) { 980 throw new TvLauncherUnsupportedOperationException("No All Widgets"); 981 } 982 983 @SuppressWarnings("unused") 984 @Override 985 public BySelector getAllWidgetsSelector() { 986 throw new TvLauncherUnsupportedOperationException("No All Widgets"); 987 } 988 989 @SuppressWarnings("unused") 990 @Override 991 public Direction getAllWidgetsScrollDirection() { 992 throw new TvLauncherUnsupportedOperationException("No All Widgets"); 993 } 994 995 @SuppressWarnings("unused") 996 @Override 997 public BySelector getHotSeatSelector() { 998 throw new TvLauncherUnsupportedOperationException("No Hot seat"); 999 } 1000 1001 @SuppressWarnings("unused") 1002 @Override 1003 public Direction getWorkspaceScrollDirection() { 1004 throw new TvLauncherUnsupportedOperationException("No Workspace"); 1005 } 1006 } 1007