Home | History | Annotate | Download | only in launcherhelper
      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
     57     public String getSupportedLauncherPackage() {
     58         return PACKAGE_LAUNCHER;
     59     }
     60 
     61     /**
     62      * {@inheritDoc}
     63      */
     64     @Override
     65     public void setUiDevice(UiDevice uiDevice) {
     66         mDevice = uiDevice;
     67         mDPadUtil = new DPadUtil(mDevice);
     68     }
     69 
     70     /**
     71      * {@inheritDoc}
     72      */
     73     @Override
     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
    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
    118     public BySelector getWorkspaceSelector() {
    119         return By.res(getSupportedLauncherPackage(), "main_list_view");
    120     }
    121 
    122     /**
    123      * {@inheritDoc}
    124      */
    125     @Override
    126     public BySelector getSearchRowSelector() {
    127         return By.res(getSupportedLauncherPackage(), "search_view");
    128     }
    129 
    130     /**
    131      * {@inheritDoc}
    132      */
    133     @Override
    134     public BySelector getNotificationRowSelector() {
    135         return By.res(getSupportedLauncherPackage(), "notification_view");
    136     }
    137 
    138     /**
    139      * {@inheritDoc}
    140      */
    141     @Override
    142     public BySelector getAppsRowSelector() {
    143         return By.res(getSupportedLauncherPackage(), "list").desc("Apps");
    144     }
    145 
    146     /**
    147      * {@inheritDoc}
    148      */
    149     @Override
    150     public BySelector getGamesRowSelector() {
    151         return By.res(getSupportedLauncherPackage(), "list").desc("Games");
    152     }
    153 
    154     /**
    155      * {@inheritDoc}
    156      */
    157     @Override
    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
    167     public BySelector getAppWidgetSelector() {
    168         return By.clazz(getSupportedLauncherPackage(), "android.appwidget.AppWidgetHostView");
    169     }
    170 
    171     /**
    172      * {@inheritDoc}
    173      */
    174     @Override
    175     public BySelector getNowPlayingCardSelector() {
    176         return By.res(getSupportedLauncherPackage(), "content_text").text("Now Playing");
    177     }
    178 
    179     /**
    180      * {@inheritDoc}
    181      */
    182     @Override
    183     public Direction getAllAppsScrollDirection() {
    184         return Direction.RIGHT;
    185     }
    186 
    187     /**
    188      * {@inheritDoc}
    189      */
    190     @Override
    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
    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
    209     public void setInstrumentation(Instrumentation instrumentation) {
    210         mInstrumentation = instrumentation;
    211     }
    212 
    213     /**
    214      * {@inheritDoc}
    215      */
    216     @Override
    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
    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
    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
    285     public UiObject2 selectAppsRow() {
    286         // Start finding Apps row from Notification row
    287         return findRow(getAppsRowSelector());
    288     }
    289 
    290     /**
    291      * {@inheritDoc}
    292      */
    293     @Override
    294     public UiObject2 selectGamesRow() {
    295         return findRow(getGamesRowSelector());
    296     }
    297 
    298     /**
    299      * {@inheritDoc}
    300      */
    301     @Override
    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
    315     public boolean hasAppWidgetSelector() {
    316         return mDevice.wait(Until.hasObject(getAppWidgetSelector()), SHORT_WAIT_TIME);
    317     }
    318 
    319     /**
    320      * {@inheritDoc}
    321      */
    322     @Override
    323     public boolean hasNowPlayingCard() {
    324         return mDevice.wait(Until.hasObject(getNowPlayingCardSelector()), SHORT_WAIT_TIME);
    325     }
    326 
    327     @SuppressWarnings("unused")
    328     @Override
    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
    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
    343     public BySelector getAllWidgetsSelector() {
    344         throw new UnsupportedOperationException(
    345                 "All Widgets is not available on Leanback Launcher.");
    346     }
    347 
    348     @SuppressWarnings("unused")
    349     @Override
    350     public Direction getAllWidgetsScrollDirection() {
    351         throw new UnsupportedOperationException(
    352                 "All Widgets is not available on Leanback Launcher.");
    353     }
    354 
    355     @SuppressWarnings("unused")
    356     @Override
    357     public BySelector getHotSeatSelector() {
    358         throw new UnsupportedOperationException(
    359                 "Hot Seat is not available on Leanback Launcher.");
    360     }
    361 
    362     @SuppressWarnings("unused")
    363     @Override
    364     public Direction getWorkspaceScrollDirection() {
    365         throw new UnsupportedOperationException(
    366                 "Workspace is not available on Leanback Launcher.");
    367     }
    368 
    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 
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    544     protected boolean isAppOpen (String appPackage) {
    545         return mDevice.hasObject(By.pkg(appPackage).depth(0));
    546     }
    547 
    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 
    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 
    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      */
    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 
    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      */
    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 
    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 
    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 
    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 
    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