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.app.Instrumentation; 20 import android.content.pm.ApplicationInfo; 21 import android.content.pm.PackageManager; 22 import android.graphics.Point; 23 import android.os.RemoteException; 24 import android.os.SystemClock; 25 import android.platform.test.utils.DPadUtil; 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 import android.util.Log; 33 34 import java.io.ByteArrayOutputStream; 35 import java.io.IOException; 36 37 public class LeanbackLauncherStrategy implements ILeanbackLauncherStrategy { 38 39 private static final String LOG_TAG = LeanbackLauncherStrategy.class.getSimpleName(); 40 private static final String PACKAGE_LAUNCHER = "com.google.android.leanbacklauncher"; 41 private static final String PACKAGE_SEARCH = "com.google.android.katniss"; 42 43 private static final int MAX_SCROLL_ATTEMPTS = 20; 44 private static final int APP_LAUNCH_TIMEOUT = 10000; 45 private static final int SHORT_WAIT_TIME = 5000; // 5 sec 46 private static final int NOTIFICATION_WAIT_TIME = 60000; 47 48 protected UiDevice mDevice; 49 protected DPadUtil mDPadUtil; 50 private Instrumentation mInstrumentation; 51 52 53 /** 54 * {@inheritDoc} 55 */ 56 @Override getSupportedLauncherPackage()57 public String getSupportedLauncherPackage() { 58 return PACKAGE_LAUNCHER; 59 } 60 61 /** 62 * {@inheritDoc} 63 */ 64 @Override setUiDevice(UiDevice uiDevice)65 public void setUiDevice(UiDevice uiDevice) { 66 mDevice = uiDevice; 67 mDPadUtil = new DPadUtil(mDevice); 68 } 69 70 /** 71 * {@inheritDoc} 72 */ 73 @Override open()74 public void open() { 75 // if we see main list view, assume at home screen already 76 if (!mDevice.hasObject(getWorkspaceSelector())) { 77 mDPadUtil.pressHome(); 78 // ensure launcher is shown 79 if (!mDevice.wait(Until.hasObject(getWorkspaceSelector()), SHORT_WAIT_TIME)) { 80 // HACK: dump hierarchy to logcat 81 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 82 try { 83 mDevice.dumpWindowHierarchy(baos); 84 baos.flush(); 85 baos.close(); 86 String[] lines = baos.toString().split("\\r?\\n"); 87 for (String line : lines) { 88 Log.d(LOG_TAG, line.trim()); 89 } 90 } catch (IOException ioe) { 91 Log.e(LOG_TAG, "error dumping XML to logcat", ioe); 92 } 93 throw new RuntimeException("Failed to open leanback launcher"); 94 } 95 mDevice.waitForIdle(); 96 } 97 } 98 99 /** 100 * {@inheritDoc} 101 */ 102 @Override openAllApps(boolean reset)103 public UiObject2 openAllApps(boolean reset) { 104 UiObject2 appsRow = selectAppsRow(); 105 if (appsRow == null) { 106 throw new RuntimeException("Could not find all apps row"); 107 } 108 if (reset) { 109 Log.w(LOG_TAG, "The reset will be ignored on leanback launcher"); 110 } 111 return appsRow; 112 } 113 114 /** {@inheritDoc} */ 115 @Override openOverview()116 public void openOverview() { 117 throw new UnsupportedOperationException("Overview is not available on Leanback Launcher."); 118 } 119 120 /** {@inheritDoc} */ 121 @Override clearRecentAppsFromOverview()122 public boolean clearRecentAppsFromOverview() { 123 throw new UnsupportedOperationException( 124 "Recent apps are not available on Leanback Launcher."); 125 } 126 127 /** 128 * {@inheritDoc} 129 */ 130 @Override getWorkspaceSelector()131 public BySelector getWorkspaceSelector() { 132 return By.res(getSupportedLauncherPackage(), "main_list_view"); 133 } 134 135 /** 136 * {@inheritDoc} 137 */ 138 @Override getSearchRowSelector()139 public BySelector getSearchRowSelector() { 140 return By.res(getSupportedLauncherPackage(), "search_view"); 141 } 142 143 /** 144 * {@inheritDoc} 145 */ 146 @Override getNotificationRowSelector()147 public BySelector getNotificationRowSelector() { 148 return By.res(getSupportedLauncherPackage(), "notification_view"); 149 } 150 151 /** 152 * {@inheritDoc} 153 */ 154 @Override getAppsRowSelector()155 public BySelector getAppsRowSelector() { 156 return By.res(getSupportedLauncherPackage(), "list").desc("Apps"); 157 } 158 159 /** 160 * {@inheritDoc} 161 */ 162 @Override getGamesRowSelector()163 public BySelector getGamesRowSelector() { 164 return By.res(getSupportedLauncherPackage(), "list").desc("Games"); 165 } 166 167 /** 168 * {@inheritDoc} 169 */ 170 @Override getSettingsRowSelector()171 public BySelector getSettingsRowSelector() { 172 return By.res(getSupportedLauncherPackage(), "list").desc("").hasDescendant( 173 By.res(getSupportedLauncherPackage(), "icon"), 3); 174 } 175 176 /** 177 * {@inheritDoc} 178 */ 179 @Override getAppWidgetSelector()180 public BySelector getAppWidgetSelector() { 181 return By.clazz(getSupportedLauncherPackage(), "android.appwidget.AppWidgetHostView"); 182 } 183 184 /** 185 * {@inheritDoc} 186 */ 187 @Override getNowPlayingCardSelector()188 public BySelector getNowPlayingCardSelector() { 189 return By.res(getSupportedLauncherPackage(), "content_text").text("Now Playing"); 190 } 191 192 /** 193 * {@inheritDoc} 194 */ 195 @Override getAllAppsScrollDirection()196 public Direction getAllAppsScrollDirection() { 197 return Direction.RIGHT; 198 } 199 200 /** 201 * {@inheritDoc} 202 */ 203 @Override getAllAppsSelector()204 public BySelector getAllAppsSelector() { 205 // On Leanback launcher the Apps row corresponds to the All Apps on phone UI 206 return getAppsRowSelector(); 207 } 208 209 /** 210 * {@inheritDoc} 211 */ 212 @Override launch(String appName, String packageName)213 public long launch(String appName, String packageName) { 214 BySelector app = By.res(getSupportedLauncherPackage(), "app_banner").desc(appName); 215 return launchApp(this, app, packageName, isGame(packageName)); 216 } 217 218 /** 219 * {@inheritDoc} 220 */ 221 @Override setInstrumentation(Instrumentation instrumentation)222 public void setInstrumentation(Instrumentation instrumentation) { 223 mInstrumentation = instrumentation; 224 } 225 226 /** 227 * {@inheritDoc} 228 */ 229 @Override search(String query)230 public void search(String query) { 231 if (selectSearchRow() == null) { 232 throw new RuntimeException("Could not find search row."); 233 } 234 235 BySelector keyboardOrb = By.res(getSupportedLauncherPackage(), "keyboard_orb"); 236 UiObject2 orbButton = mDevice.wait(Until.findObject(keyboardOrb), SHORT_WAIT_TIME); 237 if (orbButton == null) { 238 throw new RuntimeException("Could not find keyboard orb."); 239 } 240 if (orbButton.isFocused()) { 241 mDPadUtil.pressDPadCenter(); 242 } else { 243 // Move the focus to keyboard orb by DPad button. 244 mDPadUtil.pressDPadRight(); 245 if (orbButton.isFocused()) { 246 mDPadUtil.pressDPadCenter(); 247 } 248 } 249 mDevice.wait(Until.gone(keyboardOrb), SHORT_WAIT_TIME); 250 251 BySelector searchEditor = By.res(PACKAGE_SEARCH, "search_text_editor"); 252 UiObject2 editText = mDevice.wait(Until.findObject(searchEditor), SHORT_WAIT_TIME); 253 if (editText == null) { 254 throw new RuntimeException("Could not find search text input."); 255 } 256 257 editText.setText(query); 258 SystemClock.sleep(SHORT_WAIT_TIME); 259 260 // Note that Enter key is pressed instead of DPad keys to dismiss leanback IME 261 mDPadUtil.pressEnter(); 262 mDevice.wait(Until.gone(searchEditor), SHORT_WAIT_TIME); 263 } 264 265 /** 266 * {@inheritDoc} 267 * 268 * Assume that the rows are sorted in the following order from the top: 269 * Search, Notification(, Partner), Apps, Games, Settings(, and Inputs) 270 */ 271 @Override selectNotificationRow()272 public UiObject2 selectNotificationRow() { 273 if (!isNotificationRowSelected()) { 274 open(); 275 mDPadUtil.pressHome(); // Home key to move to the first card in the Notification row 276 } 277 return mDevice.wait(Until.findObject( 278 getNotificationRowSelector().hasDescendant(By.focused(true), 3)), SHORT_WAIT_TIME); 279 } 280 281 /** 282 * {@inheritDoc} 283 */ 284 @Override selectSearchRow()285 public UiObject2 selectSearchRow() { 286 if (!isSearchRowSelected()) { 287 selectNotificationRow(); 288 mDPadUtil.pressDPadUp(); 289 } 290 return mDevice.wait(Until.findObject( 291 getSearchRowSelector().hasDescendant(By.focused(true))), SHORT_WAIT_TIME); 292 } 293 294 /** 295 * {@inheritDoc} 296 */ 297 @Override selectAppsRow()298 public UiObject2 selectAppsRow() { 299 // Start finding Apps row from Notification row 300 return findRow(getAppsRowSelector()); 301 } 302 303 /** 304 * {@inheritDoc} 305 */ 306 @Override selectGamesRow()307 public UiObject2 selectGamesRow() { 308 return findRow(getGamesRowSelector()); 309 } 310 311 /** 312 * {@inheritDoc} 313 */ 314 @Override selectSettingsRow()315 public UiObject2 selectSettingsRow() { 316 // Assume that the Settings row is at the lowest bottom 317 UiObject2 settings = findRow(getSettingsRowSelector(), Direction.DOWN); 318 if (settings != null && isSettingsRowSelected()) { 319 return settings; 320 } 321 return null; 322 } 323 324 /** 325 * {@inheritDoc} 326 */ 327 @Override hasAppWidgetSelector()328 public boolean hasAppWidgetSelector() { 329 return mDevice.wait(Until.hasObject(getAppWidgetSelector()), SHORT_WAIT_TIME); 330 } 331 332 /** 333 * {@inheritDoc} 334 */ 335 @Override hasNowPlayingCard()336 public boolean hasNowPlayingCard() { 337 return mDevice.wait(Until.hasObject(getNowPlayingCardSelector()), SHORT_WAIT_TIME); 338 } 339 340 @SuppressWarnings("unused") 341 @Override getAllAppsButtonSelector()342 public BySelector getAllAppsButtonSelector() { 343 throw new UnsupportedOperationException( 344 "The 'All Apps' button is not available on Leanback Launcher."); 345 } 346 347 @SuppressWarnings("unused") 348 @Override openAllWidgets(boolean reset)349 public UiObject2 openAllWidgets(boolean reset) { 350 throw new UnsupportedOperationException( 351 "All Widgets is not available on Leanback Launcher."); 352 } 353 354 @SuppressWarnings("unused") 355 @Override getAllWidgetsSelector()356 public BySelector getAllWidgetsSelector() { 357 throw new UnsupportedOperationException( 358 "All Widgets is not available on Leanback Launcher."); 359 } 360 361 @SuppressWarnings("unused") 362 @Override getAllWidgetsScrollDirection()363 public Direction getAllWidgetsScrollDirection() { 364 throw new UnsupportedOperationException( 365 "All Widgets is not available on Leanback Launcher."); 366 } 367 368 @SuppressWarnings("unused") 369 @Override getHotSeatSelector()370 public BySelector getHotSeatSelector() { 371 throw new UnsupportedOperationException( 372 "Hot Seat is not available on Leanback Launcher."); 373 } 374 375 /** {@inheritDoc} */ 376 @Override getOverviewSelector()377 public BySelector getOverviewSelector() { 378 throw new UnsupportedOperationException("Overview is not available on Leanback Launcher."); 379 } 380 381 @SuppressWarnings("unused") 382 @Override getWorkspaceScrollDirection()383 public Direction getWorkspaceScrollDirection() { 384 throw new UnsupportedOperationException( 385 "Workspace is not available on Leanback Launcher."); 386 } 387 launchApp(ILauncherStrategy launcherStrategy, BySelector app, String packageName, boolean isGame)388 protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app, 389 String packageName, boolean isGame) { 390 return launchApp(launcherStrategy, app, packageName, isGame, MAX_SCROLL_ATTEMPTS); 391 } 392 launchApp(ILauncherStrategy launcherStrategy, BySelector app, String packageName, boolean isGame, int maxScrollAttempts)393 protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app, 394 String packageName, boolean isGame, int maxScrollAttempts) { 395 unlockDeviceIfAsleep(); 396 397 if (isAppOpen(packageName)) { 398 // Application is already open 399 return 0; 400 } 401 402 // Go to the home page 403 launcherStrategy.open(); 404 405 // attempt to find the app/game icon if it's not already on the screen 406 UiObject2 container; 407 if (isGame) { 408 container = selectGamesRow(); 409 } else { 410 container = launcherStrategy.openAllApps(false); 411 } 412 UiObject2 appIcon = container.findObject(app); 413 int attempts = 0; 414 while (attempts++ < maxScrollAttempts) { 415 UiObject2 focused = container.wait(Until.findObject(By.focused(true)), SHORT_WAIT_TIME); 416 if (focused == null) { 417 throw new IllegalStateException( 418 "The App/Game row may have lost focus while activity is in transition"); 419 } 420 421 // Compare the focused icon and the app icon to search for. 422 UiObject2 focusedIcon = focused.findObject( 423 By.res(getSupportedLauncherPackage(), "app_banner")); 424 425 if (appIcon == null) { 426 appIcon = findApp(container, focusedIcon, app); 427 if (appIcon == null) { 428 throw new RuntimeException("Failed to find the app icon on screen: " 429 + packageName); 430 } 431 continue; 432 } else if (focusedIcon.equals(appIcon)) { 433 // The app icon is on the screen, and selected. 434 break; 435 } else { 436 // The app icon is on the screen, but not selected yet 437 // Move one step closer to the app icon 438 Point currentPosition = focusedIcon.getVisibleCenter(); 439 Point targetPosition = appIcon.getVisibleCenter(); 440 int dx = targetPosition.x - currentPosition.x; 441 int dy = targetPosition.y - currentPosition.y; 442 final int MARGIN = 10; 443 // The sequence of moving should be kept in the following order so as not to 444 // be stuck in case that the apps row are not even. 445 if (dx < -MARGIN) { 446 mDPadUtil.pressDPadLeft(); 447 continue; 448 } 449 if (dy < -MARGIN) { 450 mDPadUtil.pressDPadUp(); 451 continue; 452 } 453 if (dx > MARGIN) { 454 mDPadUtil.pressDPadRight(); 455 continue; 456 } 457 if (dy > MARGIN) { 458 mDPadUtil.pressDPadDown(); 459 continue; 460 } 461 throw new RuntimeException( 462 "Failed to navigate to the app icon on screen: " + packageName); 463 } 464 } 465 466 if (attempts == maxScrollAttempts) { 467 throw new RuntimeException( 468 "scrollBackToBeginning: exceeded max attempts: " + maxScrollAttempts); 469 } 470 471 // The app icon is already found and focused. 472 long ready = SystemClock.uptimeMillis(); 473 mDPadUtil.pressDPadCenter(); 474 if (!mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT)) { 475 Log.w(LOG_TAG, "no new window detected after app launch attempt."); 476 return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP; 477 } 478 mDevice.waitForIdle(); 479 if (packageName != null) { 480 Log.w(LOG_TAG, String.format( 481 "No UI element with package name %s detected.", packageName)); 482 boolean success = mDevice.wait(Until.hasObject( 483 By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT); 484 if (success) { 485 return ready; 486 } else { 487 return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP; 488 } 489 } else { 490 return ready; 491 } 492 } 493 494 /** 495 * Launch the named notification 496 * 497 * @param appName - the name of the application to launch in the Notification row 498 * @return true if application is verified to be in foreground after launch; false otherwise. 499 */ launchNotification(String appName)500 public boolean launchNotification(String appName) { 501 // Wait until notification content is loaded 502 long currentTimeMs = System.currentTimeMillis(); 503 while (isNotificationPreparing() && 504 (System.currentTimeMillis() - currentTimeMs > NOTIFICATION_WAIT_TIME)) { 505 Log.d(LOG_TAG, "Preparing recommendation..."); 506 SystemClock.sleep(SHORT_WAIT_TIME); 507 } 508 509 // Find a Notification that matches a given app name 510 UiObject2 card = findNotificationCard( 511 By.res(getSupportedLauncherPackage(), "card").descContains(appName)); 512 if (card == null) { 513 throw new IllegalStateException( 514 String.format("The Notification that matches %s not found", appName)); 515 } 516 Log.d(LOG_TAG, 517 String.format("The application %s found in the Notification row. [content_desc]%s", 518 appName, card.getContentDescription())); 519 520 // Click and wait until the Notification card opens 521 return mDPadUtil.pressDPadCenterAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT); 522 } 523 isSearchRowSelected()524 protected boolean isSearchRowSelected() { 525 UiObject2 row = mDevice.findObject(getSearchRowSelector()); 526 if (row == null) { 527 return false; 528 } 529 return row.hasObject(By.focused(true)); 530 } 531 isAppsRowSelected()532 protected boolean isAppsRowSelected() { 533 UiObject2 row = mDevice.findObject(getAppsRowSelector()); 534 if (row == null) { 535 return false; 536 } 537 return row.hasObject(By.focused(true)); 538 } 539 isGamesRowSelected()540 protected boolean isGamesRowSelected() { 541 UiObject2 row = mDevice.findObject(getGamesRowSelector()); 542 if (row == null) { 543 return false; 544 } 545 return row.hasObject(By.focused(true)); 546 } 547 isNotificationRowSelected()548 protected boolean isNotificationRowSelected() { 549 UiObject2 row = mDevice.findObject(getNotificationRowSelector()); 550 if (row == null) { 551 return false; 552 } 553 return row.hasObject(By.focused(true)); 554 } 555 isSettingsRowSelected()556 protected boolean isSettingsRowSelected() { 557 // Settings label is only visible if the settings row is selected 558 UiObject2 row = mDevice.findObject(getSettingsRowSelector()); 559 return (row != null && row.hasObject( 560 By.res(getSupportedLauncherPackage(), "label").text("Settings"))); 561 } 562 isAppOpen(String appPackage)563 protected boolean isAppOpen (String appPackage) { 564 return mDevice.hasObject(By.pkg(appPackage).depth(0)); 565 } 566 unlockDeviceIfAsleep()567 protected void unlockDeviceIfAsleep () { 568 // Turn screen on if necessary 569 try { 570 if (!mDevice.isScreenOn()) { 571 mDevice.wakeUp(); 572 } 573 } catch (RemoteException e) { 574 Log.e(LOG_TAG, "Failed to unlock the screen-off device.", e); 575 } 576 } 577 isNotificationPreparing()578 protected boolean isNotificationPreparing() { 579 // Ensure that the Notification row is visible on screen 580 if (!mDevice.hasObject(getNotificationRowSelector())) { 581 selectNotificationRow(); 582 } 583 return mDevice.hasObject(By.res(getSupportedLauncherPackage(), "notification_preparing")); 584 } 585 findNotificationCard(BySelector selector)586 protected UiObject2 findNotificationCard(BySelector selector) { 587 // Move to the first notification row, start searching to the right, then to the left 588 mDPadUtil.pressHome(); 589 UiObject2 card; 590 if ((card = findNotificationCard(selector, Direction.RIGHT)) != null) { 591 return card; 592 } 593 if ((card = findNotificationCard(selector, Direction.LEFT)) != null) { 594 return card; 595 } 596 return null; 597 } 598 599 /** 600 * Find the card in the Notification row that matches BySelector in a given direction. 601 * If a card is already selected, it returns regardless of the direction parameter. 602 * @param selector 603 * @param direction 604 * @return 605 */ findNotificationCard(BySelector selector, Direction direction)606 protected UiObject2 findNotificationCard(BySelector selector, Direction direction) { 607 if (direction != Direction.RIGHT && direction != Direction.LEFT) { 608 throw new IllegalArgumentException("Required to go either left or right to find a card" 609 + "in the Notification row"); 610 } 611 612 // Find the Notification row 613 UiObject2 notification = mDevice.findObject(getNotificationRowSelector()); 614 if (notification == null) { 615 mDPadUtil.pressHome(); 616 notification = mDevice.wait(Until.findObject(getNotificationRowSelector()), 617 SHORT_WAIT_TIME); 618 if (notification == null) { 619 throw new IllegalStateException("The Notification row is not found"); 620 } 621 } 622 623 // Find a focused card in the Notification row that matches a given selector 624 UiObject2 currentFocus = notification.findObject( 625 By.res(getSupportedLauncherPackage(), "card").focused(true)); 626 UiObject2 previousFocus = null; 627 while (!currentFocus.equals(previousFocus)) { 628 if (currentFocus.hasObject(selector)) { 629 return currentFocus; // Found 630 } 631 mDPadUtil.pressDPad(direction); 632 previousFocus = currentFocus; 633 currentFocus = notification.findObject( 634 By.res(getSupportedLauncherPackage(), "card").focused(true)); 635 } 636 Log.d(LOG_TAG, "Failed to find the Notification card until it reaches the end."); 637 return null; 638 } 639 findApp(UiObject2 container, UiObject2 focusedIcon, BySelector app)640 protected UiObject2 findApp(UiObject2 container, UiObject2 focusedIcon, BySelector app) { 641 UiObject2 appIcon; 642 // The app icon is not on the screen. 643 // Search by going left first until it finds the app icon on the screen 644 String prevText = focusedIcon.getContentDescription(); 645 String nextText; 646 do { 647 mDPadUtil.pressDPadLeft(); 648 appIcon = container.findObject(app); 649 if (appIcon != null) { 650 return appIcon; 651 } 652 nextText = container.findObject(By.focused(true)).findObject( 653 By.res(getSupportedLauncherPackage(), 654 "app_banner")).getContentDescription(); 655 } while (nextText != null && !nextText.equals(prevText)); 656 657 // If we haven't found it yet, search by going right 658 do { 659 mDPadUtil.pressDPadRight(); 660 appIcon = container.findObject(app); 661 if (appIcon != null) { 662 return appIcon; 663 } 664 nextText = container.findObject(By.focused(true)).findObject( 665 By.res(getSupportedLauncherPackage(), 666 "app_banner")).getContentDescription(); 667 } while (nextText != null && !nextText.equals(prevText)); 668 return null; 669 } 670 671 /** 672 * Find the focused row that matches BySelector in a given direction. 673 * If the row is already selected, it returns regardless of the direction parameter. 674 * @param row 675 * @param direction 676 * @return 677 */ findRow(BySelector row, Direction direction)678 protected UiObject2 findRow(BySelector row, Direction direction) { 679 if (direction != Direction.DOWN && direction != Direction.UP) { 680 throw new IllegalArgumentException("Required to go either up or down to find rows"); 681 } 682 683 UiObject2 currentFocused = mDevice.wait(Until.findObject(By.focused(true)), 684 SHORT_WAIT_TIME); 685 UiObject2 prevFocused = null; 686 while (!currentFocused.equals(prevFocused)) { 687 UiObject2 rowObject = mDevice.findObject(row); 688 if (rowObject != null && rowObject.hasObject(By.focused(true))) { 689 return rowObject; // Found 690 } 691 692 mDPadUtil.pressDPad(direction); 693 prevFocused = currentFocused; 694 currentFocused = mDevice.wait(Until.findObject(By.focused(true)), SHORT_WAIT_TIME); 695 } 696 Log.d(LOG_TAG, "Failed to find the row until it reaches the end."); 697 return null; 698 } 699 findRow(BySelector row)700 protected UiObject2 findRow(BySelector row) { 701 UiObject2 rowObject; 702 // Search by going down first until it finds the focused row. 703 if ((rowObject = findRow(row, Direction.DOWN)) != null) { 704 return rowObject; 705 } 706 // If we haven't found it yet, search by going up 707 if ((rowObject = findRow(row, Direction.UP)) != null) { 708 return rowObject; 709 } 710 return null; 711 } 712 selectRestrictedProfile()713 public void selectRestrictedProfile() { 714 UiObject2 button = findSettingInRow( 715 By.res(getSupportedLauncherPackage(), "label").text("Restricted Profile"), 716 Direction.RIGHT); 717 if (button == null) { 718 throw new IllegalStateException("Restricted Profile not found on launcher"); 719 } 720 mDPadUtil.pressDPadCenterAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT); 721 } 722 findSettingInRow(BySelector selector, Direction direction)723 protected UiObject2 findSettingInRow(BySelector selector, Direction direction) { 724 if (direction != Direction.RIGHT && direction != Direction.LEFT) { 725 throw new IllegalArgumentException("Either left or right is allowed"); 726 } 727 if (!isSettingsRowSelected()) { 728 selectSettingsRow(); 729 } 730 731 UiObject2 setting; 732 UiObject2 currentFocused = mDevice.findObject(By.focused(true)); 733 UiObject2 prevFocused = null; 734 while (!currentFocused.equals(prevFocused)) { 735 if ((setting = currentFocused.findObject(selector)) != null) { 736 return setting; 737 } 738 739 mDPadUtil.pressDPad(direction); 740 mDevice.waitForIdle(); 741 prevFocused = currentFocused; 742 currentFocused = mDevice.findObject(By.focused(true)); 743 } 744 Log.d(LOG_TAG, "Failed to find the setting in Settings row."); 745 return null; 746 } 747 isGame(String packageName)748 private boolean isGame(String packageName) { 749 boolean isGame = false; 750 if (mInstrumentation != null) { 751 try { 752 ApplicationInfo appInfo = 753 mInstrumentation.getTargetContext().getPackageManager().getApplicationInfo( 754 packageName, 0); 755 // TV game apps should use the "isGame" tag added since the L release. They are 756 // listed on the Games row on the Leanback Launcher. 757 isGame = ((appInfo.flags & ApplicationInfo.FLAG_IS_GAME) != 0) || 758 (appInfo.metaData != null && appInfo.metaData.getBoolean("isGame", false)); 759 Log.i(LOG_TAG, String.format("The package %s isGame: %b", packageName, isGame)); 760 } catch (PackageManager.NameNotFoundException e) { 761 Log.w(LOG_TAG, 762 String.format("No package found: %s, error:%s", packageName, e.toString())); 763 return false; 764 } 765 } 766 return isGame; 767 } 768 } 769