• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.PackageInfo;
22 import android.content.pm.PackageManager;
23 import android.content.pm.PackageManager.NameNotFoundException;
24 import android.graphics.Point;
25 import android.os.RemoteException;
26 import android.os.SystemClock;
27 import android.platform.test.utils.DPadUtil;
28 import android.support.test.uiautomator.By;
29 import android.support.test.uiautomator.BySelector;
30 import android.support.test.uiautomator.Direction;
31 import android.support.test.uiautomator.UiDevice;
32 import android.support.test.uiautomator.UiObject2;
33 import android.support.test.uiautomator.Until;
34 import android.util.Log;
35 import android.view.KeyEvent;
36 
37 import org.junit.Assert;
38 
39 import java.io.ByteArrayOutputStream;
40 import java.io.IOException;
41 
42 
43 public class TvLauncherStrategy implements ILeanbackLauncherStrategy {
44 
45     private static final String LOG_TAG = TvLauncherStrategy.class.getSimpleName();
46     private static final String PACKAGE_LAUNCHER = "com.google.android.tvlauncher";
47     private static final String PACKAGE_SETTINGS = "com.android.tv.settings";
48     private static final String CHANNEL_TITLE_WATCH_NEXT = "Watch Next";
49 
50     // Build version
51     private static final int BUILD_INT_BANDGAP = 1010100000;
52 
53     // Wait time
54     private static final int UI_APP_LAUNCH_WAIT_TIME_MS = 10000;
55     private static final int UI_WAIT_TIME_MS = 5000;
56     private static final int UI_TRANSITION_WAIT_TIME_MS = 1000;
57     private static final int NO_WAIT = 0;
58 
59     // Note that the selector specifies criteria for matching an UI element from/to a focused item
60     private static final BySelector SELECTOR_TOP_ROW = By.res(PACKAGE_LAUNCHER, "top_row");
61     private static final BySelector SELECTOR_APPS_ROW = By.res(PACKAGE_LAUNCHER, "apps_row");
62     private static final BySelector SELECTOR_ALL_APPS_VIEW =
63             By.res(PACKAGE_LAUNCHER, "row_list_view");
64     private static final BySelector SELECTOR_ALL_APPS_LOGO =
65             By.res(PACKAGE_LAUNCHER, "channel_logo").focused(true).descContains("Apps");
66     private static final BySelector SELECTOR_CONFIG_CHANNELS_ROW =
67             By.res(PACKAGE_LAUNCHER, "configure_channels_row");
68     private static final BySelector SELECTOR_CONTROLLER_MOVE = By.res(PACKAGE_LAUNCHER, "move");
69     private static final BySelector SELECTOR_CONTROLLER_REMOVE = By.res(PACKAGE_LAUNCHER, "remove");
70     private static final BySelector SELECTOR_NOTIFICATIONS_ROW = By.res(PACKAGE_LAUNCHER,
71             "notifications_row");
72 
73     protected UiDevice mDevice;
74     protected DPadUtil mDPadUtil;
75     private Instrumentation mInstrumentation;
76 
77     /** A {@link UiCondition} is a condition to be satisfied by BaseView or UI actions. */
78     public interface UiCondition {
apply(UiObject2 focus)79         boolean apply(UiObject2 focus);
80     }
81 
82     /**
83      * State of an item in Apps row or channel row on the Home Screen.
84      */
85     public enum HomeRowState {
86         /**
87          * State of a row when this or some other items in Apps row or channel row is not selected
88          */
89         DEFAULT,
90         /**
91          * State of a row when this or some other items in Apps row or channel row is selected.
92          */
93         SELECTED,
94         /**
95          * State of an item when one of the zoomed out states is focused:
96          * zoomed_out, channel_actions, move
97          */
98         ZOOMED_OUT
99     }
100 
101     /**
102      * State of an item in the HomeAppState.ZOOMED_OUT mode
103      */
104     public enum HomeControllerState {
105         /**
106          * Default state of an app. one of the program cards or non-channel rows is selected
107          */
108         DEFAULT,
109         /**
110          * One of the channel logos is selected, the channel title is zoomed out
111          */
112         CHANNEL_LOGO,
113         /**
114          * State when a channel is selected and showing channel actions (remove and move).
115          */
116         CHANNEL_ACTIONS,
117         /**
118          * State when a channel is being moved.
119          */
120         MOVE_CHANNEL
121     }
122 
123     /**
124      * A TvLauncherUnsupportedOperationException is an exception specific to TV Launcher. This will
125      * be thrown when the feature/method is not available on the TV Launcher.
126      */
127     class TvLauncherUnsupportedOperationException extends UnsupportedOperationException {
TvLauncherUnsupportedOperationException()128         TvLauncherUnsupportedOperationException() {
129             super();
130         }
TvLauncherUnsupportedOperationException(String msg)131         TvLauncherUnsupportedOperationException(String msg) {
132             super(msg);
133         }
134     }
135 
136     /**
137      * {@inheritDoc}
138      */
139     @Override
getSupportedLauncherPackage()140     public String getSupportedLauncherPackage() {
141         return PACKAGE_LAUNCHER;
142     }
143 
144     /**
145      * {@inheritDoc}
146      */
147     @Override
setUiDevice(UiDevice uiDevice)148     public void setUiDevice(UiDevice uiDevice) {
149         mDevice = uiDevice;
150         mDPadUtil = new DPadUtil(mDevice);
151     }
152 
153     /**
154      * {@inheritDoc}
155      */
156     @Override
open()157     public void open() {
158         // if we see main list view, assume at home screen already
159         if (!mDevice.hasObject(getWorkspaceSelector())) {
160             mDPadUtil.pressHome();
161             // ensure launcher is shown
162             if (!mDevice.wait(Until.hasObject(getWorkspaceSelector()), UI_WAIT_TIME_MS)) {
163                 // HACK: dump hierarchy to logcat
164                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
165                 try {
166                     mDevice.dumpWindowHierarchy(baos);
167                     baos.flush();
168                     baos.close();
169                     String[] lines = baos.toString().split("\\r?\\n");
170                     for (String line : lines) {
171                         Log.d(LOG_TAG, line.trim());
172                     }
173                 } catch (IOException ioe) {
174                     Log.e(LOG_TAG, "error dumping XML to logcat", ioe);
175                 }
176                 throw new RuntimeException("Failed to open TV launcher");
177             }
178             mDevice.waitForIdle();
179         }
180     }
181 
182     /**
183      * {@inheritDoc}
184      * There are two different ways to open All Apps view. If longpress is true, it will long press
185      * the HOME key to open it. Otherwise it will navigate to the "APPS" logo on the Apps row.
186      */
187     @Override
openAllApps(boolean longpress)188     public UiObject2 openAllApps(boolean longpress) {
189         if (!mDevice.hasObject(getAllAppsSelector())) {
190             if (longpress) {
191                 mDPadUtil.longPressKeyCode(KeyEvent.KEYCODE_HOME);
192             } else {
193                 Assert.assertNotNull("Could not find all apps logo", selectAllAppsLogo());
194                 mDPadUtil.pressDPadCenter();
195             }
196         }
197         return mDevice.wait(Until.findObject(getAllAppsSelector()), UI_WAIT_TIME_MS);
198     }
199 
openSettings()200     public boolean openSettings() {
201         Assert.assertNotNull(selectTopRow());
202         Assert.assertNotNull(selectBidirect(By.res(getSupportedLauncherPackage(), "settings"),
203                 Direction.RIGHT));
204         mDPadUtil.pressDPadCenter();
205         return mDevice.wait(
206                 Until.hasObject(By.res(PACKAGE_SETTINGS, "decor_title").text("Settings")),
207                 UI_WAIT_TIME_MS);
208     }
209 
openCustomizeChannels()210     public boolean openCustomizeChannels() {
211         Assert.assertNotNull(selectCustomizeChannelsRow());
212         Assert.assertNotNull(
213                 select(By.res(getSupportedLauncherPackage(), "button"), Direction.RIGHT,
214                         UI_WAIT_TIME_MS));
215         mDPadUtil.pressDPadCenter();
216         return mDevice.wait(
217                 Until.hasObject(By.res(PACKAGE_LAUNCHER, "decor_title").text("Customize channels")),
218                 UI_WAIT_TIME_MS);
219     }
220 
221     /**
222      * Get the launcher's version code.
223      * @return the version code. -1 if the launcher package is not found.
224      */
getVersionCode()225     public int getVersionCode() {
226         String pkg = getSupportedLauncherPackage();
227         if (null == pkg || pkg.isEmpty()) {
228             throw new RuntimeException("Can't find version of empty package");
229         }
230         if (mInstrumentation == null) {
231             Log.w(LOG_TAG, "Instrumentation is null. setInstrumentation should be called "
232                     + "to get the version code");
233             return -1;
234         }
235         PackageManager pm = mInstrumentation.getContext().getPackageManager();
236         PackageInfo pInfo = null;
237         try {
238             pInfo = pm.getPackageInfo(pkg, 0);
239             return pInfo.versionCode;
240         } catch (NameNotFoundException e) {
241             Log.w(LOG_TAG, String.format("package name is not found: %s", pkg));
242             return -1;
243         }
244     }
245 
246     /**
247      * {@inheritDoc}
248      */
249     @Override
getWorkspaceSelector()250     public BySelector getWorkspaceSelector() {
251         return By.res(getSupportedLauncherPackage(), "home_view_container");
252     }
253 
254     /**
255      * {@inheritDoc}
256      */
257     @Override
getSearchRowSelector()258     public BySelector getSearchRowSelector() {
259         return  SELECTOR_TOP_ROW;
260     }
261 
262     /**
263      * {@inheritDoc}
264      */
265     @Override
getAppsRowSelector()266     public BySelector getAppsRowSelector() {
267         return SELECTOR_APPS_ROW;
268     }
269 
270     /**
271      * {@inheritDoc}
272      */
273     @Override
getGamesRowSelector()274     public BySelector getGamesRowSelector() {
275         // Note that the apps and games are now in the same row on new TV Launcher.
276         return getAppsRowSelector();
277     }
278 
279     /**
280      * {@inheritDoc}
281      */
282     @Override
getAllAppsScrollDirection()283     public Direction getAllAppsScrollDirection() {
284         return Direction.DOWN;
285     }
286 
287     /**
288      * {@inheritDoc}
289      */
290     @Override
getAllAppsSelector()291     public BySelector getAllAppsSelector() {
292         return SELECTOR_ALL_APPS_VIEW;
293     }
294 
getAllAppsLogoSelector()295     public BySelector getAllAppsLogoSelector() {
296         return SELECTOR_ALL_APPS_LOGO;
297     }
298 
299     /**
300      * Returns a {@link BySelector} describing a given favorite app
301      */
getFavoriteAppSelector(String appName)302     public BySelector getFavoriteAppSelector(String appName) {
303         return By.res(getSupportedLauncherPackage(), "favorite_app_banner").desc(appName);
304     }
305 
306     /**
307      * Returns a {@link BySelector} describing a given app in Apps View
308      */
getAppInAppsViewSelector(String appName)309     public BySelector getAppInAppsViewSelector(String appName) {
310         if (getVersionCode() > BUILD_INT_BANDGAP) {
311             // bandgap or higher
312             return By.res(getSupportedLauncherPackage(), "banner_image").desc(appName);
313         }
314         return By.res(getSupportedLauncherPackage(), "app_title").text(appName);
315     }
316 
317     // Return a {@link BySelector} indicating a channel logo (in either zoom-in or default mode)
getChannelLogoSelector()318     public BySelector getChannelLogoSelector() {
319         return By.res(getSupportedLauncherPackage(), "channel_logo");
320     }
getChannelLogoSelector(String channelTitle)321     public BySelector getChannelLogoSelector(String channelTitle) {
322         return getChannelLogoSelector().desc(channelTitle);
323     }
324 
325     // Return the list of rows including "top_row", "apps_row", "channel"
326     // and "configure_channels_row"
getRowListSelector()327     public BySelector getRowListSelector() {
328         return By.res(getSupportedLauncherPackage(), "home_row_list");
329     }
330 
getHomeRowState()331     public HomeRowState getHomeRowState() {
332         HomeRowState state = HomeRowState.DEFAULT;
333         if (isAppsRowSelected() || isChannelRowSelected()) {
334             if (getHomeControllerState() != HomeControllerState.DEFAULT) {
335                 state = HomeRowState.ZOOMED_OUT;
336             } else {
337                 state = HomeRowState.SELECTED;
338             }
339         }
340         Log.d(LOG_TAG, String.format("[HomeRowState]%s", state));
341         return state;
342     }
343 
getHomeControllerState()344     public HomeControllerState getHomeControllerState() {
345         HomeControllerState state = HomeControllerState.DEFAULT;
346         UiObject2 focus = findFocus();
347         if (focus.hasObject(getChannelLogoSelector())) {
348             state = HomeControllerState.CHANNEL_LOGO;
349         } else if (focus.hasObject(SELECTOR_CONTROLLER_MOVE)) {
350             state = HomeControllerState.MOVE_CHANNEL;
351         } else if (focus.hasObject(SELECTOR_CONTROLLER_REMOVE)) {
352             state = HomeControllerState.CHANNEL_ACTIONS;
353         }
354         Log.d(LOG_TAG, String.format("[HomeControllerState]%s", state));
355         return state;
356     }
357 
358     // Return an index of a focused app or program in the Row. 0-based.
getFocusedItemIndexInRow()359     public int getFocusedItemIndexInRow() {
360         UiObject2 focusedChannel = mDevice.wait(Until.findObject(
361                 By.res(getSupportedLauncherPackage(), "items_list")
362                         .hasDescendant(By.focused(true))), UI_WAIT_TIME_MS);
363         if (focusedChannel == null) {
364             Log.w(LOG_TAG, "getFocusedItemIndexInRow: no channel has a focused item. "
365                     + "A focus may be at a logo or the top row.");
366             return -1;
367         }
368         int index = 0;
369         for (UiObject2 program : focusedChannel.getChildren()) {
370             if (findFocus(program, NO_WAIT) != null) {
371                 break;
372             }
373             ++index;
374         }
375         Log.d(LOG_TAG, String.format("getFocusedItemIndexInRow [index]%d", index));
376         return index;
377     }
378 
379     /**
380      * Return true if any item in Channel row is selected. eg, program, zoomed out, channel actions
381      */
isChannelRowSelected(String channelTitle)382     public boolean isChannelRowSelected(String channelTitle) {
383         return isChannelRowSelected(getChannelLogoSelector(channelTitle));
384     }
isChannelRowSelected()385     public boolean isChannelRowSelected() {
386         return isChannelRowSelected(getChannelLogoSelector());
387     }
isChannelRowSelected(final BySelector channelSelector)388     protected boolean isChannelRowSelected(final BySelector channelSelector) {
389         UiObject2 rowList = mDevice.findObject(getRowListSelector());
390         for (UiObject2 row : rowList.getChildren()) {
391             if (findFocus(row, NO_WAIT) != null) {
392                 return row.hasObject(channelSelector);
393             }
394         }
395         return false;
396     }
397 
isOnHomeScreen()398     public boolean isOnHomeScreen() {
399         if (!isAppOpen(getSupportedLauncherPackage())) {
400             Log.w(LOG_TAG, "This launcher is not in foreground");
401         }
402         return mDevice.hasObject(getWorkspaceSelector());
403     }
404 
isFirstAppSelected()405     public boolean isFirstAppSelected() {
406         if (!isAppsRowSelected()) {
407             return false;
408         }
409         return (getFocusedItemIndexInRow() == 0);
410     }
411 
412     /**
413      * {@inheritDoc}
414      */
415     @Override
launch(String appName, String packageName)416     public long launch(String appName, String packageName) {
417         Log.d(LOG_TAG, String.format("launching [name]%s [package]%s", appName, packageName));
418         return launchApp(this, appName, packageName, isGame(packageName));
419     }
420 
421     /**
422      * {@inheritDoc}
423      * <p>
424      * This function must be called before any UI test runs on TV.
425      * </p>
426      */
427     @Override
setInstrumentation(Instrumentation instrumentation)428     public void setInstrumentation(Instrumentation instrumentation) {
429         mInstrumentation = instrumentation;
430     }
431 
432     /**
433      * {@inheritDoc}
434      */
435     @Override
selectSearchRow()436     public UiObject2 selectSearchRow() {
437         // The Search orb is now on top row on TV Launcher
438         return selectTopRow();
439     }
440 
441     /**
442      * {@inheritDoc}
443      */
444     @Override
selectAppsRow()445     public UiObject2 selectAppsRow() {
446         return selectAppsRow(false);
447     }
448 
selectAppsRow(boolean useHomeKey)449     public UiObject2 selectAppsRow(boolean useHomeKey) {
450         Log.d(LOG_TAG, "selectAppsRow");
451         if (!isOnHomeScreen()) {
452             Log.w(LOG_TAG, "selectAppsRow should be called on Home screen");
453             open();
454         }
455 
456         if (useHomeKey) {
457             // Press the HOME key to move a focus to the first app in the Apps row.
458             mDPadUtil.pressHome();
459         } else {
460             selectBidirect(getAppsRowSelector().hasDescendant(By.focused(true)),
461                     Direction.DOWN);
462         }
463         return isAppsRowSelected() ? findFocus() : null;
464     }
465 
466     /**
467      * Select a channel row that matches a given name.
468      */
selectChannelRow(final String channelTitle)469     public UiObject2 selectChannelRow(final String channelTitle) {
470         Log.d(LOG_TAG, String.format("selectChannelRow [channel]%s", channelTitle));
471 
472         // Move out if any channel action button (eg, remove, move) is focused, so that
473         // it can scroll vertically to find a given row.
474         selectBidirect(
475                 new UiCondition() {
476                     @Override
477                     public boolean apply(UiObject2 focus) {
478                         HomeControllerState state = getHomeControllerState();
479                         return !(state == HomeControllerState.CHANNEL_ACTIONS
480                                 || state == HomeControllerState.MOVE_CHANNEL);
481                     }
482                 }, Direction.RIGHT);
483 
484         // Then scroll vertically to find a given row
485         UiObject2 focused = selectBidirect(
486                 new UiCondition() {
487                     @Override
488                     public boolean apply(UiObject2 focus) {
489                         return isChannelRowSelected(channelTitle);
490                     }
491                 }, Direction.DOWN);
492         return focused;
493     }
494 
495     /**
496      * Select the All Apps logo (or icon).
497      */
selectAllAppsLogo()498     public UiObject2 selectAllAppsLogo() {
499         Log.d(LOG_TAG, "selectAllAppsLogo");
500         return selectChannelLogo("Apps");
501     }
502 
selectChannelLogo(final String channelTitle)503     public UiObject2 selectChannelLogo(final String channelTitle) {
504         Log.d(LOG_TAG, String.format("selectChannelLogo [channel]%s", channelTitle));
505 
506         if (!isChannelRowSelected(channelTitle)) {
507             Assert.assertNotNull(selectChannelRow(channelTitle));
508         }
509         return selectBidirect(
510                 new UiCondition() {
511                     @Override
512                     public boolean apply(UiObject2 focus) {
513                         return getHomeControllerState() == HomeControllerState.CHANNEL_LOGO;
514                     }
515                 },
516                 Direction.LEFT);
517     }
518 
519     /**
520      * Returns a {@link UiObject2} describing the Top row on TV Launcher
521      * @return
522      */
523     public UiObject2 selectTopRow() {
524         open();
525         mDPadUtil.pressHome();
526         // Move up until it reaches the top.
527         int maxAttempts = 3;
528         while (maxAttempts-- > 0 && move(Direction.UP)) {
529             SystemClock.sleep(UI_TRANSITION_WAIT_TIME_MS);
530         }
531         return mDevice.wait(
532                 Until.findObject(getSearchRowSelector().hasDescendant(By.focused(true))),
533                 UI_TRANSITION_WAIT_TIME_MS);
534     }
535 
536     /**
537      * Returns a {@link UiObject2} describing the Notification row on TV Launcher
538      * @return
539      */
540     public UiObject2 selectNotificationRow() {
541         return selectBidirect(By.copy(SELECTOR_NOTIFICATIONS_ROW).hasDescendant(By.focused(true)),
542                 Direction.UP);
543     }
544 
545     /**
546      * Returns a {@link UiObject2} describing the customize channel row on TV Launcher
547      * @return
548      */
549     public UiObject2 selectCustomizeChannelsRow() {
550         return select(By.copy(SELECTOR_CONFIG_CHANNELS_ROW).hasDescendant(By.focused(true)),
551                 Direction.DOWN, UI_TRANSITION_WAIT_TIME_MS);
552     }
553 
554     public UiObject2 selectWatchNextRow() {
555         return selectChannelRow(CHANNEL_TITLE_WATCH_NEXT);
556     }
557 
558     /**
559      * Select the first app icon in the Apps row
560      */
561     public UiObject2 selectFirstAppIcon() {
562         if (!isFirstAppSelected()) {
563             Assert.assertNotNull("The Apps row must be selected.",
564                     selectAppsRow(/*useHomeKey*/ true));
565             mDPadUtil.pressBack();
566             if (getHomeRowState() == HomeRowState.ZOOMED_OUT) {
567                 mDPadUtil.pressDPadRight();
568             }
569         }
570         Assert.assertTrue("The first app in Apps row must be selected.", isFirstAppSelected());
571         return findFocus();
572     }
573 
574     /**
575      * {@inheritDoc}
576      */
577     @Override
578     public UiObject2 selectGamesRow() {
579         return selectAppsRow();
580     }
581 
582     /**
583      * Select the given app in All Apps activity.
584      * When the All Apps opens, the focus is always at the top right.
585      * Search from left to right, and down to the next row, from right to left, and
586      * down to the next row like a zigzag pattern until the app is found.
587      */
588     protected UiObject2 selectAppInAllApps(BySelector appSelector, String packageName) {
589         Assert.assertTrue(mDevice.hasObject(getAllAppsSelector()));
590 
591         // Assume that the focus always starts at the top left of the Apps view.
592         final int maxScrollAttempts = 20;
593         final int margin = 30;
594         int attempts = 0;
595         UiObject2 focused = null;
596         UiObject2 expected = null;
597         while (attempts++ < maxScrollAttempts) {
598             focused = mDevice.wait(Until.findObject(By.focused(true)), UI_WAIT_TIME_MS);
599             expected = mDevice.wait(Until.findObject(appSelector), UI_WAIT_TIME_MS);
600 
601             if (expected == null) {
602                 mDPadUtil.pressDPadDown();
603                 continue;
604             } else if (focused.hasObject(appSelector)) {
605                 // The app icon is selected.
606                 Log.i(LOG_TAG, String.format("The app %s is selected", packageName));
607                 break;
608             } else {
609                 // The app icon is on the screen, but not selected yet
610                 // Move one step closer to the app icon
611                 Point currentPosition = focused.getVisibleCenter();
612                 Point targetPosition = expected.getVisibleCenter();
613                 int dx = targetPosition.x - currentPosition.x;
614                 int dy = targetPosition.y - currentPosition.y;
615                 Log.d(LOG_TAG, String.format("selectAppInAllApps: [dx,dx][%d,%d]", dx, dy));
616                 if (dy > margin) {
617                     mDPadUtil.pressDPadDown();
618                     continue;
619                 }
620                 if (dx > margin) {
621                     mDPadUtil.pressDPadRight();
622                     continue;
623                 }
624                 if (dy < -margin) {
625                     mDPadUtil.pressDPadUp();
626                     continue;
627                 }
628                 if (dx < -margin) {
629                     mDPadUtil.pressDPadLeft();
630                     continue;
631                 }
632                 throw new RuntimeException(
633                         "Failed to navigate to the app icon on screen: " + packageName);
634             }
635         }
636         return expected;
637     }
638 
639     /**
640      * Select the given app in All Apps activity in zigzag manner.
641      * When the All Apps opens, the focus is always at the top left.
642      * Search from left to right, and down to the next row, from right to left, and
643      * down to the next row like a zigzag pattern until it founds a given app.
644      */
645     protected UiObject2 selectAppInAllAppsZigZag(BySelector appSelector, String packageName) {
646         Assert.assertTrue(mDevice.hasObject(getAllAppsSelector()));
647         Direction direction = Direction.RIGHT;
648         UiObject2 app = select(appSelector, direction, UI_TRANSITION_WAIT_TIME_MS);
649         while (app == null && move(Direction.DOWN)) {
650             direction = Direction.reverse(direction);
651             app = select(appSelector, direction, UI_TRANSITION_WAIT_TIME_MS);
652         }
653         if (app != null) {
654             Log.i(LOG_TAG, String.format("The app %s is selected", packageName));
655         }
656         return app;
657     }
658 
659     /**
660      * Select the given app in All Apps using the versioned BySelector for the app
661      */
662     public UiObject2 selectAppInAllApps(String appName, String packageName) {
663         UiObject2 app = null;
664         int versionCode = getVersionCode();
665         if (versionCode > BUILD_INT_BANDGAP) {
666             // bandgap or higher
667             Log.i(LOG_TAG,
668                     String.format("selectAppInAllApps: app banner has app name [versionCode]%d",
669                             versionCode));
670             app = selectAppInAllApps(getAppInAppsViewSelector(appName), packageName);
671         } else {
672             app = selectAppInAllAppsZigZag(getAppInAppsViewSelector(appName), packageName);
673         }
674         return app;
675     }
676 
677     /**
678      * Launch the given app in the Apps view.
679      */
680     public boolean launchAppInAppsView(String appName, String packageName) {
681         Log.d(LOG_TAG, String.format("launching in apps view [appName]%s [packageName]%s",
682                 appName, packageName));
683         openAllApps(true);
684         UiObject2 app = selectAppInAllApps(appName, packageName);
685         if (app == null) {
686             throw new RuntimeException(
687                 "Failed to navigate to the app icon in the Apps view: " + packageName);
688         }
689 
690         // The app icon is already found and focused. Then wait for it to open.
691         BySelector appMain = By.pkg(packageName).depth(0);
692         mDPadUtil.pressDPadCenter();
693         if (!mDevice.wait(Until.hasObject(appMain), UI_APP_LAUNCH_WAIT_TIME_MS)) {
694             Log.w(LOG_TAG, String.format(
695                     "No UI element with package name %s detected.", packageName));
696             return false;
697         }
698         return true;
699     }
700 
701     protected long launchApp(ILauncherStrategy launcherStrategy, String appName,
702             String packageName, boolean isGame) {
703         unlockDeviceIfAsleep();
704 
705         if (isAppOpen(packageName)) {
706             // Application is already open
707             return 0;
708         }
709 
710         // Go to the home page, and select the Apps row
711         launcherStrategy.open();
712         selectAppsRow();
713 
714         // Search for the app in the Favorite Apps row first.
715         // If not exists, open the 'All Apps' and search for the app there
716         UiObject2 app = null;
717         BySelector favAppSelector = getFavoriteAppSelector(appName);
718         if (mDevice.hasObject(favAppSelector)) {
719             app = selectBidirect(By.copy(favAppSelector).focused(true), Direction.RIGHT);
720         } else {
721             openAllApps(true);
722             app = selectAppInAllApps(appName, packageName);
723         }
724         if (app == null) {
725             throw new RuntimeException(
726                     "Failed to navigate to the app icon on screen: " + packageName);
727         }
728 
729         // The app icon is already found and focused. Then wait for it to open.
730         long ready = SystemClock.uptimeMillis();
731         BySelector appMain = By.pkg(packageName).depth(0);
732         mDPadUtil.pressDPadCenter();
733         if (packageName != null) {
734             if (!mDevice.wait(Until.hasObject(appMain), UI_APP_LAUNCH_WAIT_TIME_MS)) {
735                 Log.w(LOG_TAG, String.format(
736                     "No UI element with package name %s detected.", packageName));
737                 return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
738             }
739         }
740         return ready;
741     }
742 
743     protected boolean isTopRowSelected() {
744         UiObject2 row = mDevice.findObject(getSearchRowSelector());
745         if (row == null) {
746             return false;
747         }
748         return row.hasObject(By.focused(true));
749     }
750 
751     protected boolean isAppsRowSelected() {
752         UiObject2 row = mDevice.findObject(getAppsRowSelector());
753         if (row == null) {
754             return false;
755         }
756         return row.hasObject(By.focused(true));
757     }
758 
759     protected boolean isGamesRowSelected() {
760         return isAppsRowSelected();
761     }
762 
763     // TODO(hyungtaekim): Move in the common helper
764     protected boolean isAppOpen(String appPackage) {
765         return mDevice.hasObject(By.pkg(appPackage).depth(0));
766     }
767 
768     // TODO(hyungtaekim): Move in the common helper
769     protected void unlockDeviceIfAsleep() {
770         // Turn screen on if necessary
771         try {
772             if (!mDevice.isScreenOn()) {
773                 mDevice.wakeUp();
774             }
775         } catch (RemoteException e) {
776             Log.e(LOG_TAG, "Failed to unlock the screen-off device.", e);
777         }
778     }
779 
780     private boolean isGame(String packageName) {
781         boolean isGame = false;
782         if (mInstrumentation != null) {
783             try {
784                 ApplicationInfo appInfo =
785                         mInstrumentation.getTargetContext().getPackageManager().getApplicationInfo(
786                                 packageName, 0);
787                 // TV game apps should use the "isGame" tag added since the L release. They are
788                 // listed on the Games row on the TV Launcher.
789                 isGame = (appInfo.metaData != null && appInfo.metaData.getBoolean("isGame", false))
790                         || ((appInfo.flags & ApplicationInfo.FLAG_IS_GAME) != 0);
791                 Log.i(LOG_TAG, String.format("The package %s isGame: %b", packageName, isGame));
792             } catch (NameNotFoundException e) {
793                 Log.w(LOG_TAG,
794                         String.format("No package found: %s, error:%s", packageName, e.toString()));
795                 return false;
796             }
797         }
798         return isGame;
799     }
800 
801     /**
802      * {@inheritDoc}
803      */
804     @Override
805     public void search(String query) {
806         // TODO: Implement this method when the feature is available
807         throw new UnsupportedOperationException("search is not yet implemented");
808     }
809 
810     public void selectRestrictedProfile() {
811         // TODO: Implement this method when the feature is available
812         throw new UnsupportedOperationException(
813                 "The Restricted Profile is not yet available on TV Launcher.");
814     }
815 
816 
817     // Convenient methods for UI actions
818 
819     /**
820      * Select an UI element with given {@link BySelector}. This action keeps moving a focus
821      * in a given {@link Direction} until it finds a matched element.
822      * @param selector the search criteria to match an element
823      * @param direction the direction to find
824      * @param timeoutMs timeout in milliseconds to select
825      * @return a UiObject2 which represents the matched element
826      */
827     public UiObject2 select(final BySelector selector, Direction direction, long timeoutMs) {
828         return select(new UiCondition() {
829             @Override
830             public boolean apply(UiObject2 focus) {
831                 return mDevice.hasObject(selector);
832             }
833         }, direction, timeoutMs);
834     }
835 
836     public UiObject2 select(UiCondition condition, Direction direction, long timeoutMs) {
837         UiObject2 focus = findFocus(null, timeoutMs);
838         while (!condition.apply(focus)) {
839             Log.d(LOG_TAG, String.format("conditional select: moving a focus from %s to %s",
840                     focus, direction));
841             UiObject2 focused = focus;
842             mDPadUtil.pressDPad(direction);
843             focus = findFocus();
844             // Hack: A focus might be lost in some UI. Take one more step forward.
845             if (focus == null) {
846                 mDPadUtil.pressDPad(direction);
847                 focus = findFocus(null, timeoutMs);
848             }
849             // Check if it reaches to an end where it no longer moves a focus to next element
850             if (focused.equals(focus)) {
851                 Log.d(LOG_TAG, "conditional select: not found until it reaches to an end.");
852                 return null;
853             }
854         }
855         Log.i(LOG_TAG, String.format("conditional select: selected, %s", focus));
856         return focus;
857     }
858 
859     /**
860      * Select an element with a given {@link BySelector} in both given direction and reverse.
861      */
862     public UiObject2 selectBidirect(BySelector selector, Direction direction) {
863         Log.d(LOG_TAG, String.format("selectBidirect [direction]%s", direction));
864         UiObject2 object = select(selector, direction, UI_TRANSITION_WAIT_TIME_MS);
865         if (object == null) {
866             object = select(selector, Direction.reverse(direction), UI_TRANSITION_WAIT_TIME_MS);
867         }
868         return object;
869     }
870 
871     public UiObject2 selectBidirect(UiCondition condition, Direction direction) {
872         UiObject2 object = select(condition, direction, UI_WAIT_TIME_MS);
873         if (object == null) {
874             object = select(condition, Direction.reverse(direction), UI_WAIT_TIME_MS);
875         }
876         return object;
877     }
878 
879     /**
880      * Simulate a move pressing a key code.
881      * Return true if a focus is shifted on TV UI, otherwise false.
882      */
883     public boolean move(Direction direction) {
884         int keyCode = KeyEvent.KEYCODE_UNKNOWN;
885         switch (direction) {
886             case LEFT:
887                 keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
888                 break;
889             case RIGHT:
890                 keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
891                 break;
892             case UP:
893                 keyCode = KeyEvent.KEYCODE_DPAD_UP;
894                 break;
895             case DOWN:
896                 keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
897                 break;
898             default:
899                 throw new RuntimeException(String.format("This direction %s is not supported.",
900                     direction));
901         }
902         UiObject2 focus = mDevice.wait(Until.findObject(By.focused(true)),
903                 UI_TRANSITION_WAIT_TIME_MS);
904         mDPadUtil.pressKeyCodeAndWait(keyCode);
905         return !focus.equals(mDevice.wait(Until.findObject(By.focused(true)),
906                 UI_TRANSITION_WAIT_TIME_MS));
907     }
908 
909     /**
910      * Return the {@link UiObject2} that has a focused element searching through the entire view
911      * hierarchy.
912      */
913     public UiObject2 findFocus(UiObject2 fromObject, long timeoutMs) {
914         UiObject2 focused;
915         if (fromObject == null) {
916             focused = mDevice.wait(Until.findObject(By.focused(true)), timeoutMs);
917         } else {
918             focused = fromObject.wait(Until.findObject(By.focused(true)), timeoutMs);
919         }
920         return focused;
921     }
922 
923     public UiObject2 findFocus() {
924         return findFocus(null, UI_WAIT_TIME_MS);
925     }
926 
927     // Unsupported methods
928 
929     @SuppressWarnings("unused")
930     @Override
931     public BySelector getNotificationRowSelector() {
932         throw new TvLauncherUnsupportedOperationException("No Notification row");
933     }
934 
935     @SuppressWarnings("unused")
936     @Override
937     public BySelector getSettingsRowSelector() {
938         throw new TvLauncherUnsupportedOperationException("No Settings row");
939     }
940 
941     @SuppressWarnings("unused")
942     @Override
943     public BySelector getAppWidgetSelector() {
944         throw new TvLauncherUnsupportedOperationException();
945     }
946 
947     @SuppressWarnings("unused")
948     @Override
949     public BySelector getNowPlayingCardSelector() {
950         throw new TvLauncherUnsupportedOperationException("No Now Playing Card");
951     }
952 
953     @SuppressWarnings("unused")
954     @Override
955     public UiObject2 selectSettingsRow() {
956         throw new TvLauncherUnsupportedOperationException("No Settings row");
957     }
958 
959     @SuppressWarnings("unused")
960     @Override
961     public boolean hasAppWidgetSelector() {
962         throw new TvLauncherUnsupportedOperationException();
963     }
964 
965     @SuppressWarnings("unused")
966     @Override
967     public boolean hasNowPlayingCard() {
968         throw new TvLauncherUnsupportedOperationException("No Now Playing Card");
969     }
970 
971     @SuppressWarnings("unused")
972     @Override
973     public BySelector getAllAppsButtonSelector() {
974         throw new TvLauncherUnsupportedOperationException("No All Apps button");
975     }
976 
977     @SuppressWarnings("unused")
978     @Override
979     public UiObject2 openAllWidgets(boolean reset) {
980         throw new TvLauncherUnsupportedOperationException("No All Widgets");
981     }
982 
983     @SuppressWarnings("unused")
984     @Override
985     public BySelector getAllWidgetsSelector() {
986         throw new TvLauncherUnsupportedOperationException("No All Widgets");
987     }
988 
989     @SuppressWarnings("unused")
990     @Override
991     public Direction getAllWidgetsScrollDirection() {
992         throw new TvLauncherUnsupportedOperationException("No All Widgets");
993     }
994 
995     @SuppressWarnings("unused")
996     @Override
997     public BySelector getHotSeatSelector() {
998         throw new TvLauncherUnsupportedOperationException("No Hot seat");
999     }
1000 
1001     @SuppressWarnings("unused")
1002     @Override
1003     public Direction getWorkspaceScrollDirection() {
1004         throw new TvLauncherUnsupportedOperationException("No Workspace");
1005     }
1006 }
1007