blob: 59084ae3faa82181a8a606c9ba207f5c6de98713 [file] [log] [blame]
/*
* Copyright (C) 2016 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.graphics.Point;
import android.os.RemoteException;
import android.os.SystemClock;
import android.platform.test.utils.DPadUtil;
import android.support.test.uiautomator.*;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class LeanbackLauncherStrategy implements ILeanbackLauncherStrategy {
private static final String LOG_TAG = LeanbackLauncherStrategy.class.getSimpleName();
private static final String PACKAGE_LAUNCHER = "com.google.android.leanbacklauncher";
private static final String PACKAGE_SEARCH = "com.google.android.katniss";
private static final int MAX_SCROLL_ATTEMPTS = 20;
private static final int APP_LAUNCH_TIMEOUT = 10000;
private static final int SHORT_WAIT_TIME = 5000; // 5 sec
private static final int NOTIFICATION_WAIT_TIME = 30000;
protected UiDevice mDevice;
protected DPadUtil mDPadUtil = new DPadUtil();
/**
* {@inheritDoc}
*/
@Override
public String getSupportedLauncherPackage() {
return PACKAGE_LAUNCHER;
}
/**
* {@inheritDoc}
*/
@Override
public void setUiDevice(UiDevice uiDevice) {
mDevice = uiDevice;
}
/**
* {@inheritDoc}
*/
@Override
public void open() {
// if we see main list view, assume at home screen already
if (!mDevice.hasObject(getWorkspaceSelector())) {
mDPadUtil.pressHome();
// ensure launcher is shown
if (!mDevice.wait(Until.hasObject(getWorkspaceSelector()), SHORT_WAIT_TIME)) {
// HACK: dump hierarchy to logcat
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
mDevice.dumpWindowHierarchy(baos);
baos.flush();
baos.close();
String[] lines = baos.toString().split("\\r?\\n");
for (String line : lines) {
Log.d(LOG_TAG, line.trim());
}
} catch (IOException ioe) {
Log.e(LOG_TAG, "error dumping XML to logcat", ioe);
}
throw new RuntimeException("Failed to open leanback launcher");
}
mDevice.waitForIdle();
}
}
/**
* {@inheritDoc}
*/
@Override
public UiObject2 openAllApps(boolean reset) {
UiObject2 appsRow = selectAppsRow();
if (appsRow == null) {
throw new RuntimeException("Could not find all apps row");
}
if (reset) {
Log.w(LOG_TAG, "The reset will be ignored on leanback launcher");
}
return appsRow;
}
/**
* {@inheritDoc}
*/
@Override
public BySelector getWorkspaceSelector() {
return By.res(getSupportedLauncherPackage(), "main_list_view");
}
/**
* {@inheritDoc}
*/
@Override
public BySelector getSearchRowSelector() {
return By.res(getSupportedLauncherPackage(), "search_view");
}
/**
* {@inheritDoc}
*/
@Override
public BySelector getNotificationRowSelector() {
return By.res(getSupportedLauncherPackage(), "notification_view");
}
/**
* {@inheritDoc}
*/
@Override
public BySelector getAppsRowSelector() {
return By.res(getSupportedLauncherPackage(), "list").desc("Apps");
}
/**
* {@inheritDoc}
*/
@Override
public BySelector getGamesRowSelector() {
return By.res(getSupportedLauncherPackage(), "list").desc("Games");
}
/**
* {@inheritDoc}
*/
@Override
public BySelector getSettingsRowSelector() {
return By.res(getSupportedLauncherPackage(), "list").desc("").hasDescendant(
By.res(getSupportedLauncherPackage(), "icon"), 3);
}
/**
* {@inheritDoc}
*/
@Override
public BySelector getAppWidgetSelector() {
return By.clazz(getSupportedLauncherPackage(), "android.appwidget.AppWidgetHostView");
}
/**
* {@inheritDoc}
*/
@Override
public BySelector getNowPlayingCardSelector() {
return By.res(getSupportedLauncherPackage(), "content_text").text("Now Playing");
}
/**
* {@inheritDoc}
*/
@Override
public Direction getAllAppsScrollDirection() {
return Direction.RIGHT;
}
/**
* {@inheritDoc}
*/
@Override
public BySelector getAllAppsSelector() {
// On Leanback launcher the Apps row corresponds to the All Apps on phone UI
return getAppsRowSelector();
}
/**
* {@inheritDoc}
*/
@Override
public long launch(String appName, String packageName) {
BySelector app = By.res(getSupportedLauncherPackage(), "app_banner").desc(appName);
return launchApp(this, app, packageName);
}
/**
* {@inheritDoc}
*/
@Override
public void search(String query) {
if (selectSearchRow() == null) {
throw new RuntimeException("Could not find search row.");
}
BySelector keyboardOrb = By.res(getSupportedLauncherPackage(), "keyboard_orb");
UiObject2 orbButton = mDevice.wait(Until.findObject(keyboardOrb), SHORT_WAIT_TIME);
if (orbButton == null) {
throw new RuntimeException("Could not find keyboard orb.");
}
if (orbButton.isFocused()) {
mDPadUtil.pressDPadCenter();
} else {
// Move the focus to keyboard orb by DPad button.
mDPadUtil.pressDPadRight();
if (orbButton.isFocused()) {
mDPadUtil.pressDPadCenter();
}
}
mDevice.wait(Until.gone(keyboardOrb), SHORT_WAIT_TIME);
BySelector searchEditor = By.res(PACKAGE_SEARCH, "search_text_editor");
UiObject2 editText = mDevice.wait(Until.findObject(searchEditor), SHORT_WAIT_TIME);
if (editText == null) {
throw new RuntimeException("Could not find search text input.");
}
editText.setText(query);
SystemClock.sleep(SHORT_WAIT_TIME);
// Note that Enter key is pressed instead of DPad keys to dismiss leanback IME
mDPadUtil.pressEnter();
mDevice.wait(Until.gone(searchEditor), SHORT_WAIT_TIME);
}
/**
* {@inheritDoc}
*
* Assume that the rows are sorted in the following order from the top:
* Search, Notification(, Partner), Apps, Games, Settings(, and Inputs)
*/
@Override
public UiObject2 selectNotificationRow() {
if (!isNotificationRowSelected()) {
open();
mDPadUtil.pressHome(); // Home key to move to the first card in the Notification row
}
return mDevice.wait(Until.findObject(
getNotificationRowSelector().hasDescendant(By.focused(true), 3)), SHORT_WAIT_TIME);
}
/**
* {@inheritDoc}
*/
@Override
public UiObject2 selectSearchRow() {
if (!isSearchRowSelected()) {
selectNotificationRow();
mDPadUtil.pressDPadUp();
}
return mDevice.wait(Until.findObject(
getSearchRowSelector().hasDescendant(By.focused(true))), SHORT_WAIT_TIME);
}
/**
* {@inheritDoc}
*/
@Override
public UiObject2 selectAppsRow() {
// Start finding Apps row from Notification row
return findRow(getAppsRowSelector());
}
/**
* {@inheritDoc}
*/
@Override
public UiObject2 selectGamesRow() {
return findRow(getGamesRowSelector());
}
/**
* {@inheritDoc}
*/
@Override
public UiObject2 selectSettingsRow() {
// Assume that the Settings row is at the lowest bottom
UiObject2 settings = findRow(getSettingsRowSelector(), Direction.DOWN);
if (settings != null && isSettingsRowSelected()) {
return settings;
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasAppWidgetSelector() {
return mDevice.wait(Until.hasObject(getAppWidgetSelector()), SHORT_WAIT_TIME);
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasNowPlayingCard() {
return mDevice.wait(Until.hasObject(getNowPlayingCardSelector()), SHORT_WAIT_TIME);
}
@SuppressWarnings("unused")
@Override
public BySelector getAllAppsButtonSelector() {
throw new UnsupportedOperationException(
"The 'All Apps' button is not available on Leanback Launcher.");
}
@SuppressWarnings("unused")
@Override
public UiObject2 openAllWidgets(boolean reset) {
throw new UnsupportedOperationException(
"All Widgets is not available on Leanback Launcher.");
}
@SuppressWarnings("unused")
@Override
public BySelector getAllWidgetsSelector() {
throw new UnsupportedOperationException(
"All Widgets is not available on Leanback Launcher.");
}
@SuppressWarnings("unused")
@Override
public Direction getAllWidgetsScrollDirection() {
throw new UnsupportedOperationException(
"All Widgets is not available on Leanback Launcher.");
}
@SuppressWarnings("unused")
@Override
public BySelector getHotSeatSelector() {
throw new UnsupportedOperationException(
"Hot Seat is not available on Leanback Launcher.");
}
@SuppressWarnings("unused")
@Override
public Direction getWorkspaceScrollDirection() {
throw new UnsupportedOperationException(
"Workspace is not available on Leanback Launcher.");
}
protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
String packageName) {
return launchApp(launcherStrategy, app, packageName, MAX_SCROLL_ATTEMPTS);
}
protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
String packageName, int maxScrollAttempts) {
unlockDeviceIfAsleep();
if (isAppOpen(packageName)) {
// Application is already open
return 0;
}
// Go to the home page
launcherStrategy.open();
// attempt to find the app icon if it's not already on the screen
UiObject2 container = launcherStrategy.openAllApps(false);
UiObject2 appIcon = container.findObject(app);
int attempts = 0;
while (attempts++ < maxScrollAttempts) {
// Compare the focused icon and the app icon to search for.
UiObject2 focusedIcon = container.findObject(By.focused(true))
.findObject(By.res(getSupportedLauncherPackage(), "app_banner"));
if (appIcon == null) {
appIcon = findApp(container, focusedIcon, app);
if (appIcon == null) {
throw new RuntimeException("Failed to find the app icon on screen: "
+ packageName);
}
continue;
} else if (focusedIcon.equals(appIcon)) {
// The app icon is on the screen, and selected.
break;
} else {
// The app icon is on the screen, but not selected yet
// Move one step closer to the app icon
Point currentPosition = focusedIcon.getVisibleCenter();
Point targetPosition = appIcon.getVisibleCenter();
int dx = targetPosition.x - currentPosition.x;
int dy = targetPosition.y - currentPosition.y;
final int MARGIN = 10;
// The sequence of moving should be kept in the following order so as not to
// be stuck in case that the apps row are not even.
if (dx < -MARGIN) {
mDPadUtil.pressDPadLeft();
continue;
}
if (dy < -MARGIN) {
mDPadUtil.pressDPadUp();
continue;
}
if (dx > MARGIN) {
mDPadUtil.pressDPadRight();
continue;
}
if (dy > MARGIN) {
mDPadUtil.pressDPadDown();
continue;
}
throw new RuntimeException(
"Failed to navigate to the app icon on screen: " + packageName);
}
}
if (attempts == maxScrollAttempts) {
throw new RuntimeException(
"scrollBackToBeginning: exceeded max attempts: " + maxScrollAttempts);
}
// The app icon is already found and focused.
long ready = SystemClock.uptimeMillis();
mDPadUtil.pressDPadCenter();
if (!mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT)) {
Log.w(LOG_TAG, "no new window detected after app launch attempt.");
return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
}
mDevice.waitForIdle();
if (packageName != null) {
Log.w(LOG_TAG, String.format(
"No UI element with package name %s detected.", packageName));
boolean success = mDevice.wait(Until.hasObject(
By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT);
if (success) {
return ready;
} else {
return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
}
} else {
return ready;
}
}
/**
* Launch the named notification
*
* @param appName - the name of the application to launch in the Notification row
* @return true if application is verified to be in foreground after launch; false otherwise.
*/
public boolean launchNotification(String appName) {
// Wait until notification content is loaded
long currentTimeMs = System.currentTimeMillis();
while (isNotificationPreparing() &&
(System.currentTimeMillis() - currentTimeMs > NOTIFICATION_WAIT_TIME)) {
Log.d(LOG_TAG, "Preparing recommendation...");
SystemClock.sleep(SHORT_WAIT_TIME);
}
// Find a Notification that matches a given app name
UiObject2 card = findNotificationCard(
By.res(getSupportedLauncherPackage(), "card").descContains(appName));
if (card == null) {
throw new IllegalStateException(
String.format("The Notification that matches %s not found", appName));
}
Log.d(LOG_TAG,
String.format("The application %s found in the Notification row. [content_desc]%s",
appName, card.getContentDescription()));
// Click and wait until the Notification card opens
return mDPadUtil.pressDPadCenterAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT);
}
protected boolean isSearchRowSelected() {
UiObject2 row = mDevice.findObject(getSearchRowSelector());
if (row == null) {
return false;
}
return row.hasObject(By.focused(true));
}
protected boolean isAppsRowSelected() {
UiObject2 row = mDevice.findObject(getAppsRowSelector());
if (row == null) {
return false;
}
return row.hasObject(By.focused(true));
}
protected boolean isGamesRowSelected() {
UiObject2 row = mDevice.findObject(getGamesRowSelector());
if (row == null) {
return false;
}
return row.hasObject(By.focused(true));
}
protected boolean isNotificationRowSelected() {
UiObject2 row = mDevice.findObject(getNotificationRowSelector());
if (row == null) {
return false;
}
return row.hasObject(By.focused(true));
}
protected boolean isSettingsRowSelected() {
// Settings label is only visible if the settings row is selected
UiObject2 row = mDevice.findObject(getSettingsRowSelector());
return (row != null && row.hasObject(
By.res(getSupportedLauncherPackage(), "label").text("Settings")));
}
protected boolean isAppOpen (String appPackage) {
return mDevice.hasObject(By.pkg(appPackage).depth(0));
}
protected void unlockDeviceIfAsleep () {
// Turn screen on if necessary
try {
if (!mDevice.isScreenOn()) {
mDevice.wakeUp();
}
} catch (RemoteException e) {
Log.e(LOG_TAG, "Failed to unlock the screen-off device.", e);
}
}
protected boolean isNotificationPreparing() {
// Ensure that the Notification row is visible on screen
if (!mDevice.hasObject(getNotificationRowSelector())) {
selectNotificationRow();
}
return mDevice.hasObject(By.res(getSupportedLauncherPackage(), "notification_preparing"));
}
protected UiObject2 findNotificationCard(BySelector selector) {
// Move to the first notification, Search to the right
mDPadUtil.pressHome();
// Find if a focused card matches a given selector
UiObject2 currentFocus = mDevice.findObject(getNotificationRowSelector())
.findObject(By.res(getSupportedLauncherPackage(), "card").focused(true));
UiObject2 previousFocus = null;
while (!currentFocus.equals(previousFocus)) {
if (currentFocus.hasObject(selector)) {
return currentFocus; // Found
}
mDPadUtil.pressDPadRight();
previousFocus = currentFocus;
currentFocus = mDevice.findObject(getNotificationRowSelector())
.findObject(By.res(getSupportedLauncherPackage(), "card").focused(true));
}
Log.d(LOG_TAG, "Failed to find the Notification card until it reaches the end.");
return null;
}
protected UiObject2 findApp(UiObject2 container, UiObject2 focusedIcon, BySelector app) {
UiObject2 appIcon;
// The app icon is not on the screen.
// Search by going left first until it finds the app icon on the screen
String prevText = focusedIcon.getContentDescription();
String nextText;
do {
mDPadUtil.pressDPadLeft();
appIcon = container.findObject(app);
if (appIcon != null) {
return appIcon;
}
nextText = container.findObject(By.focused(true)).findObject(
By.res(getSupportedLauncherPackage(),
"app_banner")).getContentDescription();
} while (nextText != null && !nextText.equals(prevText));
// If we haven't found it yet, search by going right
do {
mDPadUtil.pressDPadRight();
appIcon = container.findObject(app);
if (appIcon != null) {
return appIcon;
}
nextText = container.findObject(By.focused(true)).findObject(
By.res(getSupportedLauncherPackage(),
"app_banner")).getContentDescription();
} while (nextText != null && !nextText.equals(prevText));
return null;
}
/**
* Find the focused row that matches BySelector in a given direction.
* If the row is already selected, it returns regardless of the direction parameter.
* @param row
* @param direction
* @return
*/
protected UiObject2 findRow(BySelector row, Direction direction) {
if (direction != Direction.DOWN && direction != Direction.UP) {
throw new IllegalArgumentException("Required to go either up or down to find rows");
}
UiObject2 currentFocused = mDevice.findObject(By.focused(true));
UiObject2 prevFocused = null;
while (!currentFocused.equals(prevFocused)) {
UiObject2 rowObject = mDevice.findObject(row);
if (rowObject != null && rowObject.hasObject(By.focused(true))) {
return rowObject; // Found
}
mDPadUtil.pressDPad(direction);
prevFocused = currentFocused;
currentFocused = mDevice.findObject(By.focused(true));
}
Log.d(LOG_TAG, "Failed to find the row until it reaches the end.");
return null;
}
protected UiObject2 findRow(BySelector row) {
UiObject2 rowObject;
// Search by going down first until it finds the focused row.
if ((rowObject = findRow(row, Direction.DOWN)) != null) {
return rowObject;
}
// If we haven't found it yet, search by going up
if ((rowObject = findRow(row, Direction.UP)) != null) {
return rowObject;
}
return null;
}
public void selectRestrictedProfile() {
UiObject2 button = findSettingInRow(
By.res(getSupportedLauncherPackage(), "label").text("Restricted Profile"),
Direction.RIGHT);
if (button == null) {
throw new IllegalStateException("Restricted Profile not found on launcher");
}
mDPadUtil.pressDPadCenterAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT);
}
protected UiObject2 findSettingInRow(BySelector selector, Direction direction) {
if (direction != Direction.RIGHT && direction != Direction.LEFT) {
throw new IllegalArgumentException("Either left or right is allowed");
}
if (!isSettingsRowSelected()) {
selectSettingsRow();
}
UiObject2 setting;
UiObject2 currentFocused = mDevice.findObject(By.focused(true));
UiObject2 prevFocused = null;
while (!currentFocused.equals(prevFocused)) {
if ((setting = currentFocused.findObject(selector)) != null) {
return setting;
}
mDPadUtil.pressDPad(direction);
mDevice.waitForIdle();
prevFocused = currentFocused;
currentFocused = mDevice.findObject(By.focused(true));
}
Log.d(LOG_TAG, "Failed to find the setting in Settings row.");
return null;
}
}