• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 package android.support.test.launcherhelper;
17 
18 import android.app.Instrumentation;
19 import android.os.SystemClock;
20 import android.support.test.uiautomator.By;
21 import android.support.test.uiautomator.BySelector;
22 import android.support.test.uiautomator.Direction;
23 import android.support.test.uiautomator.UiDevice;
24 import android.support.test.uiautomator.UiObject2;
25 import android.support.test.uiautomator.Until;
26 import android.system.helpers.CommandsHelper;
27 
28 import java.util.Arrays;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.regex.Pattern;
32 import java.util.stream.Collectors;
33 import java.util.stream.Stream;
34 
35 public class AutoLauncherStrategy implements IAutoLauncherStrategy {
36     private static final String LOG_TAG = AutoLauncherStrategy.class.getSimpleName();
37     private static final String CAR_LENSPICKER = "com.android.car.carlauncher";
38     private static final String SYSTEM_UI_PACKAGE = "com.android.systemui";
39     private static final String MAPS_PACKAGE = "com.google.android.apps.maps";
40     private static final String MEDIA_PACKAGE = "com.android.car.media";
41     private static final String RADIO_PACKAGE = "com.android.car.radio";
42     private static final String DIAL_PACKAGE = "com.android.car.dialer";
43     private static final String ASSISTANT_PACKAGE = "com.google.android.googlequicksearchbox";
44     private static final String SETTINGS_PACKAGE = "com.android.car.settings";
45     private static final String APP_SWITCH_ID = "car_ui_toolbar_menu_item_icon_container";
46     private static final String APP_LIST_ID = "apps_grid";
47 
48     private static final long APP_LAUNCH_TIMEOUT = 30000;
49     private static final long UI_WAIT_TIMEOUT = 5000;
50     private static final long POLL_INTERVAL = 100;
51 
52     private static final BySelector UP_BTN =
53             By.res(Pattern.compile(".*:id/car_ui_scrollbar_page_up"));
54     private static final BySelector DOWN_BTN =
55             By.res(Pattern.compile(".*:id/car_ui_scrollbar_page_down"));
56     private static final BySelector APP_SWITCH = By.res(Pattern.compile(".*:id/" + APP_SWITCH_ID));
57     private static final BySelector APP_LIST = By.res(Pattern.compile(".*:id/" + APP_LIST_ID));
58     private static final BySelector SCROLLABLE_APP_LIST =
59             By.res(Pattern.compile(".*:id/" + APP_LIST_ID)).scrollable(true);
60     private static final BySelector QUICK_SETTINGS = By.res(SYSTEM_UI_PACKAGE, "qs");
61     private static final BySelector LEFT_HVAC = By.res(SYSTEM_UI_PACKAGE, "hvacleft");
62     private static final BySelector RIGHT_HVAC = By.res(SYSTEM_UI_PACKAGE, "hvacright");
63 
64     private static final Map<String, BySelector> FACET_MAP =
65             Stream.of(new Object[][] {
66                 { "Home", By.res(SYSTEM_UI_PACKAGE, "home").clickable(true) },
67                 { "Maps", By.res(SYSTEM_UI_PACKAGE, "maps_nav").clickable(true) },
68                 { "Media", By.res(SYSTEM_UI_PACKAGE, "music_nav").clickable(true) },
69                 { "Dial", By.res(SYSTEM_UI_PACKAGE, "phone_nav").clickable(true) },
70                 { "App Grid", By.res(SYSTEM_UI_PACKAGE, "grid_nav").clickable(true) },
71                 { "Notification", By.res(SYSTEM_UI_PACKAGE, "notifications").clickable(true) },
72                 { "Google Assistant", By.res(SYSTEM_UI_PACKAGE, "assist").clickable(true) },
73             }).collect(Collectors.toMap(data -> (String) data[0], data -> (BySelector) data[1]));
74 
75     private static final Map<String, BySelector> APP_OPEN_VERIFIERS =
76             Stream.of(new Object[][] {
77                 { "Home", By.hasDescendant(By.res(CAR_LENSPICKER, "maps"))
78                             .hasDescendant(By.res(CAR_LENSPICKER, "contextual"))
79                             .hasDescendant(By.res(CAR_LENSPICKER, "playback"))
80                 },
81                 { "Maps", By.pkg(MAPS_PACKAGE).depth(0) },
82                 { "Media", By.pkg(MEDIA_PACKAGE).depth(0) },
83                 { "Radio", By.pkg(RADIO_PACKAGE).depth(0) },
84                 { "Dial",  By.pkg(DIAL_PACKAGE).depth(0) },
85                 { "App Grid", By.res(CAR_LENSPICKER, "apps_grid") },
86                 { "Notification", By.res(SYSTEM_UI_PACKAGE, "notifications") },
87                 { "Google Assistant", By.pkg(ASSISTANT_PACKAGE) },
88                 { "Settings", By.pkg(SETTINGS_PACKAGE).depth(0) },
89             }).collect(Collectors.toMap(data -> (String) data[0], data -> (BySelector) data[1]));
90 
91     protected UiDevice mDevice;
92     private Instrumentation mInstrumentation;
93     protected CommandsHelper mCommandsHelper;
94 
95     /**
96      * {@inheritDoc}
97      */
98     @Override
getSupportedLauncherPackage()99     public String getSupportedLauncherPackage() {
100         return CAR_LENSPICKER;
101     }
102 
103     /**
104      * {@inheritDoc}
105      */
106     @Override
setUiDevice(UiDevice uiDevice)107     public void setUiDevice(UiDevice uiDevice) {
108         mDevice = uiDevice;
109     }
110 
111     /**
112      * {@inheritDoc}
113      */
114     @Override
setInstrumentation(Instrumentation instrumentation)115     public void setInstrumentation(Instrumentation instrumentation) {
116         mInstrumentation = instrumentation;
117         mCommandsHelper = CommandsHelper.getInstance(mInstrumentation);
118     }
119 
120     /**
121      * {@inheritDoc}
122      */
123     @Override
open()124     public void open() {
125         openHomeFacet();
126     }
127 
128     /**
129      * {@inheritDoc}
130      */
131     @Override
openHomeFacet()132     public void openHomeFacet() {
133         openFacet("Home");
134     }
135 
136     /**
137      * {@inheritDoc}
138      */
139     @Override
openMapsFacet()140     public void openMapsFacet() {
141         openFacet("Maps");
142     }
143 
144     /**
145      * {@inheritDoc}
146      */
147     @Override
openMediaFacet()148     public void openMediaFacet() {
149         openFacet("Media");
150     }
151 
152     /**
153      * {@inheritDoc}
154      */
155     @Override
openMediaFacet(String appName)156     public void openMediaFacet(String appName) {
157         openMediaFacet();
158 
159         // Click on app switch to open app list.
160         List<UiObject2> buttons = mDevice.wait(
161                 Until.findObjects(APP_SWITCH), APP_LAUNCH_TIMEOUT);
162         int lastIndex = buttons.size() - 1;
163         /*
164          * On some media app page, there are two buttons with the same ID,
165          * while on other media app page, only the app switch button presents.
166          * The app switch button is always the last button if not the only button.
167          */
168         UiObject2 appSwitch = buttons.get(lastIndex);
169         if (appSwitch == null) {
170             throw new RuntimeException("Failed to find app switch.");
171         }
172         appSwitch.clickAndWait(Until.newWindow(), UI_WAIT_TIMEOUT);
173         mDevice.waitForIdle();
174 
175         // Click the targeted app in app list.
176         UiObject2 app = findApplication(appName);
177         if (app == null) {
178             throw new RuntimeException(String.format("Failed to find %s app in media.", appName));
179         }
180         app.click();
181         mDevice.wait(Until.gone(APP_LIST), UI_WAIT_TIMEOUT);
182 
183         // Verify either a Radio or Media app is in foreground for success.
184         if (appName.equals("Radio")) {
185             waitUntilAppOpen("Radio", APP_LAUNCH_TIMEOUT);
186         } else {
187             waitUntilAppOpen("Media", APP_LAUNCH_TIMEOUT);
188         }
189 
190     }
191 
192     /**
193      * {@inheritDoc}
194      */
195     @Override
openDialFacet()196     public void openDialFacet() {
197         openFacet("Dial");
198     }
199 
200     /**
201      * {@inheritDoc}
202      */
203     @Override
openAppGridFacet()204     public void openAppGridFacet() {
205         openFacet("App Grid");
206     }
207 
208     /**
209      * {@inheritDoc}
210      */
211     @Override
openNotificationFacet()212     public void openNotificationFacet() {
213         openFacet("Notification");
214     }
215 
216     /** {@inheritDoc} */
217     @Override
openNotifications()218     public void openNotifications() {
219         openNotificationFacet();
220     }
221 
222     /** {@inheritDoc} */
223     @Override
pressHome()224     public void pressHome() {
225         openHomeFacet();
226     }
227 
228     /** {@inheritDoc} */
229     @Override
openAssistantFacet()230     public void openAssistantFacet() {
231         openFacet("Google Assistant");
232     }
233 
openFacet(String facetName)234     private void openFacet(String facetName) {
235         BySelector facetSelector = FACET_MAP.get(facetName);
236         UiObject2 facet = mDevice.findObject(facetSelector);
237         if (facet != null) {
238             facet.click();
239             if (!facetName.equals("Media")) {
240                 // Verify the corresponding app has been open in the foregorund for success.
241                 waitUntilAppOpen(facetName, APP_LAUNCH_TIMEOUT);
242             } else {
243                 // For Media facet, it could open either radio app or some media app.
244                 waitUntilOneOfTheAppOpen(Arrays.asList("Radio", "Media"), APP_LAUNCH_TIMEOUT);
245             }
246         } else {
247             throw new RuntimeException(String.format("Failed to find %s facet.", facetName));
248         }
249     }
250 
251     /**
252      * {@inheritDoc}
253      */
254     @Override
openQuickSettings()255     public void openQuickSettings() {
256         UiObject2 quickSettings = mDevice.findObject(QUICK_SETTINGS);
257         if (quickSettings != null) {
258             quickSettings.click();
259             waitUntilAppOpen("Settings", APP_LAUNCH_TIMEOUT);
260         } else {
261             throw new RuntimeException("Failed to find quick settings.");
262         }
263     }
264 
265     /**
266      * Wait for <code>timeout</code> milliseconds until the BySelector which can verify that the app
267      * corresponding to <code>appName</code> is in foreground has been found. If the BySelector
268      * isn't found after timeout, throw an error.
269      */
waitUntilAppOpen(String appName, long timeout)270     private void waitUntilAppOpen(String appName, long timeout) {
271         waitUntilOneOfTheAppOpen(Arrays.asList(appName), timeout);
272     }
273 
274     /**
275      * Wait for <code>time</code> milliseconds until one of the app in <code>appNames</code> has
276      * been found in the foreground.
277      */
waitUntilOneOfTheAppOpen(List<String> appNames, long timeout)278     private void waitUntilOneOfTheAppOpen(List<String> appNames, long timeout) {
279         long startTime = SystemClock.uptimeMillis();
280 
281         boolean isAnyOpen = false;
282         while (SystemClock.uptimeMillis() - startTime < timeout) {
283             isAnyOpen =
284                     appNames.stream()
285                             .map(appName -> mDevice.hasObject(APP_OPEN_VERIFIERS.get(appName)))
286                             .anyMatch(isOpen -> isOpen == true);
287             if (isAnyOpen) {
288                 break;
289             }
290 
291             SystemClock.sleep(POLL_INTERVAL);
292         }
293 
294         if (!isAnyOpen) {
295             throw new IllegalStateException(
296                     String.format(
297                             "Did not find any app of %s in foreground after %d ms.",
298                             String.join(", ", appNames), timeout));
299         }
300     }
301 
302     /**
303      * {@inheritDoc}
304      */
305     @Override
clickLeftHvac()306     public void clickLeftHvac() {
307         clickHvac(LEFT_HVAC);
308     }
309 
310     /**
311      * {@inheritDoc}
312      */
313     @Override
clickRightHvac()314     public void clickRightHvac() {
315         clickHvac(RIGHT_HVAC);
316     }
317 
clickHvac(BySelector hvacSelector)318     private void clickHvac(BySelector hvacSelector) {
319         UiObject2 hvac = mDevice.findObject(hvacSelector);
320         if (hvac != null) {
321             // Hvac is not verifiable from uiautomator. It does not starts a new window, nor does
322             // it spawn any specific identifiable ui components from uiautomator's perspective.
323             // Therefore, this wait does not verify a new window, but rather it always timeout
324             // after <code>APP_LAUNCH_TIMEOUT</code> amount of time so that hvac has sufficient
325             // time to be opened/closed.
326             hvac.clickAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT);
327             mDevice.waitForIdle();
328         } else {
329             throw new RuntimeException("Failed to find hvac.");
330         }
331     }
332 
333     @Override
checkApplicationExists(String appName)334     public boolean checkApplicationExists(String appName) {
335         openAppGridFacet();
336         UiObject2 app = findApplication(appName);
337         return app != null;
338     }
339 
340     @Override
openApp(String appName)341     public void openApp(String appName) {
342         if (checkApplicationExists(appName)) {
343             UiObject2 app = mDevice.findObject(By.clickable(true).hasDescendant(By.text(appName)));
344             app.clickAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT);
345             mDevice.waitForIdle();
346         } else {
347             throw new RuntimeException(String.format("Application %s not found", appName));
348         }
349     }
350 
351     @Override
openBluetoothAudioApp()352     public void openBluetoothAudioApp() {
353         String appName = "Bluetooth Audio";
354         if (checkApplicationExists(appName)) {
355             UiObject2 app = mDevice.findObject(By.clickable(true).hasDescendant(By.text(appName)));
356             app.clickAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT);
357             mDevice.waitForIdle();
358         } else {
359             throw new RuntimeException(String.format("Application %s not found", appName));
360         }
361     }
362 
363     @Override
openGooglePlayStore()364     public void openGooglePlayStore() {
365         openApp("Play Store");
366         SystemClock.sleep(APP_LAUNCH_TIMEOUT);
367     }
368 
findApplication(String appName)369     private UiObject2 findApplication(String appName) {
370         BySelector appSelector = By.clickable(true).hasDescendant(By.text(appName));
371         if (mDevice.hasObject(SCROLLABLE_APP_LIST)) {
372             // App list has more than 1 page. Scroll if necessary when searching for app.
373             UiObject2 down = mDevice.findObject(DOWN_BTN);
374             UiObject2 up = mDevice.findObject(UP_BTN);
375 
376             while (up.isEnabled()) {
377                 up.click();
378                 mDevice.waitForIdle();
379             }
380 
381             UiObject2 app = mDevice.wait(Until.findObject(appSelector), UI_WAIT_TIMEOUT);
382             while (app == null && down.isEnabled()) {
383                 down.click();
384                 mDevice.waitForIdle();
385                 app = mDevice.wait(Until.findObject(appSelector), UI_WAIT_TIMEOUT);
386             }
387             return app;
388         } else {
389             // App list only has 1 page.
390             return mDevice.findObject(appSelector);
391         }
392     }
393 
394     @SuppressWarnings("unused")
395     @Override
openAllApps(boolean reset)396     public UiObject2 openAllApps(boolean reset) {
397         throw new UnsupportedOperationException(
398                 "The feature not supported on Auto");
399     }
400 
401     @SuppressWarnings("unused")
402     @Override
getAllAppsButtonSelector()403     public BySelector getAllAppsButtonSelector() {
404         throw new UnsupportedOperationException(
405                 "The feature not supported on Auto");
406     }
407 
408     @SuppressWarnings("unused")
409     @Override
getAllAppsSelector()410     public BySelector getAllAppsSelector() {
411         throw new UnsupportedOperationException(
412                 "The feature not supported on Auto");
413     }
414 
415     @SuppressWarnings("unused")
416     @Override
getAllAppsScrollDirection()417     public Direction getAllAppsScrollDirection() {
418         throw new UnsupportedOperationException(
419                 "The feature not supported on Auto");
420     }
421 
422     @SuppressWarnings("unused")
423     @Override
openAllWidgets(boolean reset)424     public UiObject2 openAllWidgets(boolean reset) {
425         throw new UnsupportedOperationException(
426                 "The feature not supported on Auto");
427     }
428 
429     @SuppressWarnings("unused")
430     @Override
getAllWidgetsSelector()431     public BySelector getAllWidgetsSelector() {
432         throw new UnsupportedOperationException(
433                 "The feature not supported on Auto");
434     }
435 
436     @SuppressWarnings("unused")
437     @Override
getAllWidgetsScrollDirection()438     public Direction getAllWidgetsScrollDirection() {
439         throw new UnsupportedOperationException(
440                 "The feature not supported on Auto");
441     }
442 
443     @SuppressWarnings("unused")
444     @Override
getWorkspaceSelector()445     public BySelector getWorkspaceSelector() {
446         throw new UnsupportedOperationException(
447                 "The feature not supported on Auto");
448     }
449 
450     @SuppressWarnings("unused")
451     @Override
getHotSeatSelector()452     public BySelector getHotSeatSelector() {
453         throw new UnsupportedOperationException(
454                 "The feature not supported on Auto");
455     }
456 
457     @SuppressWarnings("unused")
458     @Override
getWorkspaceScrollDirection()459     public Direction getWorkspaceScrollDirection() {
460         throw new UnsupportedOperationException(
461                 "The feature not supported on Auto");
462     }
463 
464     @SuppressWarnings("unused")
465     @Override
launch(String appName, String packageName)466     public long launch(String appName, String packageName) {
467         openApp(appName);
468         return 0;
469     }
470 }
471