blob: f8ea94f44192f467b4cc600b95e2ecc98113bcda [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.platform.test.helpers;
import android.app.Instrumentation;
import android.platform.test.helpers.exceptions.UiTimeoutException;
import android.platform.test.helpers.exceptions.UnknownUiException;
import android.platform.test.utils.DPadUtil;
import android.support.test.launcherhelper.ILeanbackLauncherStrategy;
import android.support.test.launcherhelper.LauncherStrategyFactory;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.Direction;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;
import android.util.Log;
/**
* This app helper handles the following important widgets for TV apps:
* BrowseFragment, DetailsFragment, SearchFragment and PlaybackOverlayFragment
*/
public abstract class AbstractLeanbackAppHelper extends AbstractStandardAppHelper {
private static final String TAG = AbstractLeanbackAppHelper.class.getSimpleName();
private static final long OPEN_ROW_CONTENT_WAIT_TIME_MS = 5000;
private static final long OPEN_HEADER_WAIT_TIME_MS = 5000;
private static final int OPEN_SIDE_PANEL_MAX_ATTEMPTS = 5;
private static final long MAIN_ACTIVITY_WAIT_TIME_MS = 250;
private static final long SELECT_WAIT_TIME_MS = 5000;
// The notable widget classes in Leanback Library
public enum Widget {
BROWSE_HEADERS_FRAGMENT,
BROWSE_ROWS_FRAGMENT,
DETAILS_FRAGMENT,
SEARCH_FRAGMENT,
VERTICAL_GRID_FRAGMENT,
GUIDED_STEP_FRAGMENT,
PLAYBACK_OVERLAY_FRAGMENT,
ERROR_FRAGMENT
}
protected DPadUtil mDPadUtil;
public ILeanbackLauncherStrategy mLauncherStrategy;
public AbstractLeanbackAppHelper(Instrumentation instr) {
super(instr);
mDPadUtil = new DPadUtil(instr);
mLauncherStrategy = LauncherStrategyFactory.getInstance(
mDevice).getLeanbackLauncherStrategy();
}
/**
* @return {@link BySelector} describing the row headers (in the left pane) in
* the Browse fragment
*/
protected BySelector getBrowseHeadersSelector() {
return By.res(getPackage(), "browse_headers").hasChild(By.selected(true));
}
/**
* @return {@link BySelector} describing a row content (in the right pane) selected in
* the Browse fragment
*/
protected BySelector getBrowseRowsSelector() {
return By.res(getPackage(), "row_content").hasChild(By.selected(true));
}
/**
* @return {@link BySelector} describing the Details fragment
*/
protected BySelector getDetailsFragmentSelector() {
return By.res(getPackage(), "details_fragment");
}
/**
* @return {@link BySelector} describing the Search fragment
*/
protected BySelector getSearchFragmentSelector() {
return By.res(getPackage(), "lb_search_frame");
}
/**
* @return {@link BySelector} describing the Vertical grid fragment
*/
protected BySelector getVerticalGridFragmentSelector() {
return By.res(getPackage(), "grid_frame");
}
/**
* @return {@link BySelector} describing the Guided step fragment
*/
protected BySelector getGuidedStepFragmentSelector() {
return By.res(getPackage(), "guidedactions_list");
}
/**
* @return {@link BySelector} describing the Playback overlay fragment
*/
protected BySelector getPlaybackOverlayFragmentSelector() {
return By.res(getPackage(), "playback_controls_dock");
}
/**
* @return {@link BySelector} describing the Error fragment
*/
protected BySelector getErrorFragmentSelector() {
return By.res(getPackage(), "error_frame");
}
/**
* @return {@link BySelector} describing the main activity (mostly the Browse fragment).
* Note that not every application has its main activity, so the override is optional.
*/
protected BySelector getMainActivitySelector() {
return null;
}
// TODO Move waitForOpen and open to AbstractStandardAppHelper
/**
* Setup expectation: None. Waits for the application to begin running.
* @param timeoutMs
* @return true if the application is open successfully
*/
public boolean waitForOpen(long timeoutMs) {
return mDevice.wait(Until.hasObject(By.pkg(getPackage()).depth(0)), timeoutMs);
}
/**
* Setup expectation: On the launcher home screen.
* <p>
* Launches the desired application and wait for it to begin running before returning.
* </p>
* @param timeoutMs
*/
public void open(long timeoutMs) {
open();
if (!waitForOpen(timeoutMs)) {
throw new UiTimeoutException(String.format("Timed out to open a target package %s:"
+ " %d(ms)", getPackage(), timeoutMs));
}
}
/**
* Setup expectation: Side panel is selected on the Browse fragment
* <p>
* Best effort attempt to go to the row headers, and open the selected header.
* </p>
*/
public void openHeader(String headerName) {
openBrowseHeaders();
// header is focused; it should not be after pressing the DPad
selectHeader(headerName);
mDevice.pressDPadCenter();
// Test for focus change and selection result
BySelector rowContent = getBrowseRowsSelector();
if (!mDevice.wait(Until.hasObject(rowContent), OPEN_ROW_CONTENT_WAIT_TIME_MS)) {
throw new UnknownUiException(
String.format("Failed to find row content that matches the header: %s",
headerName));
}
Log.v(TAG, "Successfully opened header");
}
/**
* Setup expectation: On navigation screen on the Browse fragment
*
* Best effort attempt to open the row headers in the Browse fragment.
* @param onMainActivity True if it opens the side panel on app's main activity.
*/
public void openBrowseHeaders(boolean onMainActivity) {
if (onMainActivity) {
returnToMainActivity();
}
int attempts = 0;
while (!waitForBrowseHeadersSelected(OPEN_HEADER_WAIT_TIME_MS)
&& attempts++ < OPEN_SIDE_PANEL_MAX_ATTEMPTS) {
mDevice.pressDPadLeft();
}
if (attempts == OPEN_SIDE_PANEL_MAX_ATTEMPTS) {
throw new UnknownUiException("Failed to open side panel");
}
}
public void openBrowseHeaders() {
openBrowseHeaders(false);
}
/**
* Select target item through the container in the given direction.
* @param container
* @param target
* @param direction
* @return the focused object
*/
public UiObject2 select(UiObject2 container, BySelector target, Direction direction) {
if (container == null) {
throw new IllegalArgumentException("The container should not be null.");
}
UiObject2 focus = container.findObject(By.focused(true));
if (focus == null) {
throw new UnknownUiException("The container should have a focused descendant.");
}
while (!focus.hasObject(target)) {
UiObject2 prev = focus;
mDPadUtil.pressDPad(direction);
focus = container.findObject(By.focused(true));
if (focus == null) {
mDPadUtil.pressDPad(Direction.reverse(direction));
focus = container.findObject(By.focused(true));
}
if (focus.equals(prev)) {
// It reached at the end, but no target is found.
return null;
}
}
return focus;
}
/**
* Setup expectation: On guided fragment.
* <p>
* Best effort attempt to select a given guided action.
* </p>
*/
public UiObject2 selectGuidedAction(String action) {
assertWidgetEquals(Widget.GUIDED_STEP_FRAGMENT);
UiObject2 container = mDevice.wait(
Until.findObject(
By.res(getPackage(), "guidedactions_list").hasChild(By.focused(true))),
SELECT_WAIT_TIME_MS);
// Search down, then up
BySelector selector = By.res(getPackage(), "guidedactions_item_title").text(action);
UiObject2 focused = select(container, selector, Direction.DOWN);
if (focused != null) {
return focused;
}
focused = select(container, selector, Direction.UP);
if (focused != null) {
return focused;
}
throw new UnknownUiException(String.format("Failed to select guided action: %s", action));
}
/**
* Setup expectation: On guided fragment. Return the string in guidance title.
*/
public String getGuidanceTitleText() {
assertWidgetEquals(Widget.GUIDED_STEP_FRAGMENT);
UiObject2 object = mDevice.wait(
Until.findObject(By.res(getPackage(), "guidance_title")), SELECT_WAIT_TIME_MS);
return object.getText();
}
/**
* Setup expectation: On row fragment.
* @param title of the card
* @return UIObject2 for the focusable card that matches a given name in title
*/
private UiObject2 getCardInRowByTitle(String title) {
assertWidgetEquals(Widget.BROWSE_ROWS_FRAGMENT);
return mDevice.wait(Until.findObject(
By.focused(true).hasDescendant(By.res(getPackage(), "title_text").text(title))),
SELECT_WAIT_TIME_MS);
}
/**
* Setup expectation: On row fragment.
* @param title of the card
* @return String text of content in a card that has a given name in title
*/
public String getCardContentText(String title) {
UiObject2 card = getCardInRowByTitle(title);
if (card == null) {
throw new IllegalStateException("Failed to find a card in row content " + title);
}
return card.findObject(By.res(getPackage(), "content_text")).getText();
}
/**
* Setup expectation: On row fragment.
* @param title of the card
* @return true if it finds a card that matches a given name in title
*/
public boolean hasCardInRow(String title) {
return (getCardInRowByTitle(title) != null);
}
/**
* Setup expectation: On row fragment.
* <p>
* Open a card that matches a given title in row content
* </p>
* @param title of the card
*/
public void openCardInRow(String title) {
assertWidgetEquals(Widget.BROWSE_ROWS_FRAGMENT);
UiObject2 card = getCardInRowByTitle(title);
if (card == null) {
throw new IllegalStateException("Failed to find a card in row content " + title);
}
if (!card.isFocused()) {
card.click(); // move a focus
card = getCardInRowByTitle(title);
if (card == null) {
throw new IllegalStateException("Failed to find a card in row content " + title);
}
}
mDPadUtil.pressDPadCenter();
mDevice.wait(Until.gone(By.res(getPackage(), "title_text").text(title)),
SELECT_WAIT_TIME_MS);
}
/**
* Attempts to return to main activity with getMainActivitySelector()
* by pressing the back button repeatedly and sleeping briefly to allow for UI slowness.
*/
public void returnToMainActivity() {
int maxBackAttempts = 10;
BySelector selector = getMainActivitySelector();
if (selector == null) {
throw new IllegalStateException("getMainActivitySelector() should be overridden.");
}
while (!mDevice.wait(Until.hasObject(selector), MAIN_ACTIVITY_WAIT_TIME_MS)
&& maxBackAttempts-- > 0) {
mDevice.pressBack();
}
}
/**
* Setup expectation: None.
* <p>
* Asserts that a given widget provided by the Support Library is shown on TV app.
* </p>
*/
public void assertWidgetEquals(Widget expected) {
if (!hasWidget(expected)) {
throw new UnknownUiException("No widget matches " + expected.name());
}
}
private boolean hasWidget(Widget expected) {
switch (expected) {
case BROWSE_HEADERS_FRAGMENT:
return mDevice.hasObject(getBrowseHeadersSelector());
case BROWSE_ROWS_FRAGMENT:
return mDevice.hasObject(getBrowseRowsSelector());
case DETAILS_FRAGMENT:
return mDevice.hasObject(getDetailsFragmentSelector());
case SEARCH_FRAGMENT:
return mDevice.hasObject(getSearchFragmentSelector());
case VERTICAL_GRID_FRAGMENT:
return mDevice.hasObject(getVerticalGridFragmentSelector());
case GUIDED_STEP_FRAGMENT:
return mDevice.hasObject(getGuidedStepFragmentSelector());
case PLAYBACK_OVERLAY_FRAGMENT:
return mDevice.hasObject(getPlaybackOverlayFragmentSelector());
case ERROR_FRAGMENT:
return mDevice.hasObject(getErrorFragmentSelector());
default:
Log.w(TAG, "Unable to find the widget in the list: " + expected.name());
return false;
}
}
@Override
public void dismissInitialDialogs() {
return;
}
private boolean waitForBrowseHeadersSelected(long timeoutMs) {
return mDevice.wait(Until.hasObject(getBrowseHeadersSelector()), timeoutMs);
}
protected UiObject2 selectHeader(String headerName) {
UiObject2 container = mDevice.wait(
Until.findObject(getBrowseHeadersSelector()), OPEN_HEADER_WAIT_TIME_MS);
BySelector header = By.clazz(".TextView").text(headerName);
// Wait until the row header text appears at runtime. This needs to be long enough to run
// under low bandwidth environments in the test lab.
mDevice.wait(Until.findObject(header), 60 * 1000);
// Search up, then down
UiObject2 focused = select(container, header, Direction.UP);
if (focused != null) {
return focused;
}
focused = select(container, header, Direction.DOWN);
if (focused != null) {
return focused;
}
throw new UnknownUiException("Failed to select header");
}
}