• 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.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 import android.view.KeyEvent;
34 
35 import org.junit.Assert;
36 
37 import java.io.ByteArrayOutputStream;
38 import java.io.IOException;
39 
40 
41 public class TvLauncherStrategy implements ILeanbackLauncherStrategy {
42 
43     private static final String LOG_TAG = TvLauncherStrategy.class.getSimpleName();
44     private static final String PACKAGE_LAUNCHER = "com.google.android.tvlauncher";
45 
46     private static final int APP_LAUNCH_TIMEOUT = 10000;
47     private static final int SHORT_WAIT_TIME = 5000;    // 5 sec
48     private static final int UI_TRANSITION_WAIT_TIME = 1000;
49 
50     // Note that the selector specifies criteria for matching an UI element from/to a focused item
51     private static final BySelector SELECTOR_TOP_ROW = By.res(PACKAGE_LAUNCHER, "top_row");
52     private static final BySelector SELECTOR_APPS_ROW = By.res(PACKAGE_LAUNCHER, "apps_row");
53     private static final BySelector SELECTOR_ALL_APPS_VIEW =
54             By.res(PACKAGE_LAUNCHER, "row_list_view");
55     private static final BySelector SELECTOR_ALL_APPS_LOGO =
56             By.res(PACKAGE_LAUNCHER, "channel_logo").focused(true).descContains("Apps");
57     private static final BySelector SELECTOR_CONFIG_CHANNELS_ROW =
58             By.res(PACKAGE_LAUNCHER, "configure_channels_row");
59 
60     protected UiDevice mDevice;
61     protected DPadUtil mDPadUtil;
62     private Instrumentation mInstrumentation;
63 
64     /**
65      * A TvLauncherUnsupportedOperationException is an exception specific to TV Launcher. This will
66      * be thrown when the feature/method is not available on the TV Launcher.
67      */
68     class TvLauncherUnsupportedOperationException extends UnsupportedOperationException {
TvLauncherUnsupportedOperationException()69         TvLauncherUnsupportedOperationException() {
70             super();
71         }
TvLauncherUnsupportedOperationException(String msg)72         TvLauncherUnsupportedOperationException(String msg) {
73             super(msg);
74         }
75     }
76 
77     /**
78      * {@inheritDoc}
79      */
80     @Override
getSupportedLauncherPackage()81     public String getSupportedLauncherPackage() {
82         return PACKAGE_LAUNCHER;
83     }
84 
85     /**
86      * {@inheritDoc}
87      */
88     // TODO(hyungtaekim): Move this common implementation to abstract class for TV launchers
89     @Override
setUiDevice(UiDevice uiDevice)90     public void setUiDevice(UiDevice uiDevice) {
91         mDevice = uiDevice;
92         mDPadUtil = new DPadUtil(mDevice);
93     }
94 
95     /**
96      * {@inheritDoc}
97      */
98     @Override
open()99     public void open() {
100         // if we see main list view, assume at home screen already
101         if (!mDevice.hasObject(getWorkspaceSelector())) {
102             mDPadUtil.pressHome();
103             // ensure launcher is shown
104             if (!mDevice.wait(Until.hasObject(getWorkspaceSelector()), SHORT_WAIT_TIME)) {
105                 // HACK: dump hierarchy to logcat
106                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
107                 try {
108                     mDevice.dumpWindowHierarchy(baos);
109                     baos.flush();
110                     baos.close();
111                     String[] lines = baos.toString().split("\\r?\\n");
112                     for (String line : lines) {
113                         Log.d(LOG_TAG, line.trim());
114                     }
115                 } catch (IOException ioe) {
116                     Log.e(LOG_TAG, "error dumping XML to logcat", ioe);
117                 }
118                 throw new RuntimeException("Failed to open TV launcher");
119             }
120             mDevice.waitForIdle();
121         }
122     }
123 
124     /**
125      * {@inheritDoc}
126      * There are two different ways to open All Apps view. If longpress is true, it will long press
127      * the HOME key to open it. Otherwise it will navigate to the "APPS" logo on the Apps row.
128      */
129     @Override
openAllApps(boolean longpress)130     public UiObject2 openAllApps(boolean longpress) {
131         if (longpress) {
132             mDPadUtil.longPressKeyCode(KeyEvent.KEYCODE_HOME);
133         } else {
134             Assert.assertNotNull("Could not find all apps logo", selectAppsLogo());
135             mDPadUtil.pressDPadCenter();
136         }
137         return mDevice.wait(Until.findObject(getAllAppsSelector()), SHORT_WAIT_TIME);
138     }
139 
140     /**
141      * {@inheritDoc}
142      */
143     @Override
getWorkspaceSelector()144     public BySelector getWorkspaceSelector() {
145         return By.res(getSupportedLauncherPackage(), "home_view_container");
146     }
147 
148     /**
149      * {@inheritDoc}
150      */
151     @Override
getSearchRowSelector()152     public BySelector getSearchRowSelector() {
153         return  SELECTOR_TOP_ROW;
154     }
155 
156     /**
157      * {@inheritDoc}
158      */
159     @Override
getAppsRowSelector()160     public BySelector getAppsRowSelector() {
161         return SELECTOR_APPS_ROW;
162     }
163 
164     /**
165      * {@inheritDoc}
166      */
167     @Override
getGamesRowSelector()168     public BySelector getGamesRowSelector() {
169         // Note that the apps and games are now in the same row on new TV Launcher.
170         return getAppsRowSelector();
171     }
172 
173     /**
174      * {@inheritDoc}
175      */
176     @Override
getAllAppsScrollDirection()177     public Direction getAllAppsScrollDirection() {
178         return Direction.DOWN;
179     }
180 
181     /**
182      * {@inheritDoc}
183      */
184     @Override
getAllAppsSelector()185     public BySelector getAllAppsSelector() {
186         return SELECTOR_ALL_APPS_VIEW;
187     }
188 
getAllAppsLogoSelector()189     public BySelector getAllAppsLogoSelector() {
190         return SELECTOR_ALL_APPS_LOGO;
191     }
192 
193     /**
194      * Returns a {@link BySelector} describing a given favorite app
195      */
getFavoriteAppSelector(String appName)196     public BySelector getFavoriteAppSelector(String appName) {
197         return By.res(getSupportedLauncherPackage(), "favorite_app_banner").text(appName);
198     }
199 
200     /**
201      * Returns a {@link BySelector} describing a given app in Apps View
202      */
getAppInAppsViewSelector(String appName)203     public BySelector getAppInAppsViewSelector(String appName) {
204         return By.res(getSupportedLauncherPackage(), "app_title").text(appName);
205     }
206 
207     /**
208      * {@inheritDoc}
209      */
210     @Override
launch(String appName, String packageName)211     public long launch(String appName, String packageName) {
212         return launchApp(this, appName, packageName, isGame(packageName));
213     }
214 
215     /**
216      * {@inheritDoc}
217      * <p>
218      * This function must be called before any UI test runs on TV.
219      * </p>
220      */
221     @Override
setInstrumentation(Instrumentation instrumentation)222     public void setInstrumentation(Instrumentation instrumentation) {
223         mInstrumentation = instrumentation;
224     }
225 
226     /**
227      * {@inheritDoc}
228      */
229     @Override
selectSearchRow()230     public UiObject2 selectSearchRow() {
231         // The Search orb is now on top row on TV Launcher
232         return selectTopRow();
233     }
234 
235     /**
236      * {@inheritDoc}
237      */
238     @Override
selectAppsRow()239     public UiObject2 selectAppsRow() {
240         return selectBidirect(getAppsRowSelector().hasDescendant(By.focused(true)),
241                 Direction.DOWN);
242     }
243 
selectChannelsRow(String channelName)244     public UiObject2 selectChannelsRow(String channelName) {
245         // TODO:
246         return null;
247     }
248 
selectAppsLogo()249     public UiObject2 selectAppsLogo() {
250         Assert.assertNotNull("Could not find all apps row", selectAppsRow());
251         return selectBidirect(getAllAppsLogoSelector().hasDescendant(By.focused(true)),
252                 Direction.LEFT);
253     }
254 
255     /**
256      * Returns a {@link UiObject2} describing the Top row on TV Launcher
257      * @return
258      */
selectTopRow()259     public UiObject2 selectTopRow() {
260         return select(getSearchRowSelector().hasDescendant(By.focused(true)),
261                 Direction.UP, UI_TRANSITION_WAIT_TIME);
262     }
263 
264     /**
265      * Returns a {@link UiObject2} describing the Config Channels row on TV Launcher
266      * @return
267      */
selectConfigChannelsRow()268     public UiObject2 selectConfigChannelsRow() {
269         return select(SELECTOR_CONFIG_CHANNELS_ROW.hasDescendant(By.focused(true)),
270                 Direction.DOWN, UI_TRANSITION_WAIT_TIME);
271     }
272 
273     /**
274      * {@inheritDoc}
275      */
276     @Override
selectGamesRow()277     public UiObject2 selectGamesRow() {
278         return selectAppsRow();
279     }
280 
281     /**
282      * Select the given app in All Apps activity.
283      * When the All Apps opens, the focus is always at the top right.
284      * Search from left to right, and down to the next row, from right to left, and
285      * down to the next row like a zigzag pattern until the app is found.
286      */
selectAppInAllApps(BySelector appSelector, String packageName)287     protected UiObject2 selectAppInAllApps(BySelector appSelector, String packageName) {
288         openAllApps(true);
289 
290         // Assume that the focus always starts at the top left of the Apps view.
291         final int maxScrollAttempts = 20;
292         final int margin = 10;
293         int attempts = 0;
294         UiObject2 focused = null;
295         UiObject2 expected = null;
296         while (attempts++ < maxScrollAttempts) {
297             focused = mDevice.wait(Until.findObject(By.focused(true)), SHORT_WAIT_TIME);
298             expected = mDevice.wait(Until.findObject(appSelector), SHORT_WAIT_TIME);
299 
300             if (expected == null) {
301                 mDPadUtil.pressDPadDown();
302                 continue;
303             } else if (focused.getVisibleCenter().equals(expected.getVisibleCenter())) {
304                 // The app icon is on the screen, and selected.
305                 Log.i(LOG_TAG, String.format("The app %s is selected", packageName));
306                 break;
307             } else {
308                 // The app icon is on the screen, but not selected yet
309                 // Move one step closer to the app icon
310                 Point currentPosition = focused.getVisibleCenter();
311                 Point targetPosition = expected.getVisibleCenter();
312                 int dx = targetPosition.x - currentPosition.x;
313                 int dy = targetPosition.y - currentPosition.y;
314                 if (dy > margin) {
315                     mDPadUtil.pressDPadDown();
316                     continue;
317                 }
318                 if (dx > margin) {
319                     mDPadUtil.pressDPadRight();
320                     continue;
321                 }
322                 if (dy < -margin) {
323                     mDPadUtil.pressDPadUp();
324                     continue;
325                 }
326                 if (dx < -margin) {
327                     mDPadUtil.pressDPadLeft();
328                     continue;
329                 }
330                 throw new RuntimeException(
331                         "Failed to navigate to the app icon on screen: " + packageName);
332             }
333         }
334         return expected;
335     }
336 
337     /**
338      * Select the given app in All Apps activity in zigzag manner.
339      * When the All Apps opens, the focus is always at the top left.
340      * Search from left to right, and down to the next row, from right to left, and
341      * down to the next row like a zigzag pattern until it founds a given app.
342      */
selectAppInAllAppsZigZag(BySelector appSelector, String packageName)343     public UiObject2 selectAppInAllAppsZigZag(BySelector appSelector, String packageName) {
344         Direction direction = Direction.RIGHT;
345         UiObject2 app = select(appSelector, direction, UI_TRANSITION_WAIT_TIME);
346         while (app == null && move(Direction.DOWN)) {
347             direction = Direction.reverse(direction);
348             app = select(appSelector, direction, UI_TRANSITION_WAIT_TIME);
349         }
350         if (app != null) {
351             Log.i(LOG_TAG, String.format("The app %s is selected", packageName));
352         }
353         return app;
354     }
355 
launchApp(ILauncherStrategy launcherStrategy, String appName, String packageName, boolean isGame)356     protected long launchApp(ILauncherStrategy launcherStrategy, String appName,
357             String packageName, boolean isGame) {
358         unlockDeviceIfAsleep();
359 
360         if (isAppOpen(packageName)) {
361             // Application is already open
362             return 0;
363         }
364 
365         // Go to the home page, and select the Apps row
366         launcherStrategy.open();
367         selectAppsRow();
368 
369         // Search for the app in the Favorite Apps row first.
370         // If not exists, open the 'All Apps' and search for the app there
371         UiObject2 app = null;
372         BySelector favAppSelector = getFavoriteAppSelector(appName);
373         if (mDevice.hasObject(favAppSelector)) {
374             app = selectBidirect(By.focused(true).hasDescendant(favAppSelector), Direction.RIGHT);
375         } else {
376             openAllApps(true);
377             // Find app in Apps View in zigzag mode with app selector for Apps View
378             // because the app title no longer appears until focused.
379             app = selectAppInAllAppsZigZag(getAppInAppsViewSelector(appName), packageName);
380         }
381         if (app == null) {
382             throw new RuntimeException(
383                     "Failed to navigate to the app icon on screen: " + packageName);
384         }
385 
386         // The app icon is already found and focused. Then wait for it to open.
387         long ready = SystemClock.uptimeMillis();
388         mDPadUtil.pressDPadCenter();
389         if (packageName != null) {
390             if (!mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT)) {
391                 Log.w(LOG_TAG, String.format(
392                     "No UI element with package name %s detected.", packageName));
393                 return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
394             }
395         }
396         return ready;
397     }
398 
isTopRowSelected()399     protected boolean isTopRowSelected() {
400         UiObject2 row = mDevice.findObject(getSearchRowSelector());
401         if (row == null) {
402             return false;
403         }
404         return row.hasObject(By.focused(true));
405     }
406 
isAppsRowSelected()407     protected boolean isAppsRowSelected() {
408         UiObject2 row = mDevice.findObject(getAppsRowSelector());
409         if (row == null) {
410             return false;
411         }
412         return row.hasObject(By.focused(true));
413     }
414 
isGamesRowSelected()415     protected boolean isGamesRowSelected() {
416         return isAppsRowSelected();
417     }
418 
419     // TODO(hyungtaekim): Move in the common helper
isAppOpen(String appPackage)420     protected boolean isAppOpen(String appPackage) {
421         return mDevice.hasObject(By.pkg(appPackage).depth(0));
422     }
423 
424     // TODO(hyungtaekim): Move in the common helper
unlockDeviceIfAsleep()425     protected void unlockDeviceIfAsleep() {
426         // Turn screen on if necessary
427         try {
428             if (!mDevice.isScreenOn()) {
429                 mDevice.wakeUp();
430             }
431         } catch (RemoteException e) {
432             Log.e(LOG_TAG, "Failed to unlock the screen-off device.", e);
433         }
434     }
435 
isGame(String packageName)436     private boolean isGame(String packageName) {
437         boolean isGame = false;
438         if (mInstrumentation != null) {
439             try {
440                 ApplicationInfo appInfo =
441                         mInstrumentation.getTargetContext().getPackageManager().getApplicationInfo(
442                                 packageName, 0);
443                 // TV game apps should use the "isGame" tag added since the L release. They are
444                 // listed on the Games row on the TV Launcher.
445                 isGame = (appInfo.metaData != null && appInfo.metaData.getBoolean("isGame", false))
446                         || ((appInfo.flags & ApplicationInfo.FLAG_IS_GAME) != 0);
447                 Log.i(LOG_TAG, String.format("The package %s isGame: %b", packageName, isGame));
448             } catch (PackageManager.NameNotFoundException e) {
449                 Log.w(LOG_TAG,
450                         String.format("No package found: %s, error:%s", packageName, e.toString()));
451                 return false;
452             }
453         }
454         return isGame;
455     }
456 
457     /**
458      * {@inheritDoc}
459      */
460     @Override
search(String query)461     public void search(String query) {
462         // TODO: Implement this method when the feature is available
463         throw new UnsupportedOperationException("search is not yet implemented");
464     }
465 
selectRestrictedProfile()466     public void selectRestrictedProfile() {
467         // TODO: Implement this method when the feature is available
468         throw new UnsupportedOperationException(
469                 "The Restricted Profile is not yet available on TV Launcher.");
470     }
471 
472 
473     // Convenient methods for UI actions
474 
475     /**
476      * Select an UI element with given {@link BySelector}. This action keeps moving a focus
477      * in a given {@link Direction} until it finds a matched element.
478      * @param selector the search criteria to match an element
479      * @param direction the direction to find
480      * @param timeoutMs timeout in milliseconds to select
481      * @return a UiObject2 which represents the matched element
482      */
select(BySelector selector, Direction direction, long timeoutMs)483     public UiObject2 select(BySelector selector, Direction direction, long timeoutMs) {
484         UiObject2 focus = mDevice.wait(Until.findObject(By.focused(true)), SHORT_WAIT_TIME);
485         while (!mDevice.wait(Until.hasObject(selector), timeoutMs)) {
486             Log.d(LOG_TAG, String.format("select: moving a focus from %s to %s", focus, direction));
487             UiObject2 focused = focus;
488             mDPadUtil.pressDPad(direction);
489             focus = mDevice.wait(Until.findObject(By.focused(true)), SHORT_WAIT_TIME);
490             // Hack: A focus might be lost in some UI. Take one more step forward.
491             if (focus == null) {
492                 mDPadUtil.pressDPad(direction);
493                 focus = mDevice.wait(Until.findObject(By.focused(true)), SHORT_WAIT_TIME);
494             }
495             // Check if it reaches to an end where it no longer moves a focus to next element
496             if (focused.equals(focus)) {
497                 Log.d(LOG_TAG, "select: not found until it reaches to an end.");
498                 return null;
499             }
500         }
501         Log.i(LOG_TAG, String.format("select: %s is selected", focus));
502         return focus;
503     }
504 
505     /**
506      * Select an element with a given {@link BySelector} in both given direction and reverse.
507      */
selectBidirect(BySelector selector, Direction direction)508     public UiObject2 selectBidirect(BySelector selector, Direction direction) {
509         Log.d(LOG_TAG, String.format("selectBidirect [direction]%s", direction));
510         UiObject2 object = select(selector, direction, UI_TRANSITION_WAIT_TIME);
511         if (object == null) {
512             object = select(selector, Direction.reverse(direction), UI_TRANSITION_WAIT_TIME);
513         }
514         return object;
515     }
516 
517     /**
518      * Simulate a move pressing a key code.
519      * Return true if a focus is shifted on TV UI, otherwise false.
520      */
move(Direction direction)521     public boolean move(Direction direction) {
522         int keyCode = KeyEvent.KEYCODE_UNKNOWN;
523         switch (direction) {
524             case LEFT:
525                 keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
526                 break;
527             case RIGHT:
528                 keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
529                 break;
530             case UP:
531                 keyCode = KeyEvent.KEYCODE_DPAD_UP;
532                 break;
533             case DOWN:
534                 keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
535                 break;
536             default:
537                 throw new RuntimeException(String.format("This direction %s is not supported.",
538                     direction));
539         }
540         UiObject2 focus = mDevice.wait(Until.findObject(By.focused(true)),
541                 UI_TRANSITION_WAIT_TIME);
542         mDPadUtil.pressKeyCodeAndWait(keyCode);
543         return !focus.equals(mDevice.wait(Until.findObject(By.focused(true)),
544                 UI_TRANSITION_WAIT_TIME));
545     }
546 
547 
548     // Unsupported methods
549 
550     @SuppressWarnings("unused")
551     @Override
getNotificationRowSelector()552     public BySelector getNotificationRowSelector() {
553         throw new TvLauncherUnsupportedOperationException("No Notification row");
554     }
555 
556     @SuppressWarnings("unused")
557     @Override
getSettingsRowSelector()558     public BySelector getSettingsRowSelector() {
559         throw new TvLauncherUnsupportedOperationException("No Settings row");
560     }
561 
562     @SuppressWarnings("unused")
563     @Override
getAppWidgetSelector()564     public BySelector getAppWidgetSelector() {
565         throw new TvLauncherUnsupportedOperationException();
566     }
567 
568     @SuppressWarnings("unused")
569     @Override
getNowPlayingCardSelector()570     public BySelector getNowPlayingCardSelector() {
571         throw new TvLauncherUnsupportedOperationException("No Now Playing Card");
572     }
573 
574     @SuppressWarnings("unused")
575     @Override
selectNotificationRow()576     public UiObject2 selectNotificationRow() {
577         throw new TvLauncherUnsupportedOperationException("No Notification row");
578     }
579 
580     @SuppressWarnings("unused")
581     @Override
selectSettingsRow()582     public UiObject2 selectSettingsRow() {
583         throw new TvLauncherUnsupportedOperationException("No Settings row");
584     }
585 
586     @SuppressWarnings("unused")
587     @Override
hasAppWidgetSelector()588     public boolean hasAppWidgetSelector() {
589         throw new TvLauncherUnsupportedOperationException();
590     }
591 
592     @SuppressWarnings("unused")
593     @Override
hasNowPlayingCard()594     public boolean hasNowPlayingCard() {
595         throw new TvLauncherUnsupportedOperationException("No Now Playing Card");
596     }
597 
598     @SuppressWarnings("unused")
599     @Override
getAllAppsButtonSelector()600     public BySelector getAllAppsButtonSelector() {
601         throw new TvLauncherUnsupportedOperationException("No All Apps button");
602     }
603 
604     @SuppressWarnings("unused")
605     @Override
openAllWidgets(boolean reset)606     public UiObject2 openAllWidgets(boolean reset) {
607         throw new TvLauncherUnsupportedOperationException("No All Widgets");
608     }
609 
610     @SuppressWarnings("unused")
611     @Override
getAllWidgetsSelector()612     public BySelector getAllWidgetsSelector() {
613         throw new TvLauncherUnsupportedOperationException("No All Widgets");
614     }
615 
616     @SuppressWarnings("unused")
617     @Override
getAllWidgetsScrollDirection()618     public Direction getAllWidgetsScrollDirection() {
619         throw new TvLauncherUnsupportedOperationException("No All Widgets");
620     }
621 
622     @SuppressWarnings("unused")
623     @Override
getHotSeatSelector()624     public BySelector getHotSeatSelector() {
625         throw new TvLauncherUnsupportedOperationException("No Hot seat");
626     }
627 
628     @SuppressWarnings("unused")
629     @Override
getWorkspaceScrollDirection()630     public Direction getWorkspaceScrollDirection() {
631         throw new TvLauncherUnsupportedOperationException("No Workspace");
632     }
633 }
634