Home | History | Annotate | Download | only in helpers
      1 /*
      2  * Copyright (C) 2016 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 
     17 package android.platform.test.helpers;
     18 
     19 import android.app.Instrumentation;
     20 import android.platform.test.helpers.exceptions.UiTimeoutException;
     21 import android.platform.test.helpers.exceptions.UnknownUiException;
     22 import android.platform.test.utils.DPadUtil;
     23 import android.support.test.launcherhelper.ILeanbackLauncherStrategy;
     24 import android.support.test.launcherhelper.LauncherStrategyFactory;
     25 import android.support.test.uiautomator.By;
     26 import android.support.test.uiautomator.BySelector;
     27 import android.support.test.uiautomator.Direction;
     28 import android.support.test.uiautomator.UiObject2;
     29 import android.support.test.uiautomator.Until;
     30 import android.util.Log;
     31 
     32 /**
     33  *  This app helper handles the following important widgets for TV apps:
     34  *  BrowseFragment, DetailsFragment, SearchFragment and PlaybackOverlayFragment
     35  */
     36 public abstract class AbstractLeanbackAppHelper extends AbstractStandardAppHelper {
     37 
     38     private static final String TAG = AbstractLeanbackAppHelper.class.getSimpleName();
     39     private static final long OPEN_ROW_CONTENT_WAIT_TIME_MS = 5000;
     40     private static final long OPEN_HEADER_WAIT_TIME_MS = 5000;
     41     private static final int OPEN_SIDE_PANEL_MAX_ATTEMPTS = 5;
     42     private static final long MAIN_ACTIVITY_WAIT_TIME_MS = 250;
     43     private static final long SELECT_WAIT_TIME_MS = 5000;
     44 
     45     // The notable widget classes in Leanback Library
     46     public enum Widget {
     47         BROWSE_HEADERS_FRAGMENT,
     48         BROWSE_ROWS_FRAGMENT,
     49         DETAILS_FRAGMENT,
     50         SEARCH_FRAGMENT,
     51         VERTICAL_GRID_FRAGMENT,
     52         GUIDED_STEP_FRAGMENT,
     53         PLAYBACK_OVERLAY_FRAGMENT,
     54         ERROR_FRAGMENT
     55     }
     56 
     57     protected DPadUtil mDPadUtil;
     58     public ILeanbackLauncherStrategy mLauncherStrategy;
     59 
     60 
     61     public AbstractLeanbackAppHelper(Instrumentation instr) {
     62         super(instr);
     63         mDPadUtil = new DPadUtil(instr);
     64         mLauncherStrategy = LauncherStrategyFactory.getInstance(
     65                 mDevice).getLeanbackLauncherStrategy();
     66     }
     67 
     68     /**
     69      * @return {@link BySelector} describing the row headers (in the left pane) in
     70      * the Browse fragment
     71      */
     72     protected BySelector getBrowseHeadersSelector() {
     73         return By.res(getPackage(), "browse_headers").hasChild(By.selected(true));
     74     }
     75 
     76     /**
     77      * @return {@link BySelector} describing a row content (in the right pane) selected in
     78      * the Browse fragment
     79      */
     80     protected BySelector getBrowseRowsSelector() {
     81         return By.res(getPackage(), "row_content").hasChild(By.selected(true));
     82     }
     83 
     84     /**
     85      * @return {@link BySelector} describing the Details fragment
     86      */
     87     protected BySelector getDetailsFragmentSelector() {
     88         return By.res(getPackage(), "details_fragment");
     89     }
     90 
     91     /**
     92      * @return {@link BySelector} describing the Search fragment
     93      */
     94     protected BySelector getSearchFragmentSelector() {
     95         return By.res(getPackage(), "lb_search_frame");
     96     }
     97 
     98     /**
     99      * @return {@link BySelector} describing the Vertical grid fragment
    100      */
    101     protected BySelector getVerticalGridFragmentSelector() {
    102         return By.res(getPackage(), "grid_frame");
    103     }
    104 
    105     /**
    106      * @return {@link BySelector} describing the Guided step fragment
    107      */
    108     protected BySelector getGuidedStepFragmentSelector() {
    109         return By.res(getPackage(), "guidedactions_list");
    110     }
    111 
    112     /**
    113      * @return {@link BySelector} describing the Playback overlay fragment
    114      */
    115     protected BySelector getPlaybackOverlayFragmentSelector() {
    116         return By.res(getPackage(), "playback_controls_dock");
    117     }
    118 
    119     /**
    120      * @return {@link BySelector} describing the Error fragment
    121      */
    122     protected BySelector getErrorFragmentSelector() {
    123         return By.res(getPackage(), "error_frame");
    124     }
    125 
    126     /**
    127      * @return {@link BySelector} describing the main activity (mostly the Browse fragment).
    128      * Note that not every application has its main activity, so the override is optional.
    129      */
    130     protected BySelector getMainActivitySelector() {
    131         return null;
    132     }
    133 
    134     // TODO Move waitForOpen and open to AbstractStandardAppHelper
    135     /**
    136      * Setup expectation: None. Waits for the application to begin running.
    137      * @param timeoutMs
    138      * @return true if the application is open successfully
    139      */
    140     public boolean waitForOpen(long timeoutMs) {
    141         return mDevice.wait(Until.hasObject(By.pkg(getPackage()).depth(0)), timeoutMs);
    142     }
    143 
    144     /**
    145      * Setup expectation: On the launcher home screen.
    146      * <p>
    147      * Launches the desired application and wait for it to begin running before returning.
    148      * </p>
    149      * @param timeoutMs
    150      */
    151     public void open(long timeoutMs) {
    152         open();
    153         if (!waitForOpen(timeoutMs)) {
    154             throw new UiTimeoutException(String.format("Timed out to open a target package %s:"
    155                     + " %d(ms)", getPackage(), timeoutMs));
    156         }
    157     }
    158 
    159     /**
    160      * Setup expectation: Side panel is selected on the Browse fragment
    161      * <p>
    162      * Best effort attempt to go to the row headers, and open the selected header.
    163      * </p>
    164      */
    165     public void openHeader(String headerName) {
    166         openBrowseHeaders();
    167         // header is focused; it should not be after pressing the DPad
    168         selectHeader(headerName);
    169         mDevice.pressDPadCenter();
    170 
    171         // Test for focus change and selection result
    172         BySelector rowContent = getBrowseRowsSelector();
    173         if (!mDevice.wait(Until.hasObject(rowContent), OPEN_ROW_CONTENT_WAIT_TIME_MS)) {
    174             throw new UnknownUiException(
    175                     String.format("Failed to find row content that matches the header: %s",
    176                             headerName));
    177         }
    178         Log.v(TAG, "Successfully opened header");
    179     }
    180 
    181     /**
    182      * Setup expectation: On navigation screen on the Browse fragment
    183      *
    184      * Best effort attempt to open the row headers in the Browse fragment.
    185      * @param onMainActivity True if it opens the side panel on app's main activity.
    186      */
    187     public void openBrowseHeaders(boolean onMainActivity) {
    188         if (onMainActivity) {
    189             returnToMainActivity();
    190         }
    191         int attempts = 0;
    192         while (!waitForBrowseHeadersSelected(OPEN_HEADER_WAIT_TIME_MS)
    193                 && attempts++ < OPEN_SIDE_PANEL_MAX_ATTEMPTS) {
    194             mDevice.pressDPadLeft();
    195         }
    196         if (attempts == OPEN_SIDE_PANEL_MAX_ATTEMPTS) {
    197             throw new UnknownUiException("Failed to open side panel");
    198         }
    199     }
    200 
    201     public void openBrowseHeaders() {
    202         openBrowseHeaders(false);
    203     }
    204 
    205     /**
    206      * Select target item through the container in the given direction.
    207      * @param container
    208      * @param target
    209      * @param direction
    210      * @return the focused object
    211      */
    212     public UiObject2 select(UiObject2 container, BySelector target, Direction direction) {
    213         if (container == null) {
    214             throw new IllegalArgumentException("The container should not be null.");
    215         }
    216         UiObject2 focus = container.findObject(By.focused(true));
    217         if (focus == null) {
    218             throw new UnknownUiException("The container should have a focused descendant.");
    219         }
    220         while (!focus.hasObject(target)) {
    221             UiObject2 prev = focus;
    222             mDPadUtil.pressDPad(direction);
    223             focus = container.findObject(By.focused(true));
    224             if (focus == null) {
    225                 mDPadUtil.pressDPad(Direction.reverse(direction));
    226                 focus = container.findObject(By.focused(true));
    227             }
    228             if (focus.equals(prev)) {
    229                 // It reached at the end, but no target is found.
    230                 return null;
    231             }
    232         }
    233         return focus;
    234     }
    235 
    236     /**
    237      * Setup expectation: On guided fragment.
    238      * <p>
    239      * Best effort attempt to select a given guided action.
    240      * </p>
    241      */
    242     public UiObject2 selectGuidedAction(String action) {
    243         assertWidgetEquals(Widget.GUIDED_STEP_FRAGMENT);
    244         UiObject2 container = mDevice.wait(
    245                 Until.findObject(
    246                         By.res(getPackage(), "guidedactions_list").hasChild(By.focused(true))),
    247                 SELECT_WAIT_TIME_MS);
    248         // Search down, then up
    249         BySelector selector = By.res(getPackage(), "guidedactions_item_title").text(action);
    250         UiObject2 focused = select(container, selector, Direction.DOWN);
    251         if (focused != null) {
    252             return focused;
    253         }
    254         focused = select(container, selector, Direction.UP);
    255         if (focused != null) {
    256             return focused;
    257         }
    258         throw new UnknownUiException(String.format("Failed to select guided action: %s", action));
    259     }
    260 
    261     /**
    262      * Setup expectation: On guided fragment. Return the string in guidance title.
    263      */
    264     public String getGuidanceTitleText() {
    265         assertWidgetEquals(Widget.GUIDED_STEP_FRAGMENT);
    266         UiObject2 object = mDevice.wait(
    267                 Until.findObject(By.res(getPackage(), "guidance_title")), SELECT_WAIT_TIME_MS);
    268         return object.getText();
    269     }
    270 
    271     /**
    272      * Setup expectation: On row fragment.
    273      * @param title of the card
    274      * @return UIObject2 for the focusable card that matches a given name in title
    275      */
    276     private UiObject2 getCardInRowByTitle(String title) {
    277         assertWidgetEquals(Widget.BROWSE_ROWS_FRAGMENT);
    278         return mDevice.wait(Until.findObject(
    279                 By.focused(true).hasDescendant(By.res(getPackage(), "title_text").text(title))),
    280                 SELECT_WAIT_TIME_MS);
    281     }
    282 
    283     /**
    284      * Setup expectation: On row fragment.
    285      * @param title of the card
    286      * @return String text of content in a card that has a given name in title
    287      */
    288     public String getCardContentText(String title) {
    289         UiObject2 card = getCardInRowByTitle(title);
    290         if (card == null) {
    291             throw new IllegalStateException("Failed to find a card in row content " + title);
    292         }
    293         return card.findObject(By.res(getPackage(), "content_text")).getText();
    294     }
    295 
    296     /**
    297      * Setup expectation: On row fragment.
    298      * @param title of the card
    299      * @return true if it finds a card that matches a given name in title
    300      */
    301     public boolean hasCardInRow(String title) {
    302         return (getCardInRowByTitle(title) != null);
    303     }
    304 
    305     /**
    306      * Setup expectation: On row fragment.
    307      * <p>
    308      * Open a card that matches a given title in row content
    309      * </p>
    310      * @param title of the card
    311      */
    312     public void openCardInRow(String title) {
    313         assertWidgetEquals(Widget.BROWSE_ROWS_FRAGMENT);
    314         UiObject2 card = getCardInRowByTitle(title);
    315         if (card == null) {
    316             throw new IllegalStateException("Failed to find a card in row content " + title);
    317         }
    318         if (!card.isFocused()) {
    319             card.click();   // move a focus
    320             card = getCardInRowByTitle(title);
    321             if (card == null) {
    322                 throw new IllegalStateException("Failed to find a card in row content " + title);
    323             }
    324         }
    325         mDPadUtil.pressDPadCenter();
    326         mDevice.wait(Until.gone(By.res(getPackage(), "title_text").text(title)),
    327                 SELECT_WAIT_TIME_MS);
    328     }
    329 
    330     /**
    331      * Attempts to return to main activity with getMainActivitySelector()
    332      * by pressing the back button repeatedly and sleeping briefly to allow for UI slowness.
    333      */
    334     public void returnToMainActivity() {
    335         int maxBackAttempts = 10;
    336         BySelector selector = getMainActivitySelector();
    337         if (selector == null) {
    338             throw new IllegalStateException("getMainActivitySelector() should be overridden.");
    339         }
    340         while (!mDevice.wait(Until.hasObject(selector), MAIN_ACTIVITY_WAIT_TIME_MS)
    341                 && maxBackAttempts-- > 0) {
    342             mDevice.pressBack();
    343         }
    344     }
    345 
    346     /**
    347      * Setup expectation: None.
    348      * <p>
    349      * Asserts that a given widget provided by the Support Library is shown on TV app.
    350      * </p>
    351      */
    352     public void assertWidgetEquals(Widget expected) {
    353         if (!hasWidget(expected)) {
    354             throw new UnknownUiException("No widget matches " + expected.name());
    355         }
    356     }
    357 
    358     private boolean hasWidget(Widget expected) {
    359         switch (expected) {
    360             case BROWSE_HEADERS_FRAGMENT:
    361                 return mDevice.hasObject(getBrowseHeadersSelector());
    362             case BROWSE_ROWS_FRAGMENT:
    363                 return mDevice.hasObject(getBrowseRowsSelector());
    364             case DETAILS_FRAGMENT:
    365                 return mDevice.hasObject(getDetailsFragmentSelector());
    366             case SEARCH_FRAGMENT:
    367                 return mDevice.hasObject(getSearchFragmentSelector());
    368             case VERTICAL_GRID_FRAGMENT:
    369                 return mDevice.hasObject(getVerticalGridFragmentSelector());
    370             case GUIDED_STEP_FRAGMENT:
    371                 return mDevice.hasObject(getGuidedStepFragmentSelector());
    372             case PLAYBACK_OVERLAY_FRAGMENT:
    373                 return mDevice.hasObject(getPlaybackOverlayFragmentSelector());
    374             case ERROR_FRAGMENT:
    375                 return mDevice.hasObject(getErrorFragmentSelector());
    376             default:
    377                 Log.w(TAG, "Unable to find the widget in the list: " + expected.name());
    378                 return false;
    379         }
    380     }
    381 
    382     @Override
    383     public void dismissInitialDialogs() {
    384         return;
    385     }
    386 
    387     private boolean waitForBrowseHeadersSelected(long timeoutMs) {
    388         return mDevice.wait(Until.hasObject(getBrowseHeadersSelector()), timeoutMs);
    389     }
    390 
    391     protected UiObject2 selectHeader(String headerName) {
    392         UiObject2 container = mDevice.wait(
    393                 Until.findObject(getBrowseHeadersSelector()), OPEN_HEADER_WAIT_TIME_MS);
    394         BySelector header = By.clazz(".TextView").text(headerName);
    395 
    396         // Wait until the row header text appears at runtime. This needs to be long enough to run
    397         // under low bandwidth environments in the test lab.
    398         mDevice.wait(Until.findObject(header), 60 * 1000);
    399 
    400         // Search up, then down
    401         UiObject2 focused = select(container, header, Direction.UP);
    402         if (focused != null) {
    403             return focused;
    404         }
    405         focused = select(container, header, Direction.DOWN);
    406         if (focused != null) {
    407             return focused;
    408         }
    409         throw new UnknownUiException("Failed to select header");
    410     }
    411 }
    412