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