blob: 3a97a4b8d497f8b89d04582b3f2d38a4bcb945ed [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.test.launcherhelper;
import android.app.Instrumentation;
import android.os.SystemClock;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.Direction;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;
import android.system.helpers.CommandsHelper;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class AutoLauncherStrategy implements IAutoLauncherStrategy {
private static final String LOG_TAG = AutoLauncherStrategy.class.getSimpleName();
private static final String CAR_LENSPICKER = "com.android.car.carlauncher";
private static final String SYSTEM_UI_PACKAGE = "com.android.systemui";
private static final String MAPS_PACKAGE = "com.google.android.apps.maps";
private static final String MEDIA_PACKAGE = "com.android.car.media";
private static final String RADIO_PACKAGE = "com.android.car.radio";
private static final String DIAL_PACKAGE = "com.android.car.dialer";
private static final String ASSISTANT_PACKAGE = "com.google.android.googlequicksearchbox";
private static final String SETTINGS_PACKAGE = "com.android.car.settings";
private static final String APP_SWITCH_ID = "app_switch_container";
private static final String APP_LIST_ID = "apps_grid";
private static final long APP_LAUNCH_TIMEOUT = 30000;
private static final long UI_WAIT_TIMEOUT = 5000;
private static final long POLL_INTERVAL = 100;
private static final BySelector UP_BTN =
By.res(Pattern.compile(".*:id/car_ui_scrollbar_page_up"));
private static final BySelector DOWN_BTN =
By.res(Pattern.compile(".*:id/car_ui_scrollbar_page_down"));
private static final BySelector APP_SWITCH = By.res(Pattern.compile(".*:id/" + APP_SWITCH_ID));
private static final BySelector APP_LIST = By.res(Pattern.compile(".*:id/" + APP_LIST_ID));
private static final BySelector SCROLLABLE_APP_LIST =
By.res(Pattern.compile(".*:id/" + APP_LIST_ID)).scrollable(true);
private static final BySelector QUICK_SETTINGS = By.res(SYSTEM_UI_PACKAGE, "qs");
private static final BySelector LEFT_HVAC = By.res(SYSTEM_UI_PACKAGE, "hvacleft");
private static final BySelector RIGHT_HVAC = By.res(SYSTEM_UI_PACKAGE, "hvacright");
private static final Map<String, BySelector> FACET_MAP =
Stream.of(new Object[][] {
{ "Home", By.res(SYSTEM_UI_PACKAGE, "home").clickable(true) },
{ "Maps", By.res(SYSTEM_UI_PACKAGE, "maps_nav").clickable(true) },
{ "Media", By.res(SYSTEM_UI_PACKAGE, "music_nav").clickable(true) },
{ "Dial", By.res(SYSTEM_UI_PACKAGE, "phone_nav").clickable(true) },
{ "App Grid", By.res(SYSTEM_UI_PACKAGE, "grid_nav").clickable(true) },
{ "Notification", By.res(SYSTEM_UI_PACKAGE, "notifications").clickable(true) },
{ "Google Assistant", By.res(SYSTEM_UI_PACKAGE, "assist").clickable(true) },
}).collect(Collectors.toMap(data -> (String) data[0], data -> (BySelector) data[1]));
private static final Map<String, BySelector> APP_OPEN_VERIFIERS =
Stream.of(new Object[][] {
{ "Home", By.hasDescendant(By.res(CAR_LENSPICKER, "maps"))
.hasDescendant(By.res(CAR_LENSPICKER, "contextual"))
.hasDescendant(By.res(CAR_LENSPICKER, "playback"))
},
{ "Maps", By.pkg(MAPS_PACKAGE).depth(0) },
{ "Media", By.pkg(MEDIA_PACKAGE).depth(0) },
{ "Radio", By.pkg(RADIO_PACKAGE).depth(0) },
{ "Dial", By.pkg(DIAL_PACKAGE).depth(0) },
{ "App Grid", By.res(CAR_LENSPICKER, "apps_grid") },
{ "Notification", By.res(SYSTEM_UI_PACKAGE, "notifications") },
{ "Google Assistant", By.pkg(ASSISTANT_PACKAGE) },
{ "Settings", By.pkg(SETTINGS_PACKAGE).depth(0) },
}).collect(Collectors.toMap(data -> (String) data[0], data -> (BySelector) data[1]));
protected UiDevice mDevice;
private Instrumentation mInstrumentation;
protected CommandsHelper mCommandsHelper;
/**
* {@inheritDoc}
*/
@Override
public String getSupportedLauncherPackage() {
return CAR_LENSPICKER;
}
/**
* {@inheritDoc}
*/
@Override
public void setUiDevice(UiDevice uiDevice) {
mDevice = uiDevice;
}
/**
* {@inheritDoc}
*/
@Override
public void setInstrumentation(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
mCommandsHelper = CommandsHelper.getInstance(mInstrumentation);
}
/**
* {@inheritDoc}
*/
@Override
public void open() {
openHomeFacet();
}
/**
* {@inheritDoc}
*/
@Override
public void openHomeFacet() {
openFacet("Home");
}
/**
* {@inheritDoc}
*/
@Override
public void openMapsFacet() {
openFacet("Maps");
}
/**
* {@inheritDoc}
*/
@Override
public void openMediaFacet() {
openFacet("Media");
}
/**
* {@inheritDoc}
*/
@Override
public void openMediaFacet(String appName) {
openMediaFacet();
// Click on app switch to open app list.
UiObject2 appSwitch = mDevice.wait(Until.findObject(APP_SWITCH), APP_LAUNCH_TIMEOUT);
if (appSwitch == null) {
throw new RuntimeException("Failed to find app switch.");
}
appSwitch.clickAndWait(Until.newWindow(), UI_WAIT_TIMEOUT);
mDevice.waitForIdle();
// Click the targeted app in app list.
UiObject2 app = findApplication(appName);
if (app == null) {
throw new RuntimeException(String.format("Failed to find %s app in media.", appName));
}
app.click();
mDevice.wait(Until.gone(APP_LIST), UI_WAIT_TIMEOUT);
// Verify either a Radio or Media app is in foreground for success.
if (appName.equals("Radio")) {
waitUntilAppOpen("Radio", APP_LAUNCH_TIMEOUT);
} else {
waitUntilAppOpen("Media", APP_LAUNCH_TIMEOUT);
}
}
/**
* {@inheritDoc}
*/
@Override
public void openDialFacet() {
openFacet("Dial");
}
/**
* {@inheritDoc}
*/
@Override
public void openAppGridFacet() {
openFacet("App Grid");
}
/**
* {@inheritDoc}
*/
@Override
public void openNotificationFacet() {
openFacet("Notification");
}
/** {@inheritDoc} */
@Override
public void openNotifications() {
openNotificationFacet();
}
/** {@inheritDoc} */
@Override
public void pressHome() {
openHomeFacet();
}
/** {@inheritDoc} */
@Override
public void openAssistantFacet() {
openFacet("Google Assistant");
}
private void openFacet(String facetName) {
BySelector facetSelector = FACET_MAP.get(facetName);
UiObject2 facet = mDevice.findObject(facetSelector);
if (facet != null) {
facet.click();
if (!facetName.equals("Media")) {
// Verify the corresponding app has been open in the foregorund for success.
waitUntilAppOpen(facetName, APP_LAUNCH_TIMEOUT);
} else {
// For Media facet, it could open either radio app or some media app.
waitUntilOneOfTheAppOpen(Arrays.asList("Radio", "Media"), APP_LAUNCH_TIMEOUT);
}
} else {
throw new RuntimeException(String.format("Failed to find %s facet.", facetName));
}
}
/**
* {@inheritDoc}
*/
@Override
public void openQuickSettings() {
UiObject2 quickSettings = mDevice.findObject(QUICK_SETTINGS);
if (quickSettings != null) {
quickSettings.click();
waitUntilAppOpen("Settings", APP_LAUNCH_TIMEOUT);
} else {
throw new RuntimeException("Failed to find quick settings.");
}
}
/**
* Wait for <code>timeout</code> milliseconds until the BySelector which can verify that the app
* corresponding to <code>appName</code> is in foreground has been found. If the BySelector
* isn't found after timeout, throw an error.
*/
private void waitUntilAppOpen(String appName, long timeout) {
waitUntilOneOfTheAppOpen(Arrays.asList(appName), timeout);
}
/**
* Wait for <code>time</code> milliseconds until one of the app in <code>appNames</code> has
* been found in the foreground.
*/
private void waitUntilOneOfTheAppOpen(List<String> appNames, long timeout) {
long startTime = SystemClock.uptimeMillis();
boolean isAnyOpen = false;
while (SystemClock.uptimeMillis() - startTime < timeout) {
isAnyOpen =
appNames.stream()
.map(appName -> mDevice.hasObject(APP_OPEN_VERIFIERS.get(appName)))
.anyMatch(isOpen -> isOpen == true);
if (isAnyOpen) {
break;
}
SystemClock.sleep(POLL_INTERVAL);
}
if (!isAnyOpen) {
throw new IllegalStateException(
String.format(
"Did not find any app of %s in foreground after %d ms.",
String.join(", ", appNames), timeout));
}
}
/**
* {@inheritDoc}
*/
@Override
public void clickLeftHvac() {
clickHvac(LEFT_HVAC);
}
/**
* {@inheritDoc}
*/
@Override
public void clickRightHvac() {
clickHvac(RIGHT_HVAC);
}
private void clickHvac(BySelector hvacSelector) {
UiObject2 hvac = mDevice.findObject(hvacSelector);
if (hvac != null) {
// Hvac is not verifiable from uiautomator. It does not starts a new window, nor does
// it spawn any specific identifiable ui components from uiautomator's perspective.
// Therefore, this wait does not verify a new window, but rather it always timeout
// after <code>APP_LAUNCH_TIMEOUT</code> amount of time so that hvac has sufficient
// time to be opened/closed.
hvac.clickAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT);
mDevice.waitForIdle();
} else {
throw new RuntimeException("Failed to find hvac.");
}
}
@Override
public boolean checkApplicationExists(String appName) {
openAppGridFacet();
UiObject2 app = findApplication(appName);
return app != null;
}
@Override
public void openApp(String appName) {
if (checkApplicationExists(appName)) {
UiObject2 app = mDevice.findObject(By.clickable(true).hasDescendant(By.text(appName)));
app.clickAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT);
mDevice.waitForIdle();
} else {
throw new RuntimeException(String.format("Application %s not found", appName));
}
}
@Override
public void openBluetoothAudioApp() {
String appName = "Bluetooth Audio";
if (checkApplicationExists(appName)) {
UiObject2 app = mDevice.findObject(By.clickable(true).hasDescendant(By.text(appName)));
app.clickAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT);
mDevice.waitForIdle();
} else {
throw new RuntimeException(String.format("Application %s not found", appName));
}
}
@Override
public void openGooglePlayStore() {
openApp("Play Store");
SystemClock.sleep(APP_LAUNCH_TIMEOUT);
}
private UiObject2 findApplication(String appName) {
BySelector appSelector = By.clickable(true).hasDescendant(By.text(appName));
if (mDevice.hasObject(SCROLLABLE_APP_LIST)) {
// App list has more than 1 page. Scroll if necessary when searching for app.
UiObject2 down = mDevice.findObject(DOWN_BTN);
UiObject2 up = mDevice.findObject(UP_BTN);
while (up.isEnabled()) {
up.click();
mDevice.waitForIdle();
}
UiObject2 app = mDevice.wait(Until.findObject(appSelector), UI_WAIT_TIMEOUT);
while (app == null && down.isEnabled()) {
down.click();
mDevice.waitForIdle();
app = mDevice.wait(Until.findObject(appSelector), UI_WAIT_TIMEOUT);
}
return app;
} else {
// App list only has 1 page.
return mDevice.findObject(appSelector);
}
}
@SuppressWarnings("unused")
@Override
public UiObject2 openAllApps(boolean reset) {
throw new UnsupportedOperationException(
"The feature not supported on Auto");
}
@SuppressWarnings("unused")
@Override
public BySelector getAllAppsButtonSelector() {
throw new UnsupportedOperationException(
"The feature not supported on Auto");
}
@SuppressWarnings("unused")
@Override
public BySelector getAllAppsSelector() {
throw new UnsupportedOperationException(
"The feature not supported on Auto");
}
@SuppressWarnings("unused")
@Override
public Direction getAllAppsScrollDirection() {
throw new UnsupportedOperationException(
"The feature not supported on Auto");
}
@SuppressWarnings("unused")
@Override
public UiObject2 openAllWidgets(boolean reset) {
throw new UnsupportedOperationException(
"The feature not supported on Auto");
}
@SuppressWarnings("unused")
@Override
public BySelector getAllWidgetsSelector() {
throw new UnsupportedOperationException(
"The feature not supported on Auto");
}
@SuppressWarnings("unused")
@Override
public Direction getAllWidgetsScrollDirection() {
throw new UnsupportedOperationException(
"The feature not supported on Auto");
}
@SuppressWarnings("unused")
@Override
public BySelector getWorkspaceSelector() {
throw new UnsupportedOperationException(
"The feature not supported on Auto");
}
@SuppressWarnings("unused")
@Override
public BySelector getHotSeatSelector() {
throw new UnsupportedOperationException(
"The feature not supported on Auto");
}
@SuppressWarnings("unused")
@Override
public Direction getWorkspaceScrollDirection() {
throw new UnsupportedOperationException(
"The feature not supported on Auto");
}
@SuppressWarnings("unused")
@Override
public long launch(String appName, String packageName) {
openApp(appName);
return 0;
}
}