TV 1p app helpers base

Add TV 1p app helpers. This includes SysUI (Launcher,
Recents, PIP, Settings, NoTouchAuth), Search,
YouTube, Play Movies & TV, Leanback Demo.

Bug: 30299617
Change-Id: I86ac86c1338d7a8d8c06d8c15e3154b00809d785
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/IRecentsHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/IRecentsHelper.java
new file mode 100644
index 0000000..8a6f054
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/IRecentsHelper.java
@@ -0,0 +1,45 @@
+/*
+ * 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.support.test.uiautomator.Direction;
+
+public interface IRecentsHelper {
+    /**
+     * Setup expectations: "Recents" is open.
+     * <p>
+     * Flings the recent apps in the specified direction.
+     * </p>
+     * @param dir the direction for the apps to move
+     */
+    void flingRecents(Direction dir);
+
+    /**
+     * Setup expectations: "Recents" is open with content
+     * <p>
+     * Clears up open recent items. Nothing happens if there is no content.
+     * </p>
+     */
+    void clearAll();
+
+    /**
+     * Setup expectations: "Recents" is open.
+     *
+     * @return True if there is Recents content.
+     */
+    boolean hasContent();
+}
diff --git a/libraries/first-party-app-helpers/tv/Android.mk b/libraries/first-party-app-helpers/tv/Android.mk
index 9e0b7dd..8d64152 100644
--- a/libraries/first-party-app-helpers/tv/Android.mk
+++ b/libraries/first-party-app-helpers/tv/Android.mk
@@ -17,7 +17,10 @@
 
 include $(CLEAR_VARS)
 LOCAL_MODULE := leanback-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := launcher-helper-lib base-app-helpers
+LOCAL_STATIC_JAVA_LIBRARIES := launcher-helper-lib base-app-helpers \
+                               tv-sysui-app-helper tv-youtube-app-helper tv-search-app-helper \
+                               tv-play-movies-app-helper \
+                               leanback-demo-app-helper
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
diff --git a/libraries/first-party-app-helpers/tv/leanback-demo-app-helper/Android.mk b/libraries/first-party-app-helpers/tv/leanback-demo-app-helper/Android.mk
new file mode 100644
index 0000000..dfa66bd
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/leanback-demo-app-helper/Android.mk
@@ -0,0 +1,24 @@
+#
+# 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)
+LOCAL_MODULE := leanback-demo-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers tv-sysui-app-helper
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
diff --git a/libraries/first-party-app-helpers/tv/leanback-demo-app-helper/src/android/platform/test/helpers/tv/LeanbackDemoHelperImpl.java b/libraries/first-party-app-helpers/tv/leanback-demo-app-helper/src/android/platform/test/helpers/tv/LeanbackDemoHelperImpl.java
new file mode 100644
index 0000000..ee73a56
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/leanback-demo-app-helper/src/android/platform/test/helpers/tv/LeanbackDemoHelperImpl.java
@@ -0,0 +1,212 @@
+/*
+ * 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.tv;
+
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.platform.test.helpers.AbstractLeanbackAppHelper;
+import android.platform.test.helpers.exceptions.UiTimeoutException;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+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;
+
+public class LeanbackDemoHelperImpl extends AbstractLeanbackAppHelper {
+
+    private static final String TAG = LeanbackDemoHelperImpl.class.getSimpleName();
+    private static final String UI_PACKAGE = "com.example.android.tvleanback";
+    private static final String ACTIVITY_MAIN = "com.example.android.tvleanback.ui.MainActivity";
+    private static final String RES_MAIN_ACTIVITY_ID = "main_frame";
+    private static final long SHORT_SLEEP_MS = 5000;    // 5 seconds
+    private static final long LONG_SLEEP_MS = 30000;    // 30 seconds
+
+    private static final String TEXT_TOOLTIP = "Hold HOME to control PIP";
+
+
+    public LeanbackDemoHelperImpl(Instrumentation instrumentation) {
+        super(instrumentation);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Videos by Google";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected BySelector getMainActivitySelector() {
+        return By.res(UI_PACKAGE, RES_MAIN_ACTIVITY_ID);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected BySelector getBrowseRowsSelector() {
+        return By.focused(true).hasChild(By.res(UI_PACKAGE, "main_image"));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void open() {
+        launchActivity();
+        // Wait until the main activity is open.
+        mDevice.wait(Until.hasObject(getMainActivitySelector()), SHORT_SLEEP_MS);
+    }
+
+    /**
+     * Setup expectation: None
+     *
+     * Launches the demo main activity with an Intent.
+     */
+    private void launchActivity() {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        intent.setComponent(new ComponentName(UI_PACKAGE, ACTIVITY_MAIN));
+        // Launch the activity
+        mInstrumentation.getContext().startActivity(intent);
+    }
+
+    /**
+     * Setup expectation: On the main activity.
+     * <p>
+     * Selects the desired video in the row content and wait for it to open the details view.
+     * </p>
+     * @param sectionName the name of section that includes the desired video
+     * @param videoName the name of video to select
+     */
+    public void selectVideoInRowContent(String sectionName, String videoName) {
+        returnToMainActivity();
+        openHeader(sectionName);
+        UiObject2 container = getRowContent(sectionName);
+        BySelector target = By.focused(true).hasDescendant(
+                By.res(UI_PACKAGE, "title_text").text(videoName), 3);
+        UiObject2 video = select(container, target, Direction.RIGHT);
+        if (video == null) {
+            throw new UnknownUiException(
+                    String.format("The video %s not found in the %s section", videoName,
+                            sectionName));
+        }
+        mDPadHelper.pressDPadCenter();
+        mDevice.waitForIdle();
+    }
+
+    /**
+     * Setup expectation: On the details view.
+     * <p>
+     * Selects the button of "WATCH TRAILER FREE".
+     * </p>
+     */
+    public void selectWatchTrailer() {
+        BySelector target = By.res(UI_PACKAGE, "lb_action_button").text("WATCH TRAILER\nFREE");
+        UiObject2 trailer = mDevice.wait(Until.findObject(target), SHORT_SLEEP_MS);
+        if (trailer == null) {
+            throw new UnknownUiException("The watch trailer button not found");
+        }
+        mDPadHelper.pressDPadCenter();
+        mDevice.waitForIdle();
+    }
+
+    /**
+     * Setup expectation: On the media control card.
+     *
+     * @return a boolean of whether the media control card has a PIP button enabled.
+     */
+    public boolean hasPipButton() {
+        // Pressing the key up brings up the media control card
+        mDPadHelper.pressDPad(Direction.UP);
+        if (!mDevice.wait(Until.hasObject(By.res(UI_PACKAGE, "controls_card")), SHORT_SLEEP_MS)) {
+            throw new UiTimeoutException("No media control card is found");
+        }
+        return mDevice.wait(Until.hasObject(
+                By.res(UI_PACKAGE, "button").desc("Enter Picture In Picture Mode")),
+                SHORT_SLEEP_MS);
+    }
+
+    /**
+     * Setup expectation: PIP window is being open.
+     *
+     * @return a boolean of whether the tooltip text is shown.
+     */
+    public boolean hasTooltipShown() {
+        return mDevice.wait(Until.hasObject(By.text(TEXT_TOOLTIP)), SHORT_SLEEP_MS);
+    }
+
+    /**
+     * Setup expectation: While playing a video in fullscreen.
+     * <p>
+     * Clicks on the PIP button and wait for it to be gone.
+     * </p>
+     */
+    public void openMediaControlsAndClickPipButton() {
+        // Pressing the key up brings up the media control card
+        mDPadHelper.pressDPad(Direction.UP);
+        if (!mDevice.wait(Until.hasObject(By.res(UI_PACKAGE, "controls_card")), SHORT_SLEEP_MS)) {
+            throw new UiTimeoutException("No media control card is found");
+        }
+        BySelector target = By.res(UI_PACKAGE, "button").desc("Enter Picture In Picture Mode");
+        UiObject2 pipButton = mDevice.wait(Until.findObject(target), SHORT_SLEEP_MS);
+        if (pipButton == null) {
+            throw new UiTimeoutException("PIP button not found");
+        }
+        pipButton.click();
+        mDevice.waitForIdle();
+        mDPadHelper.pressDPadCenter();
+        mDevice.waitForIdle();
+    }
+
+    /**
+     * 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), SHORT_SLEEP_MS)
+                && maxBackAttempts-- > 0) {
+            mDevice.pressBack();
+        }
+    }
+
+    private UiObject2 getRowContent(String rowName) {
+        return mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "row_content").desc(rowName)),
+                LONG_SLEEP_MS);
+    }
+}
+
diff --git a/libraries/first-party-app-helpers/tv/play-movies-app-helper/Android.mk b/libraries/first-party-app-helpers/tv/play-movies-app-helper/Android.mk
new file mode 100644
index 0000000..1d270f8
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/play-movies-app-helper/Android.mk
@@ -0,0 +1,24 @@
+#
+# 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)
+LOCAL_MODULE := tv-play-movies-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
diff --git a/libraries/first-party-app-helpers/tv/play-movies-app-helper/src/android/platform/test/helpers/tv/PlayMoviesHelperImpl.java b/libraries/first-party-app-helpers/tv/play-movies-app-helper/src/android/platform/test/helpers/tv/PlayMoviesHelperImpl.java
new file mode 100644
index 0000000..b07b4f6
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/play-movies-app-helper/src/android/platform/test/helpers/tv/PlayMoviesHelperImpl.java
@@ -0,0 +1,318 @@
+/*
+ * 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.tv;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.platform.test.helpers.AbstractLeanbackAppHelper;
+import android.platform.test.helpers.CommandHelper;
+import android.platform.test.helpers.exceptions.UiTimeoutException;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+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;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class PlayMoviesHelperImpl extends AbstractLeanbackAppHelper {
+
+    private static final String LOG_TAG = PlayMoviesHelperImpl.class.getSimpleName();
+    private static final String UI_PACKAGE = "com.google.android.videos";
+    private static final String RES_MAIN_ACTIVITY_ID = "browse_container_dock";
+    private static final String RES_SEARCH_ORB_ID = "title_orb";
+    private static final String RES_SEARCH_BOX_ID = "lb_search_text_editor";
+
+    private static final String TEXT_MOVIES = "Movies";
+    private static final String TEXT_MY_LIBRARY = "My library";
+    private static final String TEXT_PLAY_TRAILER = "PLAY TRAILER";
+
+    private static final long SHORT_SLEEP_MS = 5000;    // 5 seconds
+    private static final long LONG_SLEEP_MS = 30000;    // 30 seconds
+
+    private CommandHelper mCmdHelper;
+
+
+    public PlayMoviesHelperImpl(Instrumentation instrumentation) {
+        super(instrumentation);
+        mCmdHelper = new CommandHelper(instrumentation);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Play Movies & TV";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected BySelector getMainActivitySelector() {
+        return By.res(UI_PACKAGE, RES_MAIN_ACTIVITY_ID);
+    }
+
+    /**
+     * Selects search orb. The app should be opened beforehand by calling open().
+     */
+    public void selectSearchOrb() {
+        returnToMainActivity();
+
+        // Wait until the search orb appears at runtime.
+        UiObject2 searchOrb = mDevice.wait(
+                Until.findObject(By.res(UI_PACKAGE, RES_SEARCH_ORB_ID).clickable(true)),
+                SHORT_SLEEP_MS);
+        if (searchOrb == null) {
+            throw new UiTimeoutException("Failed to select search orb");
+        }
+        searchOrb.click();
+    }
+
+    /**
+     * Searches for the given query and keep the search result open.
+     * Play Movies app should be opened beforehand by calling open().
+     *
+     * @param query a search query string typed in Play Movies' search box.
+     */
+    public void search(String query) {
+        selectSearchOrb();
+        mDevice.waitForIdle();
+
+        Log.v(LOG_TAG, "Searching for the movie: " + query);
+        UiObject2 editText = mDevice.wait(Until.findObject(
+                By.res(UI_PACKAGE, RES_SEARCH_BOX_ID)), SHORT_SLEEP_MS);
+        if (editText == null) {
+            throw new UnknownUiException("Search text editor not found");
+        }
+
+        int retries = 4;
+        while(!editText.isFocused() && retries > 0) {
+            mDevice.pressDPadRight();
+            mDevice.waitForIdle();
+            retries--;
+        }
+
+        // Set query and search
+        editText.setText(query);
+        SystemClock.sleep(SHORT_SLEEP_MS);
+
+        mDevice.pressEnter();
+        SystemClock.sleep(SHORT_SLEEP_MS);
+    }
+
+    /**
+     * Finds a movie with the trailer from the search result and start playing.
+     * search() should be called right before calling this method.
+     */
+    public UiObject2 searchForMovieWithTrailer() {
+        mDevice.wait(Until.findObject(By.text(TEXT_MOVIES)), SHORT_SLEEP_MS);
+        mDevice.pressDPadCenter();
+        mDevice.waitForIdle();
+
+        // Skip until a trailer is found from the result
+        UiObject2 trailerButton = null;
+        final int MAX_ATTEMPTS_SEARCH_TRAILERS = 5;
+        for (long i = 0; i < MAX_ATTEMPTS_SEARCH_TRAILERS; i++) {
+            trailerButton = getTrailerButton();
+            if (trailerButton == null) {
+                // The trailer was not found for the movie,
+                // back and open the detail of the next movie
+                mDevice.pressBack();
+                mDevice.wait(Until.findObject(By.text(TEXT_MOVIES)), SHORT_SLEEP_MS);
+
+                mDevice.pressDPadRight();
+                SystemClock.sleep(SHORT_SLEEP_MS);
+
+                mDevice.pressDPadCenter();
+                mDevice.waitForIdle();
+            } else {
+                // The trailer was found for the movie
+                break;
+            }
+
+        }
+
+        SystemClock.sleep(SHORT_SLEEP_MS);
+        return trailerButton;
+    }
+
+    public UiObject2 getTrailerButton() {
+        return mDevice.wait(Until.findObject(By.text(TEXT_PLAY_TRAILER)), SHORT_SLEEP_MS);
+    }
+
+    /**
+     * Setup expectations: Trailer is selected, and shown in details fragment.
+     *
+     * Play a trailer
+     */
+    public void playTrailerInDetails(long durationMs) {
+        UiObject2 trailerButton = getTrailerButton();
+        if (trailerButton == null) {
+            throw new UnknownUiException("Trailer action not found");
+        }
+        trailerButton.click();
+
+        // Using "Play trailer" to wait for the playback to start
+        mDevice.wait(Until.gone(By.text(TEXT_PLAY_TRAILER)),
+                SHORT_SLEEP_MS);
+
+        // Using "Play trailer" button to wait until the trailer finishes
+        trailerButton = mDevice.wait(
+                Until.findObject(By.text(TEXT_PLAY_TRAILER)), durationMs);
+        if (trailerButton == null) {
+            throw new RuntimeException("Trailer too long or something went wrong");
+        }
+    }
+
+    /**
+     * Open My Library section
+     */
+    public void openMyLibrary() {
+        returnToMainActivity();
+        openHeader(TEXT_MY_LIBRARY);
+    }
+
+    /**
+     * Setup expectations: None.
+     * Open My Movies in My library section, wait for the list of movies to come.
+     */
+    public void openMyMoviesList() {
+        openMyLibrary();
+        if (getCardByNameInRowContent(TEXT_MOVIES) == null) {
+            throw new UnknownUiException("Movies in My library not found");
+        }
+        mDevice.performActionAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mDevice.pressDPadCenter();
+            }
+        }, Until.newWindow(), SHORT_SLEEP_MS);
+    }
+
+    /**
+     * Get a card with the given name in row_content
+     *
+     * @param title of the card
+     * @return UIObject2 for the focusable button
+     */
+    private UiObject2 getCardByNameInRowContent(String title) {
+        UiObject2 container = mDevice.findObject(
+                By.res(getPackage(), "row_content").hasDescendant(By.focused(true)));
+        return select(container, By.res(getPackage(), "title_text").text(title),
+                Direction.RIGHT);
+    }
+
+    /**
+     * Setup expectations: The movie(s) is listed in the Vertical grid fragment
+     */
+    public void selectTheFocusedMovieInVerticalGrid() {
+        assertWidgetEquals(Widget.VERTICAL_GRID_FRAGMENT);
+        mDevice.performActionAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mDevice.pressDPadCenter();
+            }
+        }, Until.newWindow(), SHORT_SLEEP_MS);
+    }
+
+    /**
+     * Setup expectations: The movie to play is listed in the Details fragment
+     *
+     * Play the selected movies from beginning
+     */
+    public void playFromBeginning() {
+        assertWidgetEquals(Widget.DETAILS_FRAGMENT);
+
+        // Play from beginning
+        UiObject2 actionButton = mDevice.wait(Until.findObject(By.clazz(".Button")),
+                LONG_SLEEP_MS);
+        if (actionButton == null) {
+            throw new UnknownUiException("action button not found");
+        }
+        String selectedText = actionButton.getText();
+        Log.v(LOG_TAG, String.format("Selected text is: %s", selectedText));
+        while (!(selectedText.toLowerCase().equals("play from beginning") ||
+                selectedText.toLowerCase().equals("play movie"))) {
+            String prevText = selectedText;
+
+            // Select the next item
+            mDevice.pressDPadRight();
+
+            // Make sure the text has changed
+            selectedText = mDevice.findObject(By.clazz(".Button").focused(true)).getText();
+            if (selectedText.equals(prevText)) {
+                throw new UnknownUiException("'Play from beginning' or 'Play movie' not found");
+            }
+        }
+        mDevice.pressDPadCenter();
+
+        // Dismiss confirmation dialog if it's a rental movie
+        UiObject2 yesButton = mDevice.wait(
+                Until.findObject(By.res(UI_PACKAGE, "guidedactions_list")), SHORT_SLEEP_MS);
+        if (yesButton != null) {
+            mDevice.pressDPadCenter();
+        }
+    }
+
+    /**
+     * Get the current playback state for a given package that owns the media session.
+     * @param packageName the package name of media session owner
+     * @return
+     * 0 = PlaybackState.STATE_NONE
+     * 1 = PlaybackState.STATE_STOPPED
+     * 2 = PlaybackState.STATE_PAUSED
+     * 3 = PlaybackState.STATE_PLAYING
+     */
+    public int getPlaybackState(String packageName) {
+        String output = mCmdHelper.executeDumpsysMediaSession();
+        // Parse the output of dumpsys media_session.
+        // Example :
+        // LeanbackSampleApp com.example.android.tvleanback/LeanbackSampleApp
+        //   package=com.example.android.tvleanback
+        //   ...
+        //   state=PlaybackState {state=3, position=0, buffered position=0, speed=1.0, updated=...}
+        int playbackState = 0;
+        int index = output.indexOf(String.format("package=%s", packageName));
+        if (index == -1) {
+            Log.w(LOG_TAG, String.format("No media session found for the package: %s", packageName));
+            return playbackState;
+        }
+        final Pattern PLAYBACKSTATE_REGEX = Pattern.compile(
+                "\\s*state=PlaybackState \\{state=(\\d+),.*");
+        Matcher matcher = PLAYBACKSTATE_REGEX.matcher(output.substring(index));
+        if (matcher.find()) {
+            playbackState = Integer.parseInt(matcher.group(1));
+            Log.i(LOG_TAG, String.format("PlaybackState=%s package=%s", playbackState, packageName));
+        }
+        return playbackState;
+    }
+}
+
diff --git a/libraries/first-party-app-helpers/tv/search-app-helper/Android.mk b/libraries/first-party-app-helpers/tv/search-app-helper/Android.mk
new file mode 100644
index 0000000..3a38984
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/search-app-helper/Android.mk
@@ -0,0 +1,24 @@
+#
+# 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)
+LOCAL_MODULE := tv-search-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
diff --git a/libraries/first-party-app-helpers/tv/search-app-helper/src/android/platform/test/helpers/tv/SearchHelperImpl.java b/libraries/first-party-app-helpers/tv/search-app-helper/src/android/platform/test/helpers/tv/SearchHelperImpl.java
new file mode 100644
index 0000000..a723e51
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/search-app-helper/src/android/platform/test/helpers/tv/SearchHelperImpl.java
@@ -0,0 +1,110 @@
+/*
+ * 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.tv;
+
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.platform.test.helpers.AbstractLeanbackAppHelper;
+import android.platform.test.helpers.exceptions.UiTimeoutException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+
+public class SearchHelperImpl extends AbstractLeanbackAppHelper {
+
+    private static final String TAG = SearchHelperImpl.class.getSimpleName();
+    private static final String UI_PACKAGE = "com.google.android.katniss";
+    private static final String SEARCH_ACTIVITY_NAME =
+            "com.google.android.katniss.search.SearchActivity";
+    private static final long SHORT_SLEEP_MS = 3000;    // 3 seconds
+
+    private static Instrumentation  mInstrumentation;
+
+    public static final int VOICE_SEARCH = 1;
+    public static final int KEYBOARD_SEARCH = 2;
+
+
+    public SearchHelperImpl(Instrumentation instrumentation) {
+        super(instrumentation);
+        mInstrumentation = instrumentation;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return null;
+    }
+
+    /**
+     * Setup expectations: None
+     *
+     * Starts the voice search activity for querying the content.
+     * @param searchType Type of search request (1=voice, 2=keyboard)
+     * @param searchQuery Query string
+     */
+    public void launchActivityAndQuery(int searchType, String searchQuery) {
+        Intent intent = new Intent("android.intent.action.ASSIST");
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.addCategory("android.intent.category.DEFAULT");
+        intent.setComponent(new ComponentName(UI_PACKAGE, SEARCH_ACTIVITY_NAME));
+        intent.putExtra("search_type", searchType);
+        intent.putExtra("query", searchQuery);
+        mInstrumentation.getContext().startActivity(intent);
+        Log.d(TAG, String.format("launchActivityAndQuery searchType=%d query=%s", searchType,
+                searchQuery));
+
+        // Ensure that the package is open
+        if (isOpen(SHORT_SLEEP_MS) == false) {
+            throw new UiTimeoutException("The Search activity is not launched.");
+        }
+        if (isInKeyboardMode()) {
+            Log.i(TAG, "Search activity Is in keyboard mode. Pressing the ENTER key.");
+            mDPadHelper.pressEnter();
+            mDevice.waitForIdle();
+        }
+    }
+
+    public BySelector getSearchTextEditorSelector() {
+        return By.res(UI_PACKAGE, "search_text_editor");
+    }
+
+    public BySelector getResultContainerSelector() {
+        return By.res(UI_PACKAGE, "container_list");
+    }
+
+    public boolean isInKeyboardMode() {
+        return mDevice.hasObject(getSearchTextEditorSelector());
+    }
+
+    public boolean isOpen(long waitMs) {
+        return mDevice.wait(Until.hasObject(By.pkg(UI_PACKAGE).depth(0)), waitMs);
+    }
+}
+
diff --git a/libraries/first-party-app-helpers/tv/sysui-app-helper/Android.mk b/libraries/first-party-app-helpers/tv/sysui-app-helper/Android.mk
new file mode 100644
index 0000000..0926908
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/sysui-app-helper/Android.mk
@@ -0,0 +1,24 @@
+#
+# 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)
+LOCAL_MODULE := tv-sysui-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
diff --git a/libraries/first-party-app-helpers/tv/sysui-app-helper/src/android/platform/test/helpers/tv/NoTouchAuthHelperImpl.java b/libraries/first-party-app-helpers/tv/sysui-app-helper/src/android/platform/test/helpers/tv/NoTouchAuthHelperImpl.java
new file mode 100644
index 0000000..1bb6d6b
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/sysui-app-helper/src/android/platform/test/helpers/tv/NoTouchAuthHelperImpl.java
@@ -0,0 +1,184 @@
+/*
+ * 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.tv;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.platform.test.helpers.AbstractLeanbackAppHelper;
+import android.platform.test.helpers.DPadHelper;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+
+/**
+ * App helper implementation class for the NoTouchAuthDelegate UI
+ * to add an account to non-touch device like TV.
+ */
+public class NoTouchAuthHelperImpl extends AbstractLeanbackAppHelper {
+    private static final String LOG_TAG = NoTouchAuthHelperImpl.class.getSimpleName();
+    private static final String UI_PACKAGE = "com.google.android.gsf.notouch";
+
+    private static final String TITLE_SIGN_IN_ONBOARDING = "Sign in to your account";
+    private static final String TITLE_SIGN_IN_ACCOUNT = "Enter your account email address";
+    private static final String TITLE_SIGN_IN_PASSWORD = "Enter your account password";
+    private static final String TITLE_SIGN_IN_ACCOUNT_ALREADY_EXISTS =
+            "This account already exists on your device";
+    private static final String TEXT_SIGN_IN_SECOND_SCREEN = "Use your phone or laptop";
+    private static final String TEXT_SIGN_IN_PASSWORD = "Use your password";
+
+    private static final long SHORT_SLEEP_MS = 3000;
+
+
+    private Context mContext;
+
+
+    public NoTouchAuthHelperImpl(Instrumentation instrumentation) {
+        super(instrumentation);
+        mDPadHelper = DPadHelper.getInstance(instrumentation);
+        mContext = instrumentation.getContext();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        throw new UnsupportedOperationException("This method is not supported for NoTouchAuth");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void open() {
+        throw new UnsupportedOperationException("This method is not supported for NoTouchAuth");
+    }
+
+    /**
+     * Setup expectations: The sign-in page is open.
+     * <p>
+     * Attempts to login with an account.
+     * </p>
+     * @return true if the attempt to login is successful. However this doesn't guarantee that
+     * the account is registered in AccountManager.
+     */
+    public boolean loginAccount(String accountName, String password) {
+        selectUseYourPassword();
+
+        // Enter the account name
+        if (!isSignInAccountPage()) {
+            throw new UnknownUiException("Failed to find the page to enter account");
+        }
+        setTextForSignIn(accountName);
+
+        // Check if the account already exists
+        if (isSignInAccountAlreadyExists()) {
+            Log.w(LOG_TAG, "Failed to log in with the account already registered.");
+            return false;
+        }
+
+        // Enter the password
+        if (!isSignInPasswordPage()) {
+            throw new UnknownUiException("Failed to find the page to enter password");
+        }
+        setTextForSignIn(password);
+        return true;
+    }
+
+    /**
+     * Setup expectations: The sign-in page is open.
+     * <p>
+     * Selects "Use Your Password".
+     * </p>
+     * @return
+     */
+    private void selectUseYourPassword() {
+        selectSignInOptions(TEXT_SIGN_IN_PASSWORD);
+        // Wait for it to open the page to enter account name
+        mDevice.waitForIdle();
+        if (!isSignInAccountPage()) {
+            throw new UnknownUiException("Failed to find the page to enter account name");
+        }
+    }
+
+    /**
+     * Setup expectations: The sign-in page is open. Selects "Use your phone or laptop" for
+     * Second Screen Setup.
+     * @return
+     */
+    private void selectUseYourPhoneOrLaptop() {
+        selectSignInOptions(TEXT_SIGN_IN_SECOND_SCREEN);
+    }
+
+    private boolean isSignInOnboardingPage() {
+        return TITLE_SIGN_IN_ONBOARDING.equals(getTitleText());
+    }
+
+    private boolean isSignInAccountPage() {
+        return TITLE_SIGN_IN_ACCOUNT.equals(getTitleText());
+    }
+
+    private boolean isSignInPasswordPage() {
+        return TITLE_SIGN_IN_PASSWORD.equals(getTitleText());
+    }
+
+    private boolean isSignInAccountAlreadyExists() {
+        return TITLE_SIGN_IN_ACCOUNT_ALREADY_EXISTS.equals(getTitleText());
+    }
+
+    private void selectSignInOptions(String optionString) {
+        if (!isSignInOnboardingPage()) {
+            throw new IllegalStateException("Should be on the sign in onboarding page");
+        }
+        UiObject2 action = mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "action")),
+                SHORT_SLEEP_MS);
+        if (action == null) {
+            throw new UnknownUiException("The container 'action' for sign-in not found");
+        }
+        UiObject2 button = select(action,
+                By.res(UI_PACKAGE, "list_item_text").text(optionString),
+                Direction.DOWN);
+        if (button == null) {
+            throw new UnknownUiException("The button not found " + optionString);
+        }
+        mDPadHelper.pressDPadCenterAndWait(Until.newWindow(), SHORT_SLEEP_MS);
+    }
+
+    private String getTitleText() {
+        return mDevice.findObject(By.res(UI_PACKAGE, "title_text")).getText();
+    }
+
+    private void setTextForSignIn(String text) {
+        UiObject2 editText = mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "text_input")),
+                SHORT_SLEEP_MS);
+        editText.setText(text);
+        mDPadHelper.pressEnterAndWait(Until.newWindow(), SHORT_SLEEP_MS);
+    }
+}
+
diff --git a/libraries/first-party-app-helpers/tv/sysui-app-helper/src/android/platform/test/helpers/tv/SysUiPipHelperImpl.java b/libraries/first-party-app-helpers/tv/sysui-app-helper/src/android/platform/test/helpers/tv/SysUiPipHelperImpl.java
new file mode 100644
index 0000000..60cab1a
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/sysui-app-helper/src/android/platform/test/helpers/tv/SysUiPipHelperImpl.java
@@ -0,0 +1,297 @@
+/*
+ * 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.tv;
+
+import static android.view.KeyEvent.KEYCODE_WINDOW;
+
+import android.app.Instrumentation;
+import android.graphics.Rect;
+import android.platform.test.helpers.DPadHelper;
+import android.platform.test.helpers.CommandHelper;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * App helper implementation class for TV's picture-in-picture in System UI
+ */
+public class SysUiPipHelperImpl {
+    private static final String LOG_TAG = SysUiPipHelperImpl.class.getSimpleName();
+    private static final String UI_PACKAGE = "com.android.systemui";
+    private static final int FULLSCREEN_WORKSPACE_STACK_ID = 1;
+    private static final int PINNED_STACK_ID = 4;   // ID of stack for a PIP window
+
+    private static final int INVALID_TASK_ID = -1;
+    private static final String ACTIVITY_PIPOVERLAY =
+            "com.android.systemui.tv.pip.PipOverlayActivity";
+    // Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows
+    private static final Rect PIP_MENU_BOUNDS = new Rect(596, 280, 1324, 690);
+    private static final Rect PIP_RECENTS_BOUNDS = new Rect(800, 54, 1120, 234);
+    private static final Rect PIP_RECENTS_FOCUSED_BOUNDS = new Rect(775, 54, 1145, 262);
+    private static final Rect PIP_SETTINGS_BOUNDS = new Rect(662, 54, 1142, 324);
+
+    private static final long SHORT_SLEEP_MS = 3000;    // 3 seconds
+
+    private DPadHelper mDPadHelper;
+    private CommandHelper mCmdHelper;
+    private UiDevice mDevice;
+
+
+    public SysUiPipHelperImpl(Instrumentation instrumentation) {
+        mDPadHelper = DPadHelper.getInstance(instrumentation);
+        mCmdHelper = new CommandHelper(instrumentation);
+        mDevice = UiDevice.getInstance(instrumentation);
+    }
+
+    /**
+     * @return the package name for this helper's application.
+     */
+    public String getPackage() {
+        return UI_PACKAGE;
+    }
+
+    /**
+     * Setup expectations: None
+     * <p>
+     * Checks if a PIP window is shown on screen.
+     * </p>
+     * @param activityName the activity of the application that implements PIP playback
+     */
+    public boolean isPipOnScreen(String activityName) {
+        String output = mCmdHelper.executeAmStackInfo(PINNED_STACK_ID);
+        Log.i(LOG_TAG, "isPipOnScreen: " + output);
+        if (null == output || "null".equalsIgnoreCase(output)) {
+            Log.i(LOG_TAG, "No PIP window is found");
+            return false;
+        }
+        // Note that ACTIVITY_PIPOVERLAY would disappear in seconds, afterwards the app playback
+        // overlay comes in. It's just safe to think of that whatever activity in pinned stack is
+        // PIP window.
+        return output.contains(activityName);
+    }
+
+    /**
+     * Setup expectations: None
+     * <p>
+     * Checks if a PIP window is shown on screen.
+     * </p>
+     * @param activityName the activity of the application that implements PIP playback
+     */
+    public boolean isInFullscreen(String activityName) {
+        String output = mCmdHelper.executeAmStackInfo(FULLSCREEN_WORKSPACE_STACK_ID);
+        Log.i(LOG_TAG, "isInFullscreen: " + output);
+        if (null == output || "null".equalsIgnoreCase(output)) {
+            Log.i(LOG_TAG, "Activity is not found in full screen: " + activityName);
+            return false;
+        }
+        return output.contains(activityName);
+    }
+
+    /**
+     * Setup expectations: None
+     * <p>
+     * Returns the bounds on screen for a PIP window.
+     * </p>
+     */
+    private Rect getPipBounds(String packageName, String activityName) {
+        return getBounds(PINNED_STACK_ID, packageName, activityName);
+    }
+
+    private Rect getBounds(int stackId, String packageName, String activityName) {
+        // Format:
+        // taskId=216: com.example.android.tvleanback/com.example.android.tvleanback.ui.PlaybackOverlayActivity bounds=[775,54][1145,262] ...
+        final Pattern BOUNDS_REGEX = Pattern.compile(
+                String.format("taskId=\\d+: %s/%s bounds=\\[(\\d+),(\\d+)\\]\\[(\\d+),(\\d+)\\]",
+                        packageName, activityName));
+        String output = mCmdHelper.executeAmStackInfo(stackId);
+        Log.d(LOG_TAG, "getBounds output=" + output);
+        Matcher matcher = BOUNDS_REGEX.matcher(output);
+        if (matcher.find()) {
+            int left = Integer.parseInt(matcher.group(1));
+            int top = Integer.parseInt(matcher.group(2));
+            int right = Integer.parseInt(matcher.group(3));
+            int bottom = Integer.parseInt(matcher.group(4));
+            Log.i(LOG_TAG, String.format("Bounds found: [%d,%d][%d,%d] for %s/%s",
+                    left, top, right, bottom, packageName, activityName));
+            return new Rect(left, top, right, bottom);
+        }
+        Log.w(LOG_TAG, "getBounds returns null");
+        return null;
+    }
+
+    /**
+     * Setup expectations: PIP is open.
+     * <p>
+     * Moves the PIP window to full screen.
+     * </p>
+     */
+    public void executeCommandPipToFullscreen(String packageName, String activityName,
+            boolean throwIfFail) {
+        int taskId = getTaskId(packageName, activityName);
+        if (taskId != INVALID_TASK_ID) {
+            mCmdHelper.executeAmStackMovetask(taskId,
+                    FULLSCREEN_WORKSPACE_STACK_ID);
+        }
+        if (throwIfFail && isPipOnScreen(activityName)) {
+            throw new UnknownUiException("Failed to move a PIP window to fullscreen");
+        }
+    }
+
+    /**
+     * Get the current playback state for a given package that owns the media session.
+     * @param packageName the package name of media session owner
+     * @return
+     * 0 = PlaybackState.STATE_NONE
+     * 1 = PlaybackState.STATE_STOPPED
+     * 2 = PlaybackState.STATE_PAUSED
+     * 3 = PlaybackState.STATE_PLAYING
+     */
+    public int getPlaybackState(String packageName) {
+        String output = mCmdHelper.executeDumpsysMediaSession();
+        // Parse the output of dumpsys media_session.
+        // Example :
+        // LeanbackSampleApp com.example.android.tvleanback/LeanbackSampleApp
+        //   package=com.example.android.tvleanback
+        //   ...
+        //   state=PlaybackState {state=3, position=0, buffered position=0, speed=1.0, updated=...}
+        int playbackState = 0;
+        int index = output.indexOf(String.format("package=%s", packageName));
+        if (index == -1) {
+            Log.w(LOG_TAG, String.format("No media session found for the package: %s", packageName));
+            return playbackState;
+        }
+        final Pattern PLAYBACKSTATE_REGEX = Pattern.compile(
+                "\\s*state=PlaybackState \\{state=(\\d+),.*");
+        Matcher matcher = PLAYBACKSTATE_REGEX.matcher(output.substring(index));
+        if (matcher.find()) {
+            playbackState = Integer.parseInt(matcher.group(1));
+            Log.i(LOG_TAG, String.format("PlaybackState=%s package=%s", playbackState, packageName));
+        }
+        return playbackState;
+    }
+
+    /**
+     * Setup expectation: None. Check if PIP overlay is shown and focused.
+     */
+    private boolean isPipStateOverlay() {
+        // TODO
+        throw new UnsupportedOperationException("This method is not yet implemented.");
+    }
+
+    /**
+     * Setup expectation: None. Check if PIP menu is shown in center.
+     */
+    public boolean isPipStateMenu(String packageName, String activityName) {
+        return PIP_MENU_BOUNDS.equals(getPipBounds(packageName, activityName));
+    }
+
+    /**
+     * Setup expectation: None. Check if the PIP is shown in Recents with focus.
+     */
+    public boolean isPipStateRecentsFocused(String packageName, String activityName) {
+        return PIP_RECENTS_FOCUSED_BOUNDS.equals(getPipBounds(packageName, activityName));
+    }
+
+    /**
+     * Setup expectation: None. Check if the PIP is shown with Settings.
+     */
+    public boolean isPipStateSettings(String packageName, String activityName) {
+        return PIP_SETTINGS_BOUNDS.equals(getPipBounds(packageName, activityName));
+    }
+
+    /**
+     * Setup expectation: When the PIP is shown in Recents with focus.
+     * <p>
+     * Toggles the media play/pause button on screen.
+     * </p>
+     */
+    public void togglePipMediaControls() {
+        UiObject2 pause = mDevice.findObject(By.res(UI_PACKAGE, "button").desc("Pause"));
+        UiObject2 play = mDevice.findObject(By.res(UI_PACKAGE, "button").desc("Play"));
+        if (pause != null) {
+            pause.click();
+        } else if (play != null) {
+            play.click();
+        } else {
+            throw new UnknownUiException("No Play/Pause button found in PIP in Recents");
+        }
+        mDevice.waitForIdle();
+        mDevice.pressDPadCenter();
+    }
+
+    /**
+     * Setup expectation: When the PIP is shown in Recents with focus.
+     * <p>
+     * Clicks the full screen button on screen.
+     * </p>
+     */
+    public void selectPipToFullScreenButton() {
+        UiObject2 button = mDevice.wait(
+                Until.findObject(By.res(UI_PACKAGE, "button").desc("Full screen")),
+                SHORT_SLEEP_MS);
+        button.click();
+        mDevice.waitForIdle();
+        mDevice.pressDPadCenter();
+    }
+
+    /**
+     * Setup expectation: When the PIP is shown in Recents with focus.
+     * <p>
+     * Clicks the Close button on screen.
+     * </p>
+     */
+    public void selectPipCloseButton() {
+        UiObject2 button = mDevice.wait(
+                Until.findObject(By.res(UI_PACKAGE, "button").desc("Close PIP")),
+                SHORT_SLEEP_MS);
+        button.click();
+        mDevice.waitForIdle();
+        mDevice.pressDPadCenter();
+
+    }
+
+    /**
+     * Setup expectation: None.
+     * <p>
+     * Check if the PIP is shown in Recents without focus.
+     * </p>
+     */
+    public boolean isPipStateRecents(String packageName, String activityName) {
+        return PIP_RECENTS_BOUNDS.equals(getPipBounds(packageName, activityName));
+    }
+
+    private int getTaskId(String packageName, String activityName) {
+        int taskId = INVALID_TASK_ID;
+        final Pattern TASK_REGEX = Pattern.compile(
+                String.format("taskId=(\\d+): %s/%s", packageName, activityName));
+        Matcher matcher = TASK_REGEX.matcher(mCmdHelper.executeAmStackList());
+        if (matcher.find()) {
+            taskId = Integer.parseInt(matcher.group(1));
+            Log.i(LOG_TAG, String.format("TaskId found: %d for %s/%s",
+                    taskId, packageName, activityName));
+        }
+        return taskId;
+    }
+}
+
diff --git a/libraries/first-party-app-helpers/tv/sysui-app-helper/src/android/platform/test/helpers/tv/SysUiRecentsHelperImpl.java b/libraries/first-party-app-helpers/tv/sysui-app-helper/src/android/platform/test/helpers/tv/SysUiRecentsHelperImpl.java
new file mode 100644
index 0000000..0872955
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/sysui-app-helper/src/android/platform/test/helpers/tv/SysUiRecentsHelperImpl.java
@@ -0,0 +1,273 @@
+/*
+ * 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.tv;
+
+import android.app.Instrumentation;
+import android.os.RemoteException;
+import android.platform.test.helpers.AbstractLeanbackAppHelper;
+import android.platform.test.helpers.DPadHelper;
+import android.platform.test.helpers.IRecentsHelper;
+import android.platform.test.helpers.exceptions.UiTimeoutException;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+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;
+
+/**
+ * App helper implementation class for Recents activity in System UI
+ */
+public class SysUiRecentsHelperImpl extends AbstractLeanbackAppHelper implements IRecentsHelper {
+    private static final String LOG_TAG = SysUiRecentsHelperImpl.class.getSimpleName();
+    private static final String UI_PACKAGE = "com.android.systemui";
+
+    private static final long RECENTS_SELECTION_TIMEOUT = 5000;
+    private static final String TEXT_NO_ITEMS = "No recent items";
+
+    private DPadHelper mDPadHelper;
+
+
+    public SysUiRecentsHelperImpl(Instrumentation instrumentation) {
+        super(instrumentation);
+        mDPadHelper = DPadHelper.getInstance(instrumentation);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        throw new UnsupportedOperationException("This method is not supported for Recents");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void open() {
+        try {
+            mDevice.pressRecentApps();
+            mDevice.waitForIdle();
+        } catch (RemoteException ex) {
+            Log.e(LOG_TAG, ex.toString());
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void exit() {
+        mDevice.pressHome();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        // Nothing to do.
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void flingRecents(Direction dir) {
+        throw new UnsupportedOperationException("This method is not supported for Recents "
+                + "in leanback library");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void clearAll() {
+        final int MAX_ATTEMPTS = 10;
+        int attempt = 0;
+        while (attempt < MAX_ATTEMPTS) {
+            if (dismissTask() == false) {
+                break;
+            }
+            ++attempt;
+            // Once all tasks are dismissed, it exits to Home screen
+            if (!isAppInForeground()) {
+                break;
+            }
+        }
+        Log.i(LOG_TAG, String.format("%d task(s) is dismissed.", attempt));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean hasContent() {
+        if (!isAppInForeground()) {
+            throw new IllegalStateException("Recents is not open.");
+        }
+        return (getTaskCountOnScreen() > 0);
+    }
+
+    /**
+     * @return True if there is no content and it shows the "No recent items" text on screen.
+     */
+    public boolean hasNoRecentItems() {
+        return mDevice.hasObject(By.text(TEXT_NO_ITEMS));
+    }
+
+    /**
+     * Setup expectations: The target application is open in Recents.
+     * <p>
+     * Selects a given application.
+     * </p>
+     * @return True if it selects a given application.
+     */
+    public boolean selectTask(String appName) {
+        boolean found = (findTask(appName) != null);
+        if (!found) {
+            Log.w(LOG_TAG, String.format("The application not found in Recents: %s", appName));
+        }
+        return found;
+    }
+
+    /**
+     * Setup expectations: "Recents" is open.
+     * @return the name for an application currently selected in Recents
+     */
+    public String getFocusedTaskName() {
+        UiObject2 taskView = mDevice.wait(Until.findObject(getTaskViewSelector()),
+                RECENTS_SELECTION_TIMEOUT);
+        if (taskView == null) {
+            throw new UiTimeoutException("No task view found in Recents");
+        }
+        return taskView.findObject(By.focused(true)).findObject(
+                By.res(UI_PACKAGE, "card_title_text")).getText();
+    }
+
+    /**
+     * Setup expectations: "Recents" is open.
+     * @return the number of tasks on the Recents screen. This may differ from the number of
+     * the actual tasks.
+     */
+    public int getTaskCountOnScreen() {
+        return mDevice.findObject(getTaskViewSelector()).getChildCount();
+    }
+
+    /**
+     * Returns a {@link BySelector} describing the task to be dismissed in Recents
+     * @return
+     */
+    private BySelector getTaskDismissSelector() {
+        // The text "Dismiss" appears only when the card is selected to be dismissed on leanback.
+        return By.pkg(UI_PACKAGE).focused(true)
+                .hasChild(By.res(UI_PACKAGE, "card_dismiss_text").text("Dismiss"));
+    }
+
+    /**
+     * Returns a {@link BySelector} describing the task view in Recents
+     * @return
+     */
+    private BySelector getTaskViewSelector() {
+        return By.res(UI_PACKAGE, "task_list");
+    }
+
+    /**
+     * Setup expectations: "Recents" is open. This method will dismiss a focused task in Recents.
+     * @return True if a task is dismissed
+     */
+    public boolean dismissTask() {
+        if (hasNoRecentItems()) {
+            Log.i(LOG_TAG, "No recent items found");
+            return false;
+        }
+
+        if (!mDevice.wait(Until.hasObject(getTaskViewSelector()), RECENTS_SELECTION_TIMEOUT)) {
+            throw new UnknownUiException("No task view found in Recents");
+        }
+        UiObject2 task = mDevice.findObject(getTaskDismissSelector());
+        if (task == null) {
+            // Select a task to be dismissed again by pressing the down key
+            mDevice.pressDPadDown();
+            task = mDevice.wait(Until.findObject(getTaskDismissSelector()),
+                    RECENTS_SELECTION_TIMEOUT);
+            if (task == null) {
+                throw new UnknownUiException("Dismiss button not found");
+            }
+        }
+        mDevice.pressDPadCenter();
+        // Confirm that the task is dismissed
+        return mDevice.wait(Until.gone(getTaskDismissSelector()), RECENTS_SELECTION_TIMEOUT);
+    }
+
+    private UiObject2 findTask(String appName) {
+        UiObject2 taskView = mDevice.wait(Until.findObject(getTaskViewSelector()),
+                RECENTS_SELECTION_TIMEOUT);
+        UiObject2 app;
+        final int MAX_ATTEMPTS = 10;
+        if ((app = findTask(taskView, appName, Direction.LEFT, MAX_ATTEMPTS)) != null) {
+            return app;
+        }
+        if ((app = findTask(taskView, appName, Direction.RIGHT, MAX_ATTEMPTS)) != null) {
+            return app;
+        }
+        return null;
+    }
+
+    private UiObject2 findTask(UiObject2 container, String appName, Direction direction,
+            int maxAttempts) {
+        UiObject2 focused = container.findObject(By.focused(true));
+        if (focused == null) {
+            throw new UnknownUiException("No focused item found in Recents");
+        }
+        String currentName = focused.findObject(By.res(UI_PACKAGE, "card_title_text")).getText();
+        String nextName;
+        int attempt = 0;
+        boolean found = false;
+        while (!(found = appName.equalsIgnoreCase(currentName)) && attempt++ < maxAttempts) {
+            mDPadHelper.pressDPad(direction);
+            nextName = container.findObject(By.focused(true)).findObject(
+                    By.res(UI_PACKAGE, "card_title_text")).getText();
+            if (currentName.equals(nextName)) {
+                // It reaches to the end in this direction
+                Log.d(LOG_TAG, String.format("%s not found in Recents until it reaches to the end",
+                        appName));
+                return null;
+            } else {
+                currentName = nextName;
+            }
+        }
+
+        if (found) {
+            return focused;
+        }
+        Log.d(LOG_TAG, String.format("%s not found in Recents by moving next %d times",
+                appName, maxAttempts));
+        return null;
+    }
+}
+
diff --git a/libraries/first-party-app-helpers/tv/sysui-app-helper/src/android/platform/test/helpers/tv/SysUiSettingsHelperImpl.java b/libraries/first-party-app-helpers/tv/sysui-app-helper/src/android/platform/test/helpers/tv/SysUiSettingsHelperImpl.java
new file mode 100644
index 0000000..b996c5e
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/sysui-app-helper/src/android/platform/test/helpers/tv/SysUiSettingsHelperImpl.java
@@ -0,0 +1,486 @@
+/*
+ * 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.tv;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.platform.test.helpers.AbstractLeanbackAppHelper;
+import android.platform.test.helpers.DPadHelper;
+import android.platform.test.helpers.exceptions.UiTimeoutException;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.provider.Settings;
+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;
+
+import java.util.regex.Pattern;
+
+
+/**
+ * App helper implementation class for TV Settings
+ */
+public class SysUiSettingsHelperImpl extends AbstractLeanbackAppHelper {
+    private static final String LOG_TAG = SysUiSettingsHelperImpl.class.getSimpleName();
+    private static final String UI_PACKAGE = "com.android.tv.settings";
+    private static final String ACTION_SETTINGS = Settings.ACTION_SETTINGS;
+    private static final String RES_PKG_ANDROID = "android";
+    private static final String RES_TITLE = "title";
+    private static final String RES_SUMMARY = "summary";
+    private static final long SHORT_SLEEP_MS = 3000;
+
+    public static final int SETTINGS_SYSTEM = 0;
+    public static final int SETTINGS_SECURE = 1;
+    public static final int SETTINGS_GLOBAL = 2;
+
+    private Context mContext;
+
+
+    public SysUiSettingsHelperImpl(Instrumentation instrumentation) {
+        super(instrumentation);
+        mDPadHelper = DPadHelper.getInstance(instrumentation);
+        mContext = instrumentation.getContext();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        throw new UnsupportedOperationException("This method is not supported for Settings");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void open() {
+        open(ACTION_SETTINGS, SHORT_SLEEP_MS);
+    }
+
+    /**
+     * Setup expectation: On the launcher home screen.
+     * <p>
+     * Launches the desired application and wait for it to begin running before returning.
+     * </p>
+     * @param timeoutMs timeout in milliseconds to open an activity
+     */
+    public void open(String action, long timeoutMs) {
+        launchActivity(action);
+        if (timeoutMs > 0 && !waitForOpen(timeoutMs)) {
+            throw new UiTimeoutException(String.format("Timed out to open a target package %s:"
+                    + " %d(ms)", getPackage(), timeoutMs));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void exit() {
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        // Nothing to do.
+    }
+
+    /**
+     * Setup expectations: The Settings is open.
+     * <p>
+     * Selects the main Settings item by title text.
+     * </p>
+     * @param title the title string of the setting to find
+     * @return true if the setting that matches the name is open
+     */
+    public boolean clickSetting(String title) {
+        UiObject2 setting = findSettingByTitle(title);
+        if (setting == null) {
+            return false;
+        }
+        mDPadHelper.pressDPadCenterAndWait(Until.newWindow(), SHORT_SLEEP_MS);
+        return true;
+    }
+
+    /**
+     * Setup expectations: The Settings is open. Selects the main Settings item by summary text.
+     * @param summary the summary string of the setting to find
+     * @return true if the setting that matches the name is open
+     */
+    public boolean clickSettingBySummary(String summary) {
+        UiObject2 setting = findSettingBySummary(summary);
+        if (setting == null) {
+            return false;
+        }
+        mDPadHelper.pressDPadCenterAndWait(Until.newWindow(), SHORT_SLEEP_MS);
+        return true;
+    }
+
+    /**
+     * Setup expectations: The Settings is open.
+     * @param title the title string of the setting to find
+     * @return true if it finds {@link UiObject2} that has a given title text
+     */
+    public boolean hasSettingByTitle(String title) {
+        return (findSettingByTitle(title) != null);
+    }
+
+    /**
+     * Setup expectations: The Settings is open.
+     * @param summary the summary string of the setting to find
+     * @return true if it finds {@link UiObject2} that has a given summary text
+     */
+    public boolean hasSettingBySummary(String summary) {
+        return (findSettingBySummary(summary) != null);
+    }
+
+    /**
+     * Setup expectations: The Settings is open. Finds the setting that matches both
+     * a given title and summary
+     * @param title the title string of the setting to find
+     * @param summary the summary string of the setting to find
+     * @return true if it finds {@link UiObject2} that has both title and summary text passed.
+     */
+    public boolean hasSettingByTitleAndSummary(String title, String summary) {
+        return (findSettingByTitleAndSummary(title, summary) != null);
+    }
+
+    /**
+     * Setup expectations: The Settings is open.
+     * @param text the name of the setting to find
+     * @return true if it finds {@link UiObject2} that has a given text either in title or summary
+     */
+    public boolean hasSettingByTitleOrSummary(String text) {
+        return (findSettingByTitleOrSummary(text) != null);
+    }
+
+    /**
+     * Setup expectations: The Settings is open. Checks if the switch bar is turned on.
+     * @param title the name of the setting to check
+     * @return true if the setting is turned on
+     */
+    public boolean isSwitchBarOn(String title) {
+        return "ON".equals(getSwitchBarText(title));
+    }
+
+    /**
+     * Setup expectations: The Settings is open. Checks if the switch bar is turned off.
+     * @param title the name of the setting to check
+     * @return true if the setting is turned off
+     */
+    public boolean isSwitchBarOff(String title) {
+        return "OFF".equals(getSwitchBarText(title));
+    }
+
+    /**
+     * Setup expectations: The Accessibility Settings is open. Finds the preview text on screen.
+     * @return true if the preview text is displayed on screen.
+     */
+    public boolean hasPreviewText() {
+        return mDevice.hasObject(By.res(UI_PACKAGE, "preview_text"));
+    }
+
+    /**
+     * Setup expectations: The Settings is open.
+     * @return {@link UiObject2} of the current focused setting
+     */
+    public String getCurrentFocusedSettingTitle() {
+        return mDevice.wait(Until.findObject(By.focused(true)), SHORT_SLEEP_MS)
+                .findObject(By.res(RES_PKG_ANDROID, "title")).getText();
+    }
+
+    /**
+     * Setup expectations: The Settings is open. Returns the summary text of the selected Settings.
+     * @param title the name of the setting to get the summary text
+     * @return String of the summary text
+     */
+    public String getSummaryTextByTitle(String title) {
+        UiObject2 settings = findSettingByTitle(title);
+        return settings.findObject(By.res(RES_PKG_ANDROID, RES_SUMMARY)).getText();
+    }
+
+    /**
+     * Setup expectations: The Settings is open.
+     *
+     * Exit the guided settings by pressing BACK key a given times
+     * @param maxDepth The maximum depth to exit the guided settings. Should be greater than 0
+     * @return true if the Settings is closed.
+     */
+    public boolean goBackGuidedSettings(int maxDepth) {
+        if (maxDepth < 1) {
+            Log.w(LOG_TAG, "maxDepth should be greater than 0");
+            maxDepth = 1;
+        }
+        UiObject2 focused = mDevice.wait(Until.findObject(By.focused(true)), SHORT_SLEEP_MS);
+        if (focused == null) {
+            throw new IllegalStateException("No focused item is found");
+        }
+        while (maxDepth-- > 0) {
+            mDPadHelper.pressBack();
+            if (!waitForOpen(SHORT_SLEEP_MS)) {
+                Log.w(LOG_TAG, "Settings is closed.");
+                return false;
+            } else if (focused.equals(
+                    mDevice.wait(Until.findObject(By.focused(true)), SHORT_SLEEP_MS))) {
+                Log.w(LOG_TAG, "The focused is the same. Nothing happened in Settings?");
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Setup expectations: On waiting for a guided setting to open.
+     * @param title the title string of the guided setting
+     * @param timeoutMs timeout in milliseconds to get the header title
+     * @return true if the guided setting that has a given title is open in timeout
+     */
+    public boolean waitForOpenGuidedSetting(String title, long timeoutMs) {
+        UiObject2 header = mDevice.wait(
+                Until.findObject(By.res(UI_PACKAGE, "decor_title").text(title)), timeoutMs);
+        return (header != null);
+    }
+
+    /**
+     * Setup expectations: The Settings is open.
+     * @return the title of the guided settings
+     */
+    private String getGuidedSettingTitle() {
+        UiObject2 header = mDevice.findObject(By.res(UI_PACKAGE, "decor_title"));
+        if (header == null) {
+            throw new UnknownUiException("Header text is not found");
+        }
+        return header.getText();
+    }
+
+    public String getStringSetting(int type, String name) {
+        switch (type) {
+            case SETTINGS_SYSTEM:
+                return Settings.System.getString(mContext.getContentResolver(), name);
+            case SETTINGS_GLOBAL:
+                return Settings.Global.getString(mContext.getContentResolver(), name);
+            case SETTINGS_SECURE:
+                return Settings.Secure.getString(mContext.getContentResolver(), name);
+        }
+        return "";
+    }
+
+    public int getIntSetting(int type, String name, int def) {
+        int value = getIntSetting(type, name);
+        return value != Integer.MIN_VALUE ? value : def;
+    }
+
+    public int getIntSetting(int type, String name) {
+        try {
+            switch (type) {
+                case SETTINGS_SYSTEM:
+                    return Settings.System.getInt(mContext.getContentResolver(), name);
+                case SETTINGS_GLOBAL:
+                    return Settings.Global.getInt(mContext.getContentResolver(), name);
+                case SETTINGS_SECURE:
+                    return Settings.Secure.getInt(mContext.getContentResolver(), name);
+            }
+        } catch (Settings.SettingNotFoundException e) {
+            Log.w(LOG_TAG, String.format("Settings not found name=%s, type=%d", name, type));
+        }
+        return Integer.MIN_VALUE;
+    }
+
+    public boolean isDeveloperOptionsEnabled() {
+        return getIntSetting(SETTINGS_GLOBAL, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED,
+                android.os.Build.TYPE.equals("eng") ? 1 : 0) == 1;
+    }
+
+    /**
+     * Setup expectations: PIN code activity is open. Set a new PIN code
+     * @param pinCode the PIN code of 4 digits
+     * @return true if a PIN code is set
+     */
+    public boolean setNewPinCode(String pinCode) {
+        return setPinCode("Set a new PIN", pinCode, Direction.DOWN);
+    }
+
+    public boolean reenterPinCode(String pinCode) {
+        return setPinCode("Re-enter new PIN", pinCode, Direction.DOWN);
+    }
+
+    public boolean enterPinCode(String pinCode) {
+        return setPinCode("Enter PIN", pinCode, Direction.DOWN);
+    }
+
+    protected BySelector getTitleSelector(String text) {
+        return By.res(RES_PKG_ANDROID, RES_TITLE).text(text);
+    }
+
+    protected BySelector getSummarySelector(String text) {
+        return By.res(RES_PKG_ANDROID, RES_SUMMARY).text(text);
+    }
+
+    /**
+     * Find the focused item in Settings that has a descendant matches the criteria.
+     * @param selector for a descendant of the setting to match.
+     * @param direction the direction to find, only accepts UP and DOWN.
+     * @return {@link UiObject2} of the focusable item that has a given descendant
+     */
+    private UiObject2 findSettingHasDescendant(BySelector selector, Direction direction) {
+        if (!isAppInForeground()) {
+            throw new IllegalStateException("Required to open the Settings ahead");
+        }
+        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;
+        UiObject2 found = null;
+        while (!currentFocused.equals(prevFocused)) {
+            if ((found = mDevice.findObject(By.focused(true).hasDescendant(selector, 3)))
+                    != null) {
+                return 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 item until it reaches the end.");
+        return found;
+    }
+
+    private String getSwitchBarText(String title) {
+        UiObject2 setting = findSettingByTitle(title);
+        return setting.findObject(By.res(RES_PKG_ANDROID, "switch_widget")).getText();
+    }
+
+    private UiObject2 findSettingByTitle(String title) {
+        return findSettingBySelector(getTitleSelector(title), true);
+    }
+
+    private UiObject2 findSettingBySummary(String summary) {
+        return findSettingBySelector(getSummarySelector(summary), true);
+    }
+
+    private UiObject2 findSettingByTitleAndSummary(String title, String summary) {
+        BySelector titleSelector = getTitleSelector(title);
+        BySelector summarySelector = getSummarySelector(summary);
+        UiObject2 setting;
+        while ((setting = findSettingHasDescendant(titleSelector, Direction.DOWN)) != null) {
+            if (setting.hasObject(summarySelector)) {
+                return setting;
+            }
+        }
+        while ((setting = findSettingHasDescendant(titleSelector, Direction.UP)) != null) {
+            if (setting.hasObject(summarySelector)) {
+                return setting;
+            }
+        }
+        return null;
+    }
+
+    private UiObject2 findSettingByTitleOrSummary(String text) {
+        final Pattern RES_REGEX = Pattern.compile(
+                String.format("%s:id/(%s|%s)", RES_PKG_ANDROID, RES_TITLE, RES_SUMMARY));
+        return findSettingBySelector(By.res(RES_REGEX).text(text), true);
+    }
+
+    private UiObject2 findSettingBySelector(BySelector selector, boolean throwIfFail) {
+        UiObject2 setting;
+        if ((setting = findSettingHasDescendant(selector, Direction.DOWN)) != null) {
+            return setting;
+        }
+        if ((setting = findSettingHasDescendant(selector, Direction.UP)) != null) {
+            return setting;
+        }
+        if (throwIfFail) {
+            throw new UnknownUiException(
+                    String.format("No focused setting matches a given selector: %s",
+                            selector.toString()));
+        }
+        return null;
+    }
+
+    private boolean setPinCode(String title, String pinCode, Direction direction) {
+        if (!isValidPinCode(pinCode)) {
+            throw new IllegalArgumentException("4 digits PIN code is valid. pinCode=" + pinCode);
+        }
+        if (direction != Direction.DOWN && direction != Direction.UP) {
+            throw new IllegalArgumentException("Either up or down is allowed");
+        }
+        if (!mDevice.wait(Until.hasObject(By.res(getPackage(), "title").text(title)),
+                SHORT_SLEEP_MS)) {
+            throw new IllegalStateException("The title for PIN code not found: " + title);
+        }
+
+        // the PIN number starts from 0 and increases by going down
+        for (char c : pinCode.toCharArray()) {
+            int number = c - '0';
+            // Note that the resource ID for the number changes by the direction to search
+            String resId = (number == 0) ? "current_number"
+                    : (direction == Direction.DOWN) ? "next_number" : "previous_number";
+            mDPadHelper.pressDPad(direction, number);
+            if (!mDevice.wait(
+                    Until.hasObject(By.res(getPackage(), resId).text(String.valueOf(c))),
+                    SHORT_SLEEP_MS)) {
+                throw new UnknownUiException("Couldn't find the number:" + c);
+            }
+            mDPadHelper.pressDPadCenter();  // Move next
+        }
+        return true;
+    }
+
+    private boolean isValidPinCode(String pinCode) {
+        final String PIN_CODE_FORMAT = "[0-9][0-9][0-9][0-9]";
+        return pinCode != null && pinCode.matches(PIN_CODE_FORMAT);
+    }
+
+    /**
+     * Setup expectations: None
+     *
+     * Starts the Settings activity
+     */
+    // TODO Move to a base or utility class that each test could access
+    private void launchActivity() {
+        launchActivity(ACTION_SETTINGS);
+    }
+
+    private void launchActivity(String action) {
+        Intent intent = new Intent(action);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        Log.d(LOG_TAG, "launchActivity intent=" + intent.toString());
+        mInstrumentation.getContext().startActivity(intent);
+    }
+}
diff --git a/libraries/first-party-app-helpers/tv/youtube-app-helper/Android.mk b/libraries/first-party-app-helpers/tv/youtube-app-helper/Android.mk
new file mode 100644
index 0000000..7c1d572
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/youtube-app-helper/Android.mk
@@ -0,0 +1,24 @@
+#
+# 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)
+LOCAL_MODULE := tv-youtube-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
diff --git a/libraries/first-party-app-helpers/tv/youtube-app-helper/src/android/platform/test/helpers/tv/YouTubeHelperImpl.java b/libraries/first-party-app-helpers/tv/youtube-app-helper/src/android/platform/test/helpers/tv/YouTubeHelperImpl.java
new file mode 100644
index 0000000..3646402
--- /dev/null
+++ b/libraries/first-party-app-helpers/tv/youtube-app-helper/src/android/platform/test/helpers/tv/YouTubeHelperImpl.java
@@ -0,0 +1,389 @@
+/*
+ * 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.tv;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.platform.test.helpers.AbstractLeanbackAppHelper;
+import android.platform.test.helpers.exceptions.UiTimeoutException;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+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;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.TimeZone;
+
+
+public class YouTubeHelperImpl extends AbstractLeanbackAppHelper {
+
+    private static final String TAG = YouTubeHelperImpl.class.getSimpleName();
+    private static final String UI_PACKAGE = "com.google.android.youtube.tv";
+    private static final String RES_MAIN_ACTIVITY_ID = "top_layout";
+    private static final long SHORT_SLEEP_MS = 5000;    // 5 seconds
+    private static final long LONG_SLEEP_MS = 30000;    // 30 seconds
+    private static final long LOADING_CONTENT_TIMEOUT_MS = 5000;
+
+
+    public YouTubeHelperImpl(Instrumentation instrumentation) {
+        super(instrumentation);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "YouTube";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected BySelector getMainActivitySelector() {
+        return By.res(UI_PACKAGE, RES_MAIN_ACTIVITY_ID);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected BySelector getBrowseHeadersSelector() {
+        return By.res(UI_PACKAGE, "guide").hasChild(By.selected(true));
+    }
+
+    /**
+     * Selects search orb.
+     */
+    public void selectSearchOrb() {
+        returnToMainActivity();
+        UiObject2 searchOrb = null;
+
+        final int MAX_ATTEMPTS_SEARCH_ORB = 20;
+        int attempt = 0;
+        // Wait until the search orb appears at runtime.
+        while (attempt++ < MAX_ATTEMPTS_SEARCH_ORB) {
+            searchOrb = mDevice.wait(Until.findObject(
+                    By.res(UI_PACKAGE, "title_orb").clickable(true)), SHORT_SLEEP_MS);
+            if (searchOrb == null) {
+                // Search orb could be found at the top of activity
+                mDPadHelper.pressDPad(Direction.UP);
+                continue;
+            }
+        }
+        if (attempt == MAX_ATTEMPTS_SEARCH_ORB) {
+            throw new UnknownUiException("Failed to select search orb");
+        }
+        searchOrb.click();
+    }
+
+    /**
+     * Search for a given query text in YouTube app
+     */
+    public void search(String query) {
+        selectSearchOrb();
+
+        UiObject2 editText = mDevice.wait(
+                Until.findObject(By.res(UI_PACKAGE, "lb_search_text_editor")),
+                5 * 60 * 1000);
+        if (editText == null) {
+            throw new UnknownUiException("Search text editor not found");
+        }
+        if (!editText.isFocused()) {
+            Log.d(TAG, "Search text editor is getting focus");
+            mDevice.pressDPadRight();
+            SystemClock.sleep(SHORT_SLEEP_MS);
+        }
+        editText.setText(query);
+        mDevice.waitForIdle();
+        mDevice.pressEnter();
+        if (!waitForContentLoaded(SHORT_SLEEP_MS)) {
+            throw new UiTimeoutException(
+                    String.format("Failed to find the search results in %d (ms)", SHORT_SLEEP_MS));
+        }
+    }
+
+    /**
+     * Setup expectations: YouTube search result is open and the first result is focused.
+     *
+     * Open the first visible search result in the list and block until the search result
+     * comes in the foreground.
+     */
+    public void openFirstSearchResult() {
+        openSearchResultByIndex(0);
+    }
+
+    /**
+     * Setup expectations: YouTube search result is open and the first result is focused.
+     *
+     * Open the (index)'th visible search result in the list and block until the search result
+     * comes in the foreground.
+     */
+    public void openSearchResultByIndex(int index) {
+        if (!isInSearchPage()) {
+            throw new IllegalStateException("Must be in search page to select search results");
+        }
+        UiObject2 rowContent = mDevice.wait(Until.findObject(
+                By.res(UI_PACKAGE, "row_content")), SHORT_SLEEP_MS);
+        if (rowContent == null) {
+            throw new IllegalStateException("No search results found");
+        }
+
+        // Select a content by index
+        UiObject2 focused = rowContent.findObject(By.focused(true));
+        if (focused == null) {
+            throw new IllegalStateException("The search result is not selected");
+        }
+        UiObject2 current;
+        for (int i = 0; i < index; ++i) {
+            mDevice.pressDPadRight();
+            SystemClock.sleep(SHORT_SLEEP_MS);
+            current = rowContent.findObject(By.focused(true));
+            if (focused.equals(current)) {
+                Log.w(TAG, "openSearchResultByIndex: the index is out of bounds.");
+                break;
+            } else {
+                focused = current;
+            }
+        }
+        mDevice.pressDPadCenter();
+
+        // Wait until the content is open
+        if (!mDevice.wait(Until.gone(By.res(UI_PACKAGE, "lb_search_bar_items")),
+                LOADING_CONTENT_TIMEOUT_MS)) {
+            throw new UiTimeoutException("Opening search result timed out");
+        }
+    }
+
+    /**
+     * Setup expectations: Loading content in the app
+     *
+     * This method blocks until the content is loaded in a content row or a search row
+     *
+     * @param timeout wait timeout in milliseconds
+     * @return true if the content is loaded within timeout, false otherwise
+     */
+    public boolean waitForContentLoaded(long timeout) {
+        return mDevice.wait(
+                Until.hasObject(By.res(UI_PACKAGE, "row_content").hasChild(By.selected(true))),
+                timeout);
+    }
+
+    /**
+     * Setup expectations: Sign-in page is open.
+     *
+     * Selects the account to use if no account has been set up.
+     */
+    public boolean signIn(String account) {
+        if (!"Sign in".equals(getGuidanceTitleText())) {
+            throw new IllegalStateException("This method should be called in the Sign-in page.");
+        }
+        if (selectGuidedAction(account) == null) {
+            Log.e(TAG, String.format("No account matches: %s", account));
+            return false;
+        }
+        mDPadHelper.pressDPadCenter();
+        return mDevice.wait(Until.hasObject(getMainActivitySelector()), SHORT_SLEEP_MS);
+    }
+
+    /**
+     * Setup expectations: On browse fragment. Sign out
+     */
+    public void signOut() {
+        openSettings();
+        if (!hasCardInRow("Sign out")) {
+            throw new UnknownUiException("Sign out is not found");
+        }
+        mDPadHelper.pressDPadCenter();
+        mDevice.wait(Until.findObject(By.res(getPackage(), "title_text").text("Sign in")),
+                SHORT_SLEEP_MS);
+    }
+
+    /**
+     * Setup expectations: The main activity is open.
+     *
+     * Returns true if no user is signed in the app.
+     */
+    public boolean isNoUserSignedIn() {
+        // Some sections "Subscriptions", "History", "Purchases" are not available
+        // with no user signed in
+        final String[] SECTIONS_FOR_SIGNED_IN_USER = {"Subscriptions", "History", "Purchases"};
+        for (String section : SECTIONS_FOR_SIGNED_IN_USER) {
+            if (mDevice.hasObject(By.res(getPackage(), "row_header").text(section))) {
+                Log.d(TAG, "The section for a signed in user is found: " + section);
+                return false;
+            }
+        }
+
+        // Open Settings and confirm that the Sign-in card is shown
+        openSettings();
+        return hasCardInRow("Sign in");
+    }
+
+    /**
+     * Setup expectations: On browse fragment
+     *
+     * Returns the name of account currently signed in
+     */
+    public String getSignInUserName() {
+        openSettings();
+        return getCardContentText("Sign out");
+    }
+
+    private boolean isInSearchPage() {
+        return mDevice.hasObject(By.res(UI_PACKAGE, "search_fragment"));
+    }
+
+    /**
+     * @return true if YouTube plays a video in the foreground
+     */
+    public boolean isInVideoPlayback() {
+        return isInVideoPlayback(0);
+    }
+
+    private boolean isInVideoPlayback(long timeoutMs) {
+        if (!isAppInForeground()) {
+            Log.w(TAG, "YouTube was closed.");
+            return false;
+        }
+        return mDevice.wait(Until.hasObject(By.res(UI_PACKAGE, "watch_player")), timeoutMs);
+    }
+
+    /**
+     * Open the Popular on YouTube section
+     */
+    public void openPopularOnYouTube() {
+        openHeader("Popular on YouTube");
+    }
+
+    public void openHome() {
+        openHeader("Home");
+    }
+
+    public void openSettings() {
+        openHeader("Settings");
+    }
+
+    private UiObject2 getFocusedVideoCard() {
+        BySelector cardSelector = By.focused(true).hasChild(
+                By.res(getPackage(), "image_card"));
+        return mDevice.wait(Until.findObject(cardSelector), SHORT_SLEEP_MS);
+    }
+
+    /**
+     * Setup expectations: YouTube is open with a focused video.
+     * @return the duration in milliseconds of a focused video
+     */
+    public long getFocusedVideoDuration() {
+        UiObject2 card = getFocusedVideoCard();
+        if (card == null) {
+            throw new IllegalStateException("Could not find the video card");
+        }
+        // Get video length
+        UiObject2 length = card.findObject(By.res(getPackage(), "duration"));
+        if (length == null) {
+            throw new UnknownUiException("Could not find an object of video duration");
+        }
+        String durationText = length.getText();
+        if (durationText == null || "".equals(durationText)) {
+            throw new UnknownUiException("Could not find length of the selected video");
+        }
+
+        String formatString = (durationText.split(":").length == 3) ? "HH:mm:ss" : "mm:ss";
+        SimpleDateFormat format = new SimpleDateFormat(formatString);
+        format.setTimeZone(TimeZone.getTimeZone("GMT"));
+        long durationMs;
+        try {
+            durationMs = format.parse(length.getText()).getTime();
+            Log.d(TAG, String.format("Video length is %d in milliseconds", durationMs));
+        } catch (ParseException e) {
+            throw new RuntimeException(String.format("Failed to parse video length '%s'",
+                    length.getText()));
+        }
+        return durationMs;
+    }
+
+    /**
+     * Setup expectations: YouTube is open with a focused video.
+     * @return the title text of a focused video
+     */
+    public String getFocusedVideoTitleText() {
+        UiObject2 card = getFocusedVideoCard();
+        if (card == null) {
+            throw new IllegalStateException("Could not find the video card");
+        }
+        return card.findObject(By.res(getPackage(), "title_text")).getText();
+    }
+
+    /**
+     * Setup expectations: YouTube is open with a focused video.
+     * @return the content text of a focused video
+     */
+    public String getFocusedVideoContentText() {
+        UiObject2 card = getFocusedVideoCard();
+        if (card == null) {
+            throw new IllegalStateException("Could not find the video card");
+        }
+        return card.findObject(By.res(getPackage(), "content_text")).getText();
+    }
+
+    /**
+     * Setup expectations: YouTube is open with a focused video.
+     * @param timeoutMs Timeout in milliseconds to play a video. Set to 0 if it plays until the
+     *                  end.
+     * @return true if it plays without an error during a given duration.
+     */
+    public boolean playFocusedVideo(long timeoutMs) {
+        long durationMs = getFocusedVideoDuration();
+        Log.i(TAG, String.format("Playing a video for %d (ms)", timeoutMs));
+
+        // Play the video
+        mDevice.pressDPadCenter();
+        if (!isInVideoPlayback(SHORT_SLEEP_MS)) {
+            throw new IllegalStateException("Must be in video playback");
+        }
+
+        // Wait for the given duration
+        if (timeoutMs <= 0 || timeoutMs > durationMs) {
+            timeoutMs = durationMs;
+        }
+        SystemClock.sleep(timeoutMs);
+        return true;
+    }
+
+    // TODO Move to a base or utility class that each test could access.
+    public void launchActivity() {
+        Intent intent = mInstrumentation.getContext().getPackageManager()
+                .getLaunchIntentForPackage(UI_PACKAGE);
+        Log.d(TAG, "launchActivity intent=" + intent.toString());
+        mInstrumentation.getContext().startActivity(intent);
+    }
+}
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 db3b0ac..16c6b3b 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
@@ -641,8 +641,12 @@
         if (button == null) {
             throw new IllegalStateException("Restricted Profile not found on launcher");
         }
-        mDevice.pressDPadCenter();
-        mDevice.wait(Until.gone(getWorkspaceSelector()), APP_LAUNCH_TIMEOUT);
+        mDevice.performActionAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mDevice.pressDPadCenter();
+            }
+        }, Until.newWindow(), APP_LAUNCH_TIMEOUT);
     }
 
     protected UiObject2 findSettingInRow(BySelector selector, Direction direction) {
diff --git a/tests/functional/tv/TvSysUiTests/AndroidManifest.xml b/tests/functional/tv/TvSysUiTests/AndroidManifest.xml
index 48a1cc0..be86f89 100644
--- a/tests/functional/tv/TvSysUiTests/AndroidManifest.xml
+++ b/tests/functional/tv/TvSysUiTests/AndroidManifest.xml
@@ -15,15 +15,16 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.test.functional.tv.sysui">
+    package="android.test.functional.tv.sysui"
+    android:sharedUserId="android.uid.system" >
+
+    <uses-sdk android:minSdkVersion="21"
+          android:targetSdkVersion="24"/>
 
     <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"
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
index dec64fe..e794d74 100644
--- 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
@@ -18,9 +18,19 @@
 
 import android.app.Instrumentation;
 import android.content.Context;
+import android.content.pm.UserInfo;
 import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
 import android.platform.test.helpers.CommandHelper;
 import android.platform.test.helpers.DPadHelper;
+import android.platform.test.helpers.tv.LeanbackDemoHelperImpl;
+import android.platform.test.helpers.tv.NoTouchAuthHelperImpl;
+import android.platform.test.helpers.tv.SearchHelperImpl;
+import android.platform.test.helpers.tv.SysUiPipHelperImpl;
+import android.platform.test.helpers.tv.SysUiRecentsHelperImpl;
+import android.platform.test.helpers.tv.SysUiSettingsHelperImpl;
+import android.platform.test.helpers.tv.YouTubeHelperImpl;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.launcherhelper.ILeanbackLauncherStrategy;
 import android.support.test.launcherhelper.LauncherStrategyFactory;
@@ -46,7 +56,15 @@
 
     protected CommandHelper mCmdHelper;
     protected DPadHelper mDPadHelper;
-    protected ILeanbackLauncherStrategy mLauncherStrategy;
+    protected LeanbackLauncherStrategy mLauncherStrategy;
+    protected LeanbackDemoHelperImpl mLeanbackDemoHelper;
+    protected NoTouchAuthHelperImpl mNoTouchAuthHelper;
+    protected SearchHelperImpl mSearchHelper;
+    protected SysUiPipHelperImpl mPipHelper;
+    protected SysUiRecentsHelperImpl mRecentsHelper;
+    protected SysUiSettingsHelperImpl mSettingsHelper;
+    protected YouTubeHelperImpl mYouTubeHelper;
+
 
     public SysUiTestBase() {
         initialize(InstrumentationRegistry.getInstrumentation());
@@ -71,6 +89,13 @@
         }
         mCmdHelper = new CommandHelper(getInstrumentation());
         mDPadHelper = DPadHelper.getInstance(getInstrumentation());
+        mLeanbackDemoHelper = new LeanbackDemoHelperImpl(getInstrumentation());
+        mNoTouchAuthHelper = new NoTouchAuthHelperImpl(getInstrumentation());
+        mPipHelper = new SysUiPipHelperImpl(getInstrumentation());
+        mRecentsHelper = new SysUiRecentsHelperImpl(getInstrumentation());
+        mSearchHelper = new SearchHelperImpl(getInstrumentation());
+        mSettingsHelper = new SysUiSettingsHelperImpl(getInstrumentation());
+        mYouTubeHelper = new YouTubeHelperImpl(getInstrumentation());
     }
 
     protected Instrumentation getInstrumentation() {
@@ -102,4 +127,23 @@
         }
         return defaultValue;
     }
+
+    protected static boolean isRestrictedUser(Context context) {
+        UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        UserInfo userInfo = userManager.getUserInfo(UserHandle.myUserId());
+        Log.d(TAG, "isRestrictedUser? " + (userInfo.isRestricted() ? "Y" : "N"));
+        return userInfo.isRestricted();
+    }
+
+    protected static boolean hasRestrictedUser(Context context) {
+        UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        for (UserInfo userInfo : userManager.getUsers()) {
+            if (userInfo.isRestricted()) {
+                Log.d(TAG, "hasRestrictedUser? Y");
+                return true;
+            }
+        }
+        Log.d(TAG, "hasRestrictedUser? N");
+        return false;
+    }
 }