• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package android.support.test.launcherhelper;
18 
19 import android.graphics.Point;
20 import android.os.RemoteException;
21 import android.os.SystemClock;
22 import android.support.test.uiautomator.*;
23 import android.util.Log;
24 
25 import java.io.ByteArrayOutputStream;
26 import java.io.IOException;
27 
28 public class LeanbackLauncherStrategy implements ILeanbackLauncherStrategy {
29 
30     private static final String LOG_TAG = LeanbackLauncherStrategy.class.getSimpleName();
31     private static final String PACKAGE_LAUNCHER = "com.google.android.leanbacklauncher";
32     private static final String PACKAGE_SEARCH = "com.google.android.katniss";
33 
34     private static final int MAX_SCROLL_ATTEMPTS = 20;
35     private static final int APP_LAUNCH_TIMEOUT = 10000;
36     private static final int SHORT_WAIT_TIME = 5000;    // 5 sec
37 
38     protected UiDevice mDevice;
39 
40 
41     /**
42      * {@inheritDoc}
43      */
44     @Override
getSupportedLauncherPackage()45     public String getSupportedLauncherPackage() {
46         return PACKAGE_LAUNCHER;
47     }
48 
49     /**
50      * {@inheritDoc}
51      */
52     @Override
setUiDevice(UiDevice uiDevice)53     public void setUiDevice(UiDevice uiDevice) {
54         mDevice = uiDevice;
55     }
56 
57     /**
58      * {@inheritDoc}
59      */
60     @Override
open()61     public void open() {
62         // if we see main list view, assume at home screen already
63         if (!mDevice.hasObject(getWorkspaceSelector())) {
64             mDevice.pressHome();
65             // ensure launcher is shown
66             if (!mDevice.wait(Until.hasObject(getWorkspaceSelector()), SHORT_WAIT_TIME)) {
67                 // HACK: dump hierarchy to logcat
68                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
69                 try {
70                     mDevice.dumpWindowHierarchy(baos);
71                     baos.flush();
72                     baos.close();
73                     String[] lines = baos.toString().split("\\r?\\n");
74                     for (String line : lines) {
75                         Log.d(LOG_TAG, line.trim());
76                     }
77                 } catch (IOException ioe) {
78                     Log.e(LOG_TAG, "error dumping XML to logcat", ioe);
79                 }
80                 throw new RuntimeException("Failed to open leanback launcher");
81             }
82             mDevice.waitForIdle();
83         }
84     }
85 
86     /**
87      * {@inheritDoc}
88      */
89     @Override
openAllApps(boolean reset)90     public UiObject2 openAllApps(boolean reset) {
91         UiObject2 appsRow = selectAppsRow();
92         if (appsRow == null) {
93             throw new RuntimeException("Could not find all apps row");
94         }
95         if (reset) {
96             Log.w(LOG_TAG, "The reset will be ignored on leanback launcher");
97         }
98         return appsRow;
99     }
100 
101     /**
102      * {@inheritDoc}
103      */
104     @Override
getWorkspaceSelector()105     public BySelector getWorkspaceSelector() {
106         return By.res(getSupportedLauncherPackage(), "main_list_view");
107     }
108 
109     /**
110      * {@inheritDoc}
111      */
112     @Override
getSearchRowSelector()113     public BySelector getSearchRowSelector() {
114         return By.res(getSupportedLauncherPackage(), "search_view");
115     }
116 
117     /**
118      * {@inheritDoc}
119      */
120     @Override
getNotificationRowSelector()121     public BySelector getNotificationRowSelector() {
122         return By.res(getSupportedLauncherPackage(), "notification_view");
123     }
124 
125     /**
126      * {@inheritDoc}
127      */
128     @Override
getAppsRowSelector()129     public BySelector getAppsRowSelector() {
130         return By.res(getSupportedLauncherPackage(), "list").desc("Apps");
131     }
132 
133     /**
134      * {@inheritDoc}
135      */
136     @Override
getGamesRowSelector()137     public BySelector getGamesRowSelector() {
138         return By.res(getSupportedLauncherPackage(), "list").desc("Games");
139     }
140 
141     /**
142      * {@inheritDoc}
143      */
144     @Override
getSettingsRowSelector()145     public BySelector getSettingsRowSelector() {
146         return By.res(getSupportedLauncherPackage(), "list").desc("")
147                 .hasDescendant(By.res("icon"));
148     }
149 
150     /**
151      * {@inheritDoc}
152      */
153     @Override
getAllAppsScrollDirection()154     public Direction getAllAppsScrollDirection() {
155         return Direction.RIGHT;
156     }
157 
158     /**
159      * {@inheritDoc}
160      */
161     @Override
getAllAppsSelector()162     public BySelector getAllAppsSelector() {
163         // On Leanback launcher the Apps row corresponds to the All Apps on phone UI
164         return getAppsRowSelector();
165     }
166 
167     /**
168      * {@inheritDoc}
169      */
170     @Override
launch(String appName, String packageName)171     public long launch(String appName, String packageName) {
172         BySelector app = By.res(getSupportedLauncherPackage(), "app_banner").desc(appName);
173         return launchApp(this, app, packageName);
174     }
175 
176     /**
177      * {@inheritDoc}
178      */
179     @Override
search(String query)180     public void search(String query) {
181         if (selectSearchRow() == null) {
182             throw new RuntimeException("Could not find search row.");
183         }
184 
185         BySelector keyboardOrb = By.res(getSupportedLauncherPackage(), "keyboard_orb");
186         UiObject2 orbButton = mDevice.wait(Until.findObject(keyboardOrb), SHORT_WAIT_TIME);
187         if (orbButton == null) {
188             throw new RuntimeException("Could not find keyboard orb.");
189         }
190         if (orbButton.isFocused()) {
191             mDevice.pressDPadCenter();
192         } else {
193             // Move the focus to keyboard orb by DPad button.
194             mDevice.pressDPadRight();
195             if (orbButton.isFocused()) {
196                 mDevice.pressDPadCenter();
197             }
198         }
199         mDevice.wait(Until.gone(keyboardOrb), SHORT_WAIT_TIME);
200 
201         BySelector searchEditor = By.res(PACKAGE_SEARCH, "search_text_editor");
202         UiObject2 editText = mDevice.wait(Until.findObject(searchEditor), SHORT_WAIT_TIME);
203         if (editText == null) {
204             throw new RuntimeException("Could not find search text input.");
205         }
206 
207         editText.setText(query);
208         SystemClock.sleep(SHORT_WAIT_TIME);
209 
210         // Note that Enter key is pressed instead of DPad keys to dismiss leanback IME
211         mDevice.pressEnter();
212         mDevice.wait(Until.gone(searchEditor), SHORT_WAIT_TIME);
213     }
214 
215     /**
216      * {@inheritDoc}
217      *
218      * Assume that the rows are sorted in the following order from the top:
219      *  Search, Notification(, Partner), Apps, Games, Settings(, and Inputs)
220      */
221     @Override
selectNotificationRow()222     public UiObject2 selectNotificationRow() {
223         if (!isNotificationRowSelected()) {
224             open();
225             mDevice.pressHome();    // Home key to move to the first card in the Notification row
226         }
227         return mDevice.wait(Until.findObject(
228                 getNotificationRowSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME);
229     }
230 
231     /**
232      * {@inheritDoc}
233      */
234     @Override
selectSearchRow()235     public UiObject2 selectSearchRow() {
236         if (!isSearchRowSelected()) {
237             selectNotificationRow();
238             mDevice.pressDPadUp();
239         }
240         return mDevice.wait(Until.findObject(
241                 getSearchRowSelector().hasDescendant(By.focused(true))), SHORT_WAIT_TIME);
242     }
243 
244     /**
245      * {@inheritDoc}
246      */
247     @Override
selectAppsRow()248     public UiObject2 selectAppsRow() {
249         // Start finding Apps row from Notification row
250         if (!isAppsRowSelected()) {
251             selectNotificationRow();
252             mDevice.pressDPadDown();
253         }
254         return mDevice.wait(Until.findObject(
255                 getAllAppsSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME);
256     }
257 
258     /**
259      * {@inheritDoc}
260      */
261     @Override
selectGamesRow()262     public UiObject2 selectGamesRow() {
263         if (!isGamesRowSelected()) {
264             selectAppsRow();
265             mDevice.pressDPadDown();
266             // If more than or equal to 16 apps are installed, the app banner could be cut off
267             // into two rows at maximum. It needs to scroll down once more.
268             if (!isGamesRowSelected()) {
269                 mDevice.pressDPadDown();
270             }
271         }
272         return mDevice.wait(Until.findObject(
273                 getGamesRowSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME);
274     }
275 
276     /**
277      * {@inheritDoc}
278      */
279     @Override
selectSettingsRow()280     public UiObject2 selectSettingsRow() {
281         if (!isSettingsRowSelected()) {
282             open();
283             mDevice.pressHome();    // Home key to move to the first card in the Notification row
284             // The Settings row is at the last position
285             final int MAX_ROW_NUMS = 8;
286             for (int i = 0; i < MAX_ROW_NUMS; ++i) {
287                 mDevice.pressDPadDown();
288             }
289         }
290         return null;
291     }
292 
293     @SuppressWarnings("unused")
294     @Override
getAllAppsButtonSelector()295     public BySelector getAllAppsButtonSelector() {
296         throw new UnsupportedOperationException(
297                 "The 'All Apps' button is not available on Leanback Launcher.");
298     }
299 
300     @SuppressWarnings("unused")
301     @Override
openAllWidgets(boolean reset)302     public UiObject2 openAllWidgets(boolean reset) {
303         throw new UnsupportedOperationException(
304                 "All Widgets is not available on Leanback Launcher.");
305     }
306 
307     @SuppressWarnings("unused")
308     @Override
getAllWidgetsSelector()309     public BySelector getAllWidgetsSelector() {
310         throw new UnsupportedOperationException(
311                 "All Widgets is not available on Leanback Launcher.");
312     }
313 
314     @SuppressWarnings("unused")
315     @Override
getAllWidgetsScrollDirection()316     public Direction getAllWidgetsScrollDirection() {
317         throw new UnsupportedOperationException(
318                 "All Widgets is not available on Leanback Launcher.");
319     }
320 
321     @SuppressWarnings("unused")
322     @Override
getHotSeatSelector()323     public BySelector getHotSeatSelector() {
324         throw new UnsupportedOperationException(
325                 "Hot Seat is not available on Leanback Launcher.");
326     }
327 
328     @SuppressWarnings("unused")
329     @Override
getWorkspaceScrollDirection()330     public Direction getWorkspaceScrollDirection() {
331         throw new UnsupportedOperationException(
332                 "Workspace is not available on Leanback Launcher.");
333     }
334 
launchApp(ILauncherStrategy launcherStrategy, BySelector app, String packageName)335     protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
336             String packageName) {
337         return launchApp(launcherStrategy, app, packageName, MAX_SCROLL_ATTEMPTS);
338     }
339 
launchApp(ILauncherStrategy launcherStrategy, BySelector app, String packageName, int maxScrollAttempts)340     protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
341             String packageName, int maxScrollAttempts) {
342         unlockDeviceIfAsleep();
343 
344         if (isAppOpen(packageName)) {
345             // Application is already open
346             return 0;
347         }
348 
349         // Go to the home page
350         launcherStrategy.open();
351         // attempt to find the app icon if it's not already on the screen
352         UiObject2 container = launcherStrategy.openAllApps(false);
353         UiObject2 appIcon = container.findObject(app);
354         int attempts = 0;
355         while (attempts++ < maxScrollAttempts) {
356             // Compare the focused icon and the app icon to search for.
357             UiObject2 focusedIcon = container.findObject(By.focused(true))
358                     .findObject(By.res(getSupportedLauncherPackage(), "app_banner"));
359 
360             if (appIcon == null) {
361                 appIcon = findApp(container, focusedIcon, app);
362                 if (appIcon == null) {
363                     throw new RuntimeException("Failed to find the app icon on screen: "
364                             + packageName);
365                 }
366                 continue;
367             } else if (focusedIcon.equals(appIcon)) {
368                 // The app icon is on the screen, and selected.
369                 break;
370             } else {
371                 // The app icon is on the screen, but not selected yet
372                 // Move one step closer to the app icon
373                 Point currentPosition = focusedIcon.getVisibleCenter();
374                 Point targetPosition = appIcon.getVisibleCenter();
375                 int dx = targetPosition.x - currentPosition.x;
376                 int dy = targetPosition.y - currentPosition.y;
377                 final int MARGIN = 10;
378                 // The sequence of moving should be kept in the following order so as not to
379                 // be stuck in case that the apps row are not even.
380                 if (dx < -MARGIN) {
381                     mDevice.pressDPadLeft();
382                     continue;
383                 }
384                 if (dy < -MARGIN) {
385                     mDevice.pressDPadUp();
386                     continue;
387                 }
388                 if (dx > MARGIN) {
389                     mDevice.pressDPadRight();
390                     continue;
391                 }
392                 if (dy > MARGIN) {
393                     mDevice.pressDPadDown();
394                     continue;
395                 }
396                 throw new RuntimeException(
397                         "Failed to navigate to the app icon on screen: " + packageName);
398             }
399         }
400 
401         if (attempts == maxScrollAttempts) {
402             throw new RuntimeException(
403                     "scrollBackToBeginning: exceeded max attempts: " + maxScrollAttempts);
404         }
405 
406         // The app icon is already found and focused.
407         long ready = SystemClock.uptimeMillis();
408         mDevice.pressDPadCenter();
409         mDevice.waitForIdle();
410         if (packageName != null) {
411             Log.w(LOG_TAG, String.format(
412                     "No UI element with package name %s detected.", packageName));
413             boolean success = mDevice.wait(Until.hasObject(
414                     By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT);
415             if (success) {
416                 return ready;
417             } else {
418                 return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
419             }
420         } else {
421             return ready;
422         }
423     }
424 
isSearchRowSelected()425     protected boolean isSearchRowSelected() {
426         UiObject2 row = mDevice.findObject(getSearchRowSelector());
427         if (row == null) {
428             return false;
429         }
430         return row.hasObject(By.focused(true));
431     }
432 
isAppsRowSelected()433     protected boolean isAppsRowSelected() {
434         UiObject2 row = mDevice.findObject(getAppsRowSelector());
435         if (row == null) {
436             return false;
437         }
438         return row.hasObject(By.focused(true));
439     }
440 
isGamesRowSelected()441     protected boolean isGamesRowSelected() {
442         UiObject2 row = mDevice.findObject(getGamesRowSelector());
443         if (row == null) {
444             return false;
445         }
446         return row.hasObject(By.focused(true));
447     }
448 
isNotificationRowSelected()449     protected boolean isNotificationRowSelected() {
450         UiObject2 row = mDevice.findObject(getNotificationRowSelector());
451         if (row == null) {
452             return false;
453         }
454         return row.hasObject(By.focused(true));
455     }
456 
isSettingsRowSelected()457     protected boolean isSettingsRowSelected() {
458         // Settings label is only visible if the settings row is selected
459         return mDevice.hasObject(By.res(getSupportedLauncherPackage(), "label").text("Settings"));
460     }
461 
isAppOpen(String appPackage)462     protected boolean isAppOpen (String appPackage) {
463         return mDevice.hasObject(By.pkg(appPackage).depth(0));
464     }
465 
unlockDeviceIfAsleep()466     protected void unlockDeviceIfAsleep () {
467         // Turn screen on if necessary
468         try {
469             if (!mDevice.isScreenOn()) {
470                 mDevice.wakeUp();
471             }
472         } catch (RemoteException e) {
473             Log.e(LOG_TAG, "Failed to unlock the screen-off device.", e);
474         }
475     }
476 
findApp(UiObject2 container, UiObject2 focusedIcon, BySelector app)477     protected UiObject2 findApp(UiObject2 container, UiObject2 focusedIcon, BySelector app) {
478         UiObject2 appIcon;
479         // The app icon is not on the screen.
480         // Search by going left first until it finds the app icon on the screen
481         String prevText = focusedIcon.getContentDescription();
482         String nextText;
483         do {
484             mDevice.pressDPadLeft();
485             appIcon = container.findObject(app);
486             if (appIcon != null) {
487                 return appIcon;
488             }
489             nextText = container.findObject(By.focused(true)).findObject(
490                     By.res(getSupportedLauncherPackage(),
491                             "app_banner")).getContentDescription();
492         } while (nextText != null && !nextText.equals(prevText));
493 
494         // If we haven't found it yet, search by going right
495         do {
496             mDevice.pressDPadRight();
497             appIcon = container.findObject(app);
498             if (appIcon != null) {
499                 return appIcon;
500             }
501             nextText = container.findObject(By.focused(true)).findObject(
502                     By.res(getSupportedLauncherPackage(),
503                             "app_banner")).getContentDescription();
504         } while (nextText != null && !nextText.equals(prevText));
505         return null;
506     }
507 }
508