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