| /* |
| * 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; |
| } |
| } |