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