TV SysUI functional tests base

This is an initial CL to add functional tests for TV SysUI
Leanback Launcher, Setting, Recents and Multiwindow.
This includes both hermetic and non-hermetic tests for
acceptance testing.
This test suite could be deployed to the testing service for
continuous test runs.

Bug: 30299617
Change-Id: If96334516843e90b1e19b0d7067e3e5e800b449d
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java
index e81bcfe..7d7b1b4 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java
@@ -17,6 +17,7 @@
 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.support.test.launcherhelper.ILeanbackLauncherStrategy;
 import android.support.test.launcherhelper.LauncherStrategyFactory;
@@ -34,10 +35,23 @@
 public abstract class AbstractLeanbackAppHelper extends AbstractStandardAppHelper {
 
     private static final String TAG = AbstractLeanbackAppHelper.class.getSimpleName();
-    private static final long OPEN_SECTION_WAIT_TIME_MS = 5000;
-    private static final long OPEN_SIDE_PANEL_WAIT_TIME_MS = 5000;
+    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 DPadHelper mDPadHelper;
     public ILeanbackLauncherStrategy mLauncherStrategy;
@@ -50,52 +64,131 @@
                 mDevice).getLeanbackLauncherStrategy();
     }
 
-    protected abstract BySelector getAppSelector();
-
-    protected abstract BySelector getSidePanelSelector();
-
-    protected abstract BySelector getSidePanelResultSelector(String sectionName);
+    /**
+     * @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));
+    }
 
     /**
-     * Selector to identify main activity for getMainActivitySelector().
-     * Not every application has its main activity, so the override is optional.
+     * @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: Side panel is selected on browse fragment
-     *
-     * Best effort attempt to go to the side panel, and open the selected section.
+     * Setup expectation: None. Waits for the application to begin running.
+     * @param timeoutMs
+     * @return true if the application is open successfully
      */
-    public void openSection(String sectionName) {
-        openSidePanel();
-        // Section header is focused; it should not be after pressing the DPad
-        selectSection(sectionName);
-        mDevice.pressDPadCenter();
-
-        // Test for focus change and selection result
-        BySelector sectionResult = getSidePanelResultSelector(sectionName);
-        if (!mDevice.wait(Until.hasObject(sectionResult), OPEN_SECTION_WAIT_TIME_MS)) {
-            throw new UnknownUiException(
-                    String.format("Failed to find result opening section %s", sectionName));
-        }
-        Log.v(TAG, "Successfully opened section");
+    public boolean waitForOpen(long timeoutMs) {
+        return mDevice.wait(Until.hasObject(By.pkg(getPackage()).depth(0)), timeoutMs);
     }
 
     /**
-     * Setup expectation: On navigation screen on browse fragment
+     * 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 side panel.
+     * 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 openSidePanel(boolean onMainActivity) {
+    public void openBrowseHeaders(boolean onMainActivity) {
         if (onMainActivity) {
             returnToMainActivity();
         }
         int attempts = 0;
-        while (!isSidePanelSelected(OPEN_SIDE_PANEL_WAIT_TIME_MS)
+        while (!waitForBrowseHeadersSelected(OPEN_HEADER_WAIT_TIME_MS)
                 && attempts++ < OPEN_SIDE_PANEL_MAX_ATTEMPTS) {
             mDevice.pressDPadLeft();
         }
@@ -104,8 +197,8 @@
         }
     }
 
-    public void openSidePanel() {
-        openSidePanel(false);
+    public void openBrowseHeaders() {
+        openBrowseHeaders(false);
     }
 
     /**
@@ -121,7 +214,7 @@
         }
         UiObject2 focus = container.findObject(By.focused(true));
         if (focus == null) {
-            throw new UnknownUiException("The container should have a focus.");
+            throw new UnknownUiException("The container should have a focused descendant.");
         }
         while (!focus.hasObject(target)) {
             UiObject2 prev = focus;
@@ -140,6 +233,100 @@
     }
 
     /**
+     * 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);
+            }
+        }
+        mDPadHelper.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.
      */
@@ -155,38 +342,69 @@
         }
     }
 
+    /**
+     * 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;
     }
 
-    protected boolean isSidePanelSelected(long timeout) {
-        UiObject2 sidePanel = mDevice.wait(Until.findObject(getSidePanelSelector()), timeout);
-        if (sidePanel == null) {
-            return false;
-        }
-        return sidePanel.hasObject(By.focused(true).minDepth(1));
+    private boolean waitForBrowseHeadersSelected(long timeoutMs) {
+        return mDevice.wait(Until.hasObject(getBrowseHeadersSelector()), timeoutMs);
     }
 
-    protected UiObject2 selectSection(String sectionName) {
+    protected UiObject2 selectHeader(String headerName) {
         UiObject2 container = mDevice.wait(
-                Until.findObject(getSidePanelSelector()), OPEN_SIDE_PANEL_WAIT_TIME_MS);
-        BySelector section = By.clazz(".TextView").text(sectionName);
+                Until.findObject(getBrowseHeadersSelector()), OPEN_HEADER_WAIT_TIME_MS);
+        BySelector header = By.clazz(".TextView").text(headerName);
 
-        // Wait until the section text appears at runtime. This needs to be long enough to run under
-        // low bandwidth environments in the test lab.
-        mDevice.wait(Until.findObject(section), 60 * 1000);
+        // 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, section, Direction.UP);
+        UiObject2 focused = select(container, header, Direction.UP);
         if (focused != null) {
             return focused;
         }
-        focused = select(container, section, Direction.DOWN);
+        focused = select(container, header, Direction.DOWN);
         if (focused != null) {
             return focused;
         }
-        throw new UnknownUiException("Failed to select section");
+        throw new UnknownUiException("Failed to select header");
     }
-
 }
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/CommandHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/CommandHelper.java
new file mode 100644
index 0000000..cc4f3df
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/CommandHelper.java
@@ -0,0 +1,74 @@
+/*
+ * 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.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Utility class to execute the shell command.
+ */
+public final class CommandHelper {
+    private static final String TAG = CommandHelper.class.getSimpleName();
+
+    private UiDevice mDevice;
+
+
+    public CommandHelper(Instrumentation instrumentation) {
+        mDevice = UiDevice.getInstance(instrumentation);
+    }
+
+    public void executeAmStackMovetask(int taskId, int stackId) {
+        executeShellCommand(
+                String.format("am stack movetask %d %d true", taskId, stackId));
+    }
+
+    public String executeAmStackInfo(int stackId) {
+        return executeShellCommand(String.format("am stack info %d", stackId));
+    }
+
+    public String executeAmStackList() {
+        return executeShellCommand("am stack list");
+    }
+
+    public String executeDumpsysMediaSession() {
+        return executeShellCommand("dumpsys media_session");
+    }
+
+    public String executeGetProp(String prop) {
+        return executeShellCommand(String.format("getprop %s", prop), true);
+    }
+
+    public String executeShellCommand(String command) {
+        return executeShellCommand(command, false);
+    }
+
+    private String executeShellCommand(String command, boolean trim) {
+        try {
+            String output = mDevice.executeShellCommand(command);
+            return output.trim();
+        } catch (IOException e) {
+            // ignore
+            Log.w(TAG, String.format("The shell command failed to run: %s exception: %s",
+                    command, e.getMessage()));
+            return "";
+        }
+    }
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/DPadHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/DPadHelper.java
index 51dd12b..c3ee0d1 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/DPadHelper.java
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/DPadHelper.java
@@ -19,7 +19,13 @@
 import android.app.Instrumentation;
 import android.os.SystemClock;
 import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.EventCondition;
 import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.io.IOException;
+
 
 public class DPadHelper {
 
@@ -40,8 +46,8 @@
         return mInstance;
     }
 
-    public void pressDPad(Direction direction) {
-        pressDPad(direction, 1, DPAD_DEFAULT_WAIT_TIME_MS);
+    public boolean pressDPad(Direction direction) {
+        return pressDPad(direction, 1, DPAD_DEFAULT_WAIT_TIME_MS);
     }
 
     public void pressDPad(Direction direction, long repeat) {
@@ -55,25 +61,112 @@
      * @param direction the direction of the button to press.
      * @param repeat the number of times to press the button.
      * @param timeout the timeout for the wait.
+     * @return true if the last key simulation is successful, else return false
      */
-    public void pressDPad(Direction direction, long repeat, long timeout) {
+    public boolean pressDPad(Direction direction, long repeat, long timeout) {
         int iteration = 0;
+        boolean result = false;
         while (iteration++ < repeat) {
             switch (direction) {
                 case LEFT:
-                    mDevice.pressDPadLeft();
+                    result = mDevice.pressDPadLeft();
                     break;
                 case RIGHT:
-                    mDevice.pressDPadRight();
+                    result = mDevice.pressDPadRight();
                     break;
                 case UP:
-                    mDevice.pressDPadUp();
+                    result = mDevice.pressDPadUp();
                     break;
                 case DOWN:
-                    mDevice.pressDPadDown();
+                    result = mDevice.pressDPadDown();
                     break;
             }
             SystemClock.sleep(timeout);
         }
+        return result;
+    }
+
+    public boolean pressDPadLeft() {
+        return mDevice.pressDPadLeft();
+    }
+
+    public boolean pressDPadRight() {
+        return mDevice.pressDPadRight();
+    }
+
+    public boolean pressDPadUp() {
+        return mDevice.pressDPadUp();
+    }
+
+    public boolean pressDPadDown() {
+        return mDevice.pressDPadDown();
+    }
+
+    public boolean pressHome() {
+        return mDevice.pressHome();
+    }
+
+    public boolean pressBack() {
+        return mDevice.pressBack();
+    }
+
+    public boolean pressDPadCenter() {
+        return mDevice.pressDPadCenter();
+    }
+
+    public boolean pressEnter() {
+        return mDevice.pressEnter();
+    }
+
+    public boolean pressPipKey() {
+        return mDevice.pressKeyCode(KeyEvent.KEYCODE_WINDOW);
+    }
+
+    public boolean pressKeyCode(int keyCode) {
+        return mDevice.pressKeyCode(keyCode);
+    }
+
+    public boolean longPressKeyCode(int keyCode) {
+        try {
+            mDevice.executeShellCommand(String.format("input keyevent --longpress %d", keyCode));
+            return true;
+        } catch (IOException e) {
+            // Ignore
+            Log.w(TAG, String.format("Failed to long press the key code: %d", keyCode));
+            return false;
+        }
+    }
+
+    /**
+     * Press the key code, and waits for the given condition to become true.
+     * @param condition
+     * @param keyCode
+     * @param timeout
+     * @param <R>
+     * @return
+     */
+    public <R> R pressKeyCodeAndWait(int keyCode, EventCondition<R> condition, long timeout) {
+        return mDevice.performActionAndWait(new KeyEventRunnable(keyCode), condition, timeout);
+    }
+
+    public <R> R pressDPadCenterAndWait(EventCondition<R> condition, long timeout) {
+        return mDevice.performActionAndWait(new KeyEventRunnable(KeyEvent.KEYCODE_DPAD_CENTER),
+                condition, timeout);
+    }
+
+    public <R> R pressEnterAndWait(EventCondition<R> condition, long timeout) {
+        return mDevice.performActionAndWait(new KeyEventRunnable(KeyEvent.KEYCODE_ENTER),
+                condition, timeout);
+    }
+
+    private class KeyEventRunnable implements Runnable {
+        private int mKeyCode;
+        public KeyEventRunnable(int keyCode) {
+            mKeyCode = keyCode;
+        }
+        @Override
+        public void run() {
+            mDevice.pressKeyCode(mKeyCode);
+        }
     }
 }
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java
index 017cdce..9658429 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java
@@ -61,9 +61,9 @@
      * Setup expectations: None
      * <p>
      * This method will return the version String from PackageManager.
-     * @param pkgName the application package
-     * @throws NameNotFoundException if the package is not found in PM
+     *
      * @return the version as a String
+     * @throws NameNotFoundException if the package is not found in PM
      */
     abstract String getVersion() throws NameNotFoundException;
 
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.java
index 52e3eea..54fccde 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.java
@@ -64,6 +64,18 @@
     public BySelector getSettingsRowSelector();
 
     /**
+     * Returns a {@link BySelector} describing the app widget (eg, clock widget)
+     * @return
+     */
+    public BySelector getAppWidgetSelector();
+
+    /**
+     * Returns a {@link BySelector} describing the Now Playing card
+     * @return
+     */
+    public BySelector getNowPlayingCardSelector();
+
+    /**
      * Returns a {@link UiObject2} describing the focused search row
      * @return
      */
@@ -92,4 +104,16 @@
      * @return
      */
     public UiObject2 selectSettingsRow();
+
+    /**
+     * Returns whether there is a match for the given app widget selector.
+     * @return
+     */
+    public boolean hasAppWidgetSelector();
+
+    /**
+     * Returns whether there is a Now Playing card on leanback launcher
+     * @return
+     */
+    public boolean hasNowPlayingCard();
 }
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
index cd3a36b..db3b0ac 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
@@ -34,6 +34,7 @@
     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;
 
@@ -143,8 +144,24 @@
      */
     @Override
     public BySelector getSettingsRowSelector() {
-        return By.res(getSupportedLauncherPackage(), "list").desc("")
-                .hasDescendant(By.res("icon"));
+        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");
     }
 
     /**
@@ -225,7 +242,7 @@
             mDevice.pressHome();    // Home key to move to the first card in the Notification row
         }
         return mDevice.wait(Until.findObject(
-                getNotificationRowSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME);
+                getNotificationRowSelector().hasDescendant(By.focused(true), 3)), SHORT_WAIT_TIME);
     }
 
     /**
@@ -247,12 +264,7 @@
     @Override
     public UiObject2 selectAppsRow() {
         // Start finding Apps row from Notification row
-        if (!isAppsRowSelected()) {
-            selectNotificationRow();
-            mDevice.pressDPadDown();
-        }
-        return mDevice.wait(Until.findObject(
-                getAllAppsSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME);
+        return findRow(getAppsRowSelector());
     }
 
     /**
@@ -260,17 +272,7 @@
      */
     @Override
     public UiObject2 selectGamesRow() {
-        if (!isGamesRowSelected()) {
-            selectAppsRow();
-            mDevice.pressDPadDown();
-            // If more than or equal to 16 apps are installed, the app banner could be cut off
-            // into two rows at maximum. It needs to scroll down once more.
-            if (!isGamesRowSelected()) {
-                mDevice.pressDPadDown();
-            }
-        }
-        return mDevice.wait(Until.findObject(
-                getGamesRowSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME);
+        return findRow(getGamesRowSelector());
     }
 
     /**
@@ -278,18 +280,30 @@
      */
     @Override
     public UiObject2 selectSettingsRow() {
-        if (!isSettingsRowSelected()) {
-            open();
-            mDevice.pressHome();    // Home key to move to the first card in the Notification row
-            // The Settings row is at the last position
-            final int MAX_ROW_NUMS = 8;
-            for (int i = 0; i < MAX_ROW_NUMS; ++i) {
-                mDevice.pressDPadDown();
-            }
+        // 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() {
@@ -406,6 +420,10 @@
         // The app icon is already found and focused.
         long ready = SystemClock.uptimeMillis();
         mDevice.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(
@@ -422,6 +440,41 @@
         }
     }
 
+    /**
+     * 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 mDevice.performActionAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mDevice.pressDPadCenter();
+            }
+        }, Until.newWindow(), APP_LAUNCH_TIMEOUT);
+    }
+
     protected boolean isSearchRowSelected() {
         UiObject2 row = mDevice.findObject(getSearchRowSelector());
         if (row == null) {
@@ -456,7 +509,9 @@
 
     protected boolean isSettingsRowSelected() {
         // Settings label is only visible if the settings row is selected
-        return mDevice.hasObject(By.res(getSupportedLauncherPackage(), "label").text("Settings"));
+        UiObject2 row = mDevice.findObject(getSettingsRowSelector());
+        return (row != null && row.hasObject(
+                By.res(getSupportedLauncherPackage(), "label").text("Settings")));
     }
 
     protected boolean isAppOpen (String appPackage) {
@@ -474,6 +529,35 @@
         }
     }
 
+    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
+        mDevice.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
+            }
+            mDevice.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.
@@ -504,4 +588,89 @@
         } 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
+            }
+
+            if (direction == Direction.DOWN) {
+                mDevice.pressDPadDown();
+            } else if (direction == Direction.UP) {
+                mDevice.pressDPadUp();
+            }
+            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");
+        }
+        mDevice.pressDPadCenter();
+        mDevice.wait(Until.gone(getWorkspaceSelector()), 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;
+            }
+
+            if (direction == Direction.RIGHT) {
+                mDevice.pressDPadRight();
+            } else if (direction == Direction.LEFT) {
+                mDevice.pressDPadLeft();
+            }
+            mDevice.waitForIdle();
+            prevFocused = currentFocused;
+            currentFocused = mDevice.findObject(By.focused(true));
+        }
+        Log.d(LOG_TAG, "Failed to find the setting in Settings row.");
+        return null;
+    }
 }
diff --git a/tests/functional/tv/TvSysUiTests/Android.mk b/tests/functional/tv/TvSysUiTests/Android.mk
new file mode 100644
index 0000000..4670cec
--- /dev/null
+++ b/tests/functional/tv/TvSysUiTests/Android.mk
@@ -0,0 +1,28 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+# ------------------------------------------------
+# build a test apk
+
+LOCAL_PACKAGE_NAME := TvSysUiTests
+LOCAL_CERTIFICATE := platform
+LOCAL_MODULE_TAGS := tests
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator android-support-test platform-test-annotations \
+    leanback-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/tv/TvSysUiTests/AndroidManifest.xml b/tests/functional/tv/TvSysUiTests/AndroidManifest.xml
new file mode 100644
index 0000000..48a1cc0
--- /dev/null
+++ b/tests/functional/tv/TvSysUiTests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.test.functional.tv.sysui">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <uses-sdk android:minSdkVersion="21"
+          android:targetSdkVersion="24"/>
+
+    <instrumentation
+            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:targetPackage="android.test.functional.tv.sysui"
+            android:label="TV Platform System UI Functional Tests" />
+</manifest>
diff --git a/tests/functional/tv/TvSysUiTests/src/android/test/functional/tv/common/SysUiTestBase.java b/tests/functional/tv/TvSysUiTests/src/android/test/functional/tv/common/SysUiTestBase.java
new file mode 100644
index 0000000..dec64fe
--- /dev/null
+++ b/tests/functional/tv/TvSysUiTests/src/android/test/functional/tv/common/SysUiTestBase.java
@@ -0,0 +1,105 @@
+/*
+ * 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.test.functional.tv.common;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Bundle;
+import android.platform.test.helpers.CommandHelper;
+import android.platform.test.helpers.DPadHelper;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.launcherhelper.ILeanbackLauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.launcherhelper.LeanbackLauncherStrategy;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import org.junit.runner.RunWith;
+
+/**
+ * Base test class for functional testing for leanback platform
+ */
+@RunWith(AndroidJUnit4.class)
+public abstract class SysUiTestBase {
+
+    private static final String TAG = SysUiTestBase.class.getSimpleName();
+
+    protected UiDevice mDevice;
+    protected Instrumentation mInstrumentation;
+    protected Context mContext;
+    protected Bundle mArguments;
+
+    protected CommandHelper mCmdHelper;
+    protected DPadHelper mDPadHelper;
+    protected ILeanbackLauncherStrategy mLauncherStrategy;
+
+    public SysUiTestBase() {
+        initialize(InstrumentationRegistry.getInstrumentation());
+    }
+
+    public SysUiTestBase(Instrumentation instrumentation) {
+        initialize(instrumentation);
+    }
+
+    private void initialize(Instrumentation instrumentation) {
+        // Initialize instances of testing support library
+        mInstrumentation = instrumentation;
+        mContext = getInstrumentation().getContext();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mArguments = InstrumentationRegistry.getArguments();
+
+        // Initialize instances of leanback and app helpers
+        ILeanbackLauncherStrategy launcherStrategy = LauncherStrategyFactory.getInstance(
+                mDevice).getLeanbackLauncherStrategy();
+        if (launcherStrategy instanceof LeanbackLauncherStrategy) {
+            mLauncherStrategy = (LeanbackLauncherStrategy) launcherStrategy;
+        }
+        mCmdHelper = new CommandHelper(getInstrumentation());
+        mDPadHelper = DPadHelper.getInstance(getInstrumentation());
+    }
+
+    protected Instrumentation getInstrumentation() {
+        return mInstrumentation;
+    }
+
+    protected int getArgumentsAsInt(String key, int defaultValue) {
+        String stringValue = mArguments.getString(key);
+        if (stringValue != null) {
+            try {
+                return Integer.parseInt(stringValue);
+            } catch (NumberFormatException e) {
+                Log.w(TAG, String.format("Unable to parse arg %s with value %s to a integer.",
+                        key, stringValue), e);
+            }
+        }
+        return defaultValue;
+    }
+
+    protected boolean getArgumentsAsBoolean(String key, boolean defaultValue) {
+        String stringValue = mArguments.getString(key);
+        if (stringValue != null) {
+            try {
+                return Boolean.parseBoolean(stringValue);
+            } catch (Exception e) {
+                Log.w(TAG, String.format("Unable to parse arg %s with value to a boolean.",
+                        key, stringValue), e);
+            }
+        }
+        return defaultValue;
+    }
+}
diff --git a/tests/functional/tv/TvSysUiTests/src/android/test/functional/tv/common/UiWatchers.java b/tests/functional/tv/TvSysUiTests/src/android/test/functional/tv/common/UiWatchers.java
new file mode 100644
index 0000000..90cb05c
--- /dev/null
+++ b/tests/functional/tv/TvSysUiTests/src/android/test/functional/tv/common/UiWatchers.java
@@ -0,0 +1,112 @@
+/*
+ * 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.test.functional.tv.common;
+
+import android.app.Instrumentation;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiWatcher;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class to monitor a UI object to dismiss.
+ */
+public final class UiWatchers {
+    private static final String LOG_TAG = UiWatchers.class.getSimpleName();
+    private static final long WAIT_TIME_MS = 3000;
+
+    private UiDevice mDevice;
+    private List<String> mWatcherNames = new ArrayList<>();
+
+
+    public UiWatchers(Instrumentation instrumentation) {
+        mDevice = UiDevice.getInstance(instrumentation);
+    }
+
+    /**
+     * Register a new watcher that looks for a object and dismisses it.
+     */
+    public void registerDismissWatcher(final String watcherName, final BySelector watch,
+            final BySelector click) {
+        if (mWatcherNames.contains(watcherName)) {
+            Log.i(LOG_TAG,
+                    String.format("The watcher %s already registered. Skipped!", watcherName));
+            return;
+        }
+        mWatcherNames.add(watcherName);
+        mDevice.registerWatcher(watcherName, new UiWatcher() {
+            @Override
+            public boolean checkForCondition() {
+                if (mDevice.hasObject(watch)) {
+                    UiObject2 dismiss = mDevice.wait(Until.findObject(click), WAIT_TIME_MS);
+                    dismiss.click();
+                    postHandler(watcherName);
+                    return true;    // triggered
+                }
+                return false;   // not triggered
+            }
+        });
+    }
+
+    public void unregisterDismissWatcher(String watcherName) {
+        mDevice.removeWatcher(watcherName);
+        mWatcherNames.remove(watcherName);
+    }
+
+    /**
+     * Checks if any registered UiWatcher have triggered.
+     * @return
+     */
+    public boolean hasWatcherTriggered() {
+        for (String watcherName : mWatcherNames) {
+            if (hasWatcherTriggered(watcherName)) {
+                Log.i(LOG_TAG,
+                        String.format("Found the watcher %s have triggered.", watcherName));
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks if a specific registered UiWatcher has triggered.
+     * @param watcherName
+     * @return
+     */
+    public boolean hasWatcherTriggered(String watcherName) {
+        if (!mWatcherNames.contains(watcherName)) {
+            Log.w(LOG_TAG, String.format("The watcher %s not registered.", watcherName));
+        }
+        return mDevice.hasWatcherTriggered(watcherName);
+    }
+
+    public void resetWatchers() {
+        mDevice.resetWatcherTriggers();
+    }
+
+    /**
+     * Current implementation ignores the exception and continues.
+     */
+    public void postHandler(String watcherName) {
+        Log.i(LOG_TAG, String.format("%s dismissed", watcherName));
+    }
+}
diff --git a/tests/functional/tv/TvSysUiTests/src/android/test/functional/tv/settings/TestAll.java b/tests/functional/tv/TvSysUiTests/src/android/test/functional/tv/settings/TestAll.java
new file mode 100644
index 0000000..5246b61
--- /dev/null
+++ b/tests/functional/tv/TvSysUiTests/src/android/test/functional/tv/settings/TestAll.java
@@ -0,0 +1,33 @@
+/*
+ * 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.test.functional.tv.settings;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * adb shell am instrument -w -r \
+ * -e class android.test.functional.tv.settings.TestAll \
+ * android.test.functional.tv.sysui/android.support.test.runner.AndroidJUnitRunner
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+})
+public class TestAll {
+    // the class remains empty,
+    // used only as a holder for the above annotations
+}