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;
+ }
}