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