aboutsummaryrefslogtreecommitdiff
path: root/tests/common
diff options
context:
space:
mode:
Diffstat (limited to 'tests/common')
-rw-r--r--tests/common/Android.mk23
-rw-r--r--tests/common/AndroidManifest.xml23
-rw-r--r--tests/common/res/drawable-xhdpi/blue.pngbin0 -> 3253 bytes
-rw-r--r--tests/common/res/drawable-xhdpi/blue_small.pngbin0 -> 1158 bytes
-rw-r--r--tests/common/res/drawable-xhdpi/crash_test_android_logo.pngbin0 -> 18721 bytes
-rw-r--r--tests/common/res/drawable-xhdpi/green.pngbin0 -> 3253 bytes
-rw-r--r--tests/common/res/drawable-xhdpi/green_large.pngbin0 -> 10263 bytes
-rw-r--r--tests/common/res/drawable-xhdpi/red.pngbin0 -> 3252 bytes
-rw-r--r--tests/common/res/drawable-xhdpi/red_large.pngbin0 -> 10262 bytes
-rw-r--r--tests/common/src/com/android/tv/testing/ChannelInfo.java268
-rw-r--r--tests/common/src/com/android/tv/testing/ChannelUtils.java186
-rw-r--r--tests/common/src/com/android/tv/testing/ComparableTester.java115
-rw-r--r--tests/common/src/com/android/tv/testing/ComparatorTester.java128
-rw-r--r--tests/common/src/com/android/tv/testing/Constants.java44
-rw-r--r--tests/common/src/com/android/tv/testing/ProgramInfo.java282
-rw-r--r--tests/common/src/com/android/tv/testing/ProgramUtils.java100
-rw-r--r--tests/common/src/com/android/tv/testing/TvContentRatingConstants.java57
-rw-r--r--tests/common/src/com/android/tv/testing/Utils.java104
-rw-r--r--tests/common/src/com/android/tv/testing/testinput/ChannelState.java114
-rw-r--r--tests/common/src/com/android/tv/testing/testinput/ChannelStateData.aidl3
-rw-r--r--tests/common/src/com/android/tv/testing/testinput/ChannelStateData.java79
-rw-r--r--tests/common/src/com/android/tv/testing/testinput/ITestInputControl.aidl9
-rw-r--r--tests/common/src/com/android/tv/testing/testinput/TestInputControlConnection.java80
-rw-r--r--tests/common/src/com/android/tv/testing/testinput/TestInputControlUtils.java33
-rw-r--r--tests/common/src/com/android/tv/testing/testinput/TvTestInputConstants.java39
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java32
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/ByResource.java49
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/Constants.java35
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java69
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java43
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java190
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java63
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java146
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java70
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java60
-rw-r--r--tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java61
36 files changed, 2505 insertions, 0 deletions
diff --git a/tests/common/Android.mk b/tests/common/Android.mk
new file mode 100644
index 00000000..f8263311
--- /dev/null
+++ b/tests/common/Android.mk
@@ -0,0 +1,23 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# Include all test java files.
+LOCAL_SRC_FILES := \
+ $(call all-java-files-under, src) \
+ $(call all-Iaidl-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-annotations \
+ mockito-target \
+ tv-common \
+ ub-uiautomator
+
+LOCAL_INSTRUMENTATION_FOR := TV
+LOCAL_MODULE := tv-test-common
+LOCAL_MODULE_TAGS := optional
+LOCAL_SDK_VERSION := system_current
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_AIDL_INCLUDES += $(LOCAL_PATH)/src
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/common/AndroidManifest.xml b/tests/common/AndroidManifest.xml
new file mode 100644
index 00000000..f3ed9a9f
--- /dev/null
+++ b/tests/common/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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.
+ -->
+<!-- Stub AndroidManifest.xml to build resources -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tv.testing"
+ android:versionCode="1">
+ <uses-sdk android:targetSdkVersion="23" android:minSdkVersion="21"/>
+ <application />
+</manifest>
diff --git a/tests/common/res/drawable-xhdpi/blue.png b/tests/common/res/drawable-xhdpi/blue.png
new file mode 100644
index 00000000..dd2044ca
--- /dev/null
+++ b/tests/common/res/drawable-xhdpi/blue.png
Binary files differ
diff --git a/tests/common/res/drawable-xhdpi/blue_small.png b/tests/common/res/drawable-xhdpi/blue_small.png
new file mode 100644
index 00000000..22394ebb
--- /dev/null
+++ b/tests/common/res/drawable-xhdpi/blue_small.png
Binary files differ
diff --git a/tests/common/res/drawable-xhdpi/crash_test_android_logo.png b/tests/common/res/drawable-xhdpi/crash_test_android_logo.png
new file mode 100644
index 00000000..2442cf04
--- /dev/null
+++ b/tests/common/res/drawable-xhdpi/crash_test_android_logo.png
Binary files differ
diff --git a/tests/common/res/drawable-xhdpi/green.png b/tests/common/res/drawable-xhdpi/green.png
new file mode 100644
index 00000000..8306b9c3
--- /dev/null
+++ b/tests/common/res/drawable-xhdpi/green.png
Binary files differ
diff --git a/tests/common/res/drawable-xhdpi/green_large.png b/tests/common/res/drawable-xhdpi/green_large.png
new file mode 100644
index 00000000..77bbb231
--- /dev/null
+++ b/tests/common/res/drawable-xhdpi/green_large.png
Binary files differ
diff --git a/tests/common/res/drawable-xhdpi/red.png b/tests/common/res/drawable-xhdpi/red.png
new file mode 100644
index 00000000..89f889b9
--- /dev/null
+++ b/tests/common/res/drawable-xhdpi/red.png
Binary files differ
diff --git a/tests/common/res/drawable-xhdpi/red_large.png b/tests/common/res/drawable-xhdpi/red_large.png
new file mode 100644
index 00000000..c52a1242
--- /dev/null
+++ b/tests/common/res/drawable-xhdpi/red_large.png
Binary files differ
diff --git a/tests/common/src/com/android/tv/testing/ChannelInfo.java b/tests/common/src/com/android/tv/testing/ChannelInfo.java
new file mode 100644
index 00000000..5197630f
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/ChannelInfo.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.support.annotation.Nullable;
+import android.util.SparseArray;
+
+/**
+ * Channel Information.
+ */
+public final class ChannelInfo {
+ private static final SparseArray<String> VIDEO_HEIGHT_TO_FORMAT_MAP = new SparseArray<>();
+ static {
+ VIDEO_HEIGHT_TO_FORMAT_MAP.put(480, TvContract.Channels.VIDEO_FORMAT_480P);
+ VIDEO_HEIGHT_TO_FORMAT_MAP.put(576, TvContract.Channels.VIDEO_FORMAT_576P);
+ VIDEO_HEIGHT_TO_FORMAT_MAP.put(720, TvContract.Channels.VIDEO_FORMAT_720P);
+ VIDEO_HEIGHT_TO_FORMAT_MAP.put(1080, TvContract.Channels.VIDEO_FORMAT_1080P);
+ VIDEO_HEIGHT_TO_FORMAT_MAP.put(2160, TvContract.Channels.VIDEO_FORMAT_2160P);
+ VIDEO_HEIGHT_TO_FORMAT_MAP.put(4320, TvContract.Channels.VIDEO_FORMAT_4320P);
+ }
+
+ /**
+ * If this is specify for logo, it will be selected randomly including null.
+ */
+ public static final String GENERATE_LOGO = "GEN";
+ // If the logo is set to {@link ChannelInfo#GENERATE_LOGO}, pick one randomly from this list.
+ private static final int[] LOGOS_RES = {0, R.drawable.crash_test_android_logo};
+
+ public static final String[] PROJECTION = {
+ TvContract.Channels.COLUMN_DISPLAY_NUMBER,
+ TvContract.Channels.COLUMN_DISPLAY_NAME,
+ TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID,
+ };
+
+ public final String number;
+ public final String name;
+ public final String logoUrl;
+ public final int originalNetworkId;
+ public final int videoWidth;
+ public final int videoHeight;
+ public final int audioChannel;
+ public final int audioLanguageCount;
+ public final boolean hasClosedCaption;
+ public final ProgramInfo program;
+ public final String appLinkText;
+ public final int appLinkColor;
+ public final String appLinkIconUri;
+ public final String appLinkPosterArtUri;
+ public final String appLinkIntentUri;
+
+ /**
+ * Create a channel info for TVTestInput.
+ *
+ * @param context a context to insert logo. It can be null if logo isn't needed.
+ * @param channelNumber a channel number to be use as an identifier.
+ * {@link #originalNetworkId} will be assigned the same value, too.
+ */
+ public static ChannelInfo create(@Nullable Context context, int channelNumber) {
+ Builder builder = new Builder()
+ .setNumber(String.valueOf(channelNumber))
+ .setName("Channel " + channelNumber)
+ .setOriginalNetworkId(channelNumber);
+ if (context != null) {
+ builder.setLogoUrl(Utils.getUriStringForResource(
+ context, LOGOS_RES[channelNumber % LOGOS_RES.length]));
+ }
+ return builder.build();
+ }
+
+ public static ChannelInfo fromCursor(Cursor c) {
+ // TODO: Fill other fields.
+ Builder builder = new Builder();
+ int index = c.getColumnIndex(TvContract.Channels.COLUMN_DISPLAY_NUMBER);
+ if (index >= 0) {
+ builder.setNumber(c.getString(index));
+ }
+ index = c.getColumnIndex(TvContract.Channels.COLUMN_DISPLAY_NAME);
+ if (index >= 0) {
+ builder.setName(c.getString(index));
+ }
+ index = c.getColumnIndex(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID);
+ if (index >= 0) {
+ builder.setOriginalNetworkId(c.getInt(index));
+ }
+ return builder.build();
+ }
+
+ private ChannelInfo(String number, String name, String logoUrl, int originalNetworkId,
+ int videoWidth, int videoHeight, int audioChannel, int audioLanguageCount,
+ boolean hasClosedCaption, ProgramInfo program, String appLinkText, int appLinkColor,
+ String appLinkIconUri, String appLinkPosterArtUri, String appLinkIntentUri) {
+ this.number = number;
+ this.name = name;
+ this.logoUrl = logoUrl;
+ this.originalNetworkId = originalNetworkId;
+ this.videoWidth = videoWidth;
+ this.videoHeight = videoHeight;
+ this.audioChannel = audioChannel;
+ this.audioLanguageCount = audioLanguageCount;
+ this.hasClosedCaption = hasClosedCaption;
+ this.program = program;
+ this.appLinkText = appLinkText;
+ this.appLinkColor = appLinkColor;
+ this.appLinkIconUri = appLinkIconUri;
+ this.appLinkPosterArtUri = appLinkPosterArtUri;
+ this.appLinkIntentUri = appLinkIntentUri;
+ }
+
+ public String getVideoFormat() {
+ return VIDEO_HEIGHT_TO_FORMAT_MAP.get(videoHeight);
+ }
+
+ @Override
+ public String toString() {
+ return "Channel{"
+ + "number=" + number
+ + ", name=" + name
+ + ", logoUri=" + logoUrl
+ + ", originalNetworkId=" + originalNetworkId
+ + ", videoWidth=" + videoWidth
+ + ", videoHeight=" + videoHeight
+ + ", audioChannel=" + audioChannel
+ + ", audioLanguageCount=" + audioLanguageCount
+ + ", hasClosedCaption=" + hasClosedCaption
+ + ", appLinkText=" + appLinkText
+ + ", appLinkColor=" + appLinkColor
+ + ", appLinkIconUri=" + appLinkIconUri
+ + ", appLinkPosterArtUri=" + appLinkPosterArtUri
+ + ", appLinkIntentUri=" + appLinkIntentUri + "}";
+ }
+
+ /**
+ * Builder class for {@code ChannelInfo}.
+ */
+ public static class Builder {
+ private String mNumber;
+ private String mName;
+ private String mLogoUrl = null;
+ private int mOriginalNetworkId;
+ private int mVideoWidth = 1920; // Width for HD video.
+ private int mVideoHeight = 1080; // Height for HD video.
+ private int mAudioChannel;
+ private int mAudioLanguageCount;
+ private boolean mHasClosedCaption;
+ private ProgramInfo mProgram;
+ private String mAppLinkText;
+ private int mAppLinkColor;
+ private String mAppLinkIconUri;
+ private String mAppLinkPosterArtUri;
+ private String mAppLinkIntentUri;
+
+ public Builder() {
+ }
+
+ public Builder(ChannelInfo other) {
+ mNumber = other.number;
+ mName = other.name;
+ mLogoUrl = other.name;
+ mOriginalNetworkId = other.originalNetworkId;
+ mVideoWidth = other.videoWidth;
+ mVideoHeight = other.videoHeight;
+ mAudioChannel = other.audioChannel;
+ mAudioLanguageCount = other.audioLanguageCount;
+ mHasClosedCaption = other.hasClosedCaption;
+ mProgram = other.program;
+ }
+
+ public Builder setName(String name) {
+ mName = name;
+ return this;
+ }
+
+ public Builder setNumber(String number) {
+ mNumber = number;
+ return this;
+ }
+
+ public Builder setLogoUrl(String logoUrl) {
+ mLogoUrl = logoUrl;
+ return this;
+ }
+
+ public Builder setOriginalNetworkId(int originalNetworkId) {
+ mOriginalNetworkId = originalNetworkId;
+ return this;
+ }
+
+ public Builder setVideoWidth(int videoWidth) {
+ mVideoWidth = videoWidth;
+ return this;
+ }
+
+ public Builder setVideoHeight(int videoHeight) {
+ mVideoHeight = videoHeight;
+ return this;
+ }
+
+ public Builder setAudioChannel(int audioChannel) {
+ mAudioChannel = audioChannel;
+ return this;
+ }
+
+ public Builder setAudioLanguageCount(int audioLanguageCount) {
+ mAudioLanguageCount = audioLanguageCount;
+ return this;
+ }
+
+ public Builder setHasClosedCaption(boolean hasClosedCaption) {
+ mHasClosedCaption = hasClosedCaption;
+ return this;
+ }
+
+ public Builder setProgram(ProgramInfo program) {
+ mProgram = program;
+ return this;
+ }
+
+ public Builder setAppLinkText(String appLinkText) {
+ mAppLinkText = appLinkText;
+ return this;
+ }
+
+ public Builder setAppLinkColor(int appLinkColor) {
+ mAppLinkColor = appLinkColor;
+ return this;
+ }
+
+ public Builder setAppLinkIconUri(String appLinkIconUri) {
+ mAppLinkIconUri = appLinkIconUri;
+ return this;
+ }
+
+ public Builder setAppLinkPosterArtUri(String appLinkPosterArtUri) {
+ mAppLinkPosterArtUri = appLinkPosterArtUri;
+ return this;
+ }
+
+ public Builder setAppLinkIntentUri(String appLinkIntentUri) {
+ mAppLinkIntentUri = appLinkIntentUri;
+ return this;
+ }
+
+ public ChannelInfo build() {
+ return new ChannelInfo(mNumber, mName, mLogoUrl, mOriginalNetworkId,
+ mVideoWidth, mVideoHeight, mAudioChannel, mAudioLanguageCount,
+ mHasClosedCaption, mProgram, mAppLinkText, mAppLinkColor,
+ mAppLinkIconUri, mAppLinkPosterArtUri, mAppLinkIntentUri);
+
+ }
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/ChannelUtils.java b/tests/common/src/com/android/tv/testing/ChannelUtils.java
new file mode 100644
index 00000000..aadbf727
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/ChannelUtils.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.media.tv.TvContract.Channels;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.tv.common.TvCommonConstants;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Static helper methods for working with {@link android.media.tv.TvContract}.
+ */
+public class ChannelUtils {
+ private static final String TAG = "ChannelUtils";
+
+ /**
+ * Query and return the map of (channel_id, ChannelInfo).
+ * See: {@link ChannelInfo#fromCursor(Cursor)}.
+ */
+ @WorkerThread
+ public static Map<Long, ChannelInfo> queryChannelInfoMapForTvInput(
+ Context context, String inputId) {
+ Uri uri = TvContract.buildChannelsUriForInput(inputId);
+ Map<Long, ChannelInfo> map = new HashMap<>();
+
+ String[] projections = new String[ChannelInfo.PROJECTION.length + 1];
+ projections[0] = Channels._ID;
+ for (int i = 0; i < ChannelInfo.PROJECTION.length; i++) {
+ projections[i + 1] = ChannelInfo.PROJECTION[i];
+ }
+ try (Cursor cursor = context.getContentResolver()
+ .query(uri, projections, null, null, null)) {
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ map.put(cursor.getLong(0), ChannelInfo.fromCursor(cursor));
+ }
+ }
+ return map;
+ }
+ }
+
+ @WorkerThread
+ public static void updateChannels(Context context, String inputId, List<ChannelInfo> channels) {
+ // Create a map from original network ID to channel row ID for existing channels.
+ SparseArray<Long> existingChannelsMap = new SparseArray<>();
+ Uri channelsUri = TvContract.buildChannelsUriForInput(inputId);
+ String[] projection = {Channels._ID, Channels.COLUMN_ORIGINAL_NETWORK_ID};
+ ContentResolver resolver = context.getContentResolver();
+ try (Cursor cursor = resolver.query(channelsUri, projection, null, null, null)) {
+ while (cursor != null && cursor.moveToNext()) {
+ long rowId = cursor.getLong(0);
+ int originalNetworkId = cursor.getInt(1);
+ existingChannelsMap.put(originalNetworkId, rowId);
+ }
+ }
+
+ Map<Uri, String> logos = new HashMap<>();
+ for (ChannelInfo channel : channels) {
+ // If a channel exists, update it. If not, insert a new one.
+ ContentValues values = new ContentValues();
+ values.put(Channels.COLUMN_INPUT_ID, inputId);
+ values.put(Channels.COLUMN_DISPLAY_NUMBER, channel.number);
+ values.put(Channels.COLUMN_DISPLAY_NAME, channel.name);
+ values.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.originalNetworkId);
+ String videoFormat = channel.getVideoFormat();
+ if (videoFormat != null) {
+ values.put(Channels.COLUMN_VIDEO_FORMAT, videoFormat);
+ } else {
+ values.putNull(Channels.COLUMN_VIDEO_FORMAT);
+ }
+ if (TvCommonConstants.IS_MNC_OR_HIGHER) {
+ if (!TextUtils.isEmpty(channel.appLinkText)) {
+ values.put(Channels.COLUMN_APP_LINK_TEXT, channel.appLinkText);
+ }
+ if (channel.appLinkColor != 0) {
+ values.put(Channels.COLUMN_APP_LINK_COLOR, channel.appLinkColor);
+ }
+ if (!TextUtils.isEmpty(channel.appLinkPosterArtUri)) {
+ values.put(Channels.COLUMN_APP_LINK_POSTER_ART_URI, channel.appLinkPosterArtUri);
+ }
+ if (!TextUtils.isEmpty(channel.appLinkIconUri)) {
+ values.put(Channels.COLUMN_APP_LINK_ICON_URI, channel.appLinkIconUri);
+ }
+ if (!TextUtils.isEmpty(channel.appLinkIntentUri)) {
+ values.put(Channels.COLUMN_APP_LINK_INTENT_URI, channel.appLinkIntentUri);
+ }
+ }
+ Long rowId = existingChannelsMap.get(channel.originalNetworkId);
+ Uri uri;
+ if (rowId == null) {
+ Log.v(TAG, "Inserting "+ channel);
+ uri = resolver.insert(TvContract.Channels.CONTENT_URI, values);
+ } else {
+ Log.v(TAG, "Updating "+ channel);
+ uri = TvContract.buildChannelUri(rowId);
+ resolver.update(uri, values, null, null);
+ existingChannelsMap.remove(channel.originalNetworkId);
+ }
+ if (!TextUtils.isEmpty(channel.logoUrl)) {
+ logos.put(TvContract.buildChannelLogoUri(uri), channel.logoUrl);
+ }
+ }
+ if (!logos.isEmpty()) {
+ new InsertLogosTask(context).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, logos);
+ }
+
+ // Deletes channels which don't exist in the new feed.
+ int size = existingChannelsMap.size();
+ for (int i = 0; i < size; ++i) {
+ Long rowId = existingChannelsMap.valueAt(i);
+ resolver.delete(TvContract.buildChannelUri(rowId), null, null);
+ }
+ }
+
+ public static void copy(InputStream is, OutputStream os) throws IOException {
+ byte[] buffer = new byte[1024];
+ int len;
+ while ((len = is.read(buffer)) != -1) {
+ os.write(buffer, 0, len);
+ }
+ }
+
+ private ChannelUtils() {
+ // Prevent instantiation.
+ }
+
+ public static class InsertLogosTask extends AsyncTask<Map<Uri, String>, Void, Void> {
+ private final Context mContext;
+
+ InsertLogosTask(Context context) {
+ mContext = context;
+ }
+
+ @SafeVarargs
+ @Override
+ public final Void doInBackground(Map<Uri, String>... logosList) {
+ for (Map<Uri, String> logos : logosList) {
+ for (Uri uri : logos.keySet()) {
+ if (uri == null) {
+ continue;
+ }
+ Uri logoUri = Uri.parse(logos.get(uri));
+ try (InputStream is = mContext.getContentResolver().openInputStream(logoUri);
+ OutputStream os = mContext.getContentResolver().openOutputStream(uri)) {
+ copy(is, os);
+ } catch (IOException ioe) {
+ Log.e(TAG, "Failed to write " + logoUri + " to " + uri, ioe);
+ }
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/ComparableTester.java b/tests/common/src/com/android/tv/testing/ComparableTester.java
new file mode 100644
index 00000000..d82e25c8
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/ComparableTester.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Tester for {@link java.lang.Comparable}s.
+ *
+ * <p>
+ * To use, create a new {@link ComparableTester} and add comparable groups
+ * where each group contains objects that are
+ * {@link java.util.Comparator#compare(Object, Object)} == 0 to each other.
+ * Groups are added in order asserting that all earlier groups have compare < 0
+ * for all later groups.
+ *
+ * <pre>{@code
+ * new ComparableTester<String>()
+ * .addEquivelentGroup("Hello", "HELLO")
+ * .addEquivelentGroup("World", "wORLD")
+ * .addEquivelentGroup("ZEBRA")
+ * .test();
+ * }
+ * </pre>
+ *
+ * @param <T> the type of objects to compare.
+ */
+public class ComparableTester<T extends Comparable<T>> {
+
+ private final List<List<T>> listOfGroups = new ArrayList<>();
+
+ @SafeVarargs
+ public final ComparableTester<T> addEquivelentGroup(T... items) {
+ listOfGroups.add(Arrays.asList(items));
+ return this;
+ }
+
+ public void test() {
+ for (int i = 0; i < listOfGroups.size(); i++) {
+ List<T> currentGroup = listOfGroups.get(i);
+ for (int j = 0; j < i; j++) {
+ List<T> lhs = listOfGroups.get(j);
+ assertOrder(i, j, lhs, currentGroup);
+ }
+ assertZero(currentGroup);
+ for (int j = i + 1; j < listOfGroups.size(); j++) {
+ List<T> rhs = listOfGroups.get(j);
+ assertOrder(i, j, currentGroup, rhs);
+ }
+ }
+ }
+
+ private void assertOrder(int less, int more, List<T> lessGroup, List<T> moreGroup) {
+ assertLess(less, more, lessGroup, moreGroup);
+ assertMore(more, less, moreGroup, lessGroup);
+ }
+
+ private void assertLess(int left, int right, Collection<T> leftGroup,
+ Collection<T> rightGroup) {
+ int leftSub = 0;
+ for (T leftItem : leftGroup) {
+ int rightSub = 0;
+ String leftName = "Item[" + left + "," + (leftSub++) + "]";
+ for (T rightItem : rightGroup) {
+ String rightName = "Item[" + right + "," + (rightSub++) + "]";
+ Assert.assertEquals(
+ leftName + " " + leftItem + " compareTo " + rightName + " " + rightItem
+ + " is <0", true, leftItem.compareTo(rightItem) < 0);
+ }
+ }
+ }
+
+ private void assertMore(int left, int right, Collection<T> leftGroup,
+ Collection<T> rightGroup) {
+ int leftSub = 0;
+ for (T leftItem : leftGroup) {
+ int rightSub = 0;
+ String leftName = "Item[" + left + "," + (leftSub++) + "]";
+ for (T rightItem : rightGroup) {
+ String rightName = "Item[" + right + "," + (rightSub++) + "]";
+ Assert.assertEquals(
+ leftName + " " + leftItem + " compareTo " + rightName + " " + rightItem
+ + " is >0", true, leftItem.compareTo(rightItem) > 0);
+ }
+ }
+ }
+
+ private void assertZero(Collection<T> group) {
+ // Test everything against everything in both directions, including against itself.
+ for (T lhs : group) {
+ for (T rhs : group) {
+ Assert.assertEquals(lhs + " compareTo " + rhs, 0, lhs.compareTo(rhs));
+ }
+ }
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/ComparatorTester.java b/tests/common/src/com/android/tv/testing/ComparatorTester.java
new file mode 100644
index 00000000..dc5cf00f
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/ComparatorTester.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing;
+
+import static junit.framework.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Tester for {@link Comparator} relationships between groups of T.
+ *
+ * <p>
+ * To use, create a new {@link ComparatorTester} and add comparable groups
+ * where each group contains objects that are
+ * {@link Comparator#compare(Object, Object)} == 0 to each other.
+ * Groups are added in order asserting that all earlier groups have compare < 0
+ * for all later groups.
+ *
+ * <pre>{@code
+ * ComparatorTester
+ * .withoutEqualsTest(String.CASE_INSENSITIVE_ORDER)
+ * .addComparableGroup("Hello", "HELLO")
+ * .addComparableGroup("World", "wORLD")
+ * .addComparableGroup("ZEBRA")
+ * .test();
+ * }
+ * </pre>
+ *
+ * @param <T> the type of objects to compare.
+ */
+public class ComparatorTester<T> {
+
+ private final List<List<T>> listOfGroups = new ArrayList<>();
+
+ private final Comparator<T> comparator;
+
+
+ public static <T> ComparatorTester<T> withoutEqualsTest(Comparator<T> comparator) {
+ return new ComparatorTester<>(comparator);
+ }
+
+ private ComparatorTester(Comparator<T> comparator) {
+ this.comparator = comparator;
+ }
+
+ @SafeVarargs
+ public final ComparatorTester<T> addComparableGroup(T... items) {
+ listOfGroups.add(Arrays.asList(items));
+ return this;
+ }
+
+ public void test() {
+ for (int i = 0; i < listOfGroups.size(); i++) {
+ List<T> currentGroup = listOfGroups.get(i);
+ for (int j = 0; j < i; j++) {
+ List<T> lhs = listOfGroups.get(j);
+ assertOrder(i, j, lhs, currentGroup);
+ }
+ assertZero(currentGroup);
+ for (int j = i + 1; j < listOfGroups.size(); j++) {
+ List<T> rhs = listOfGroups.get(j);
+ assertOrder(i, j, currentGroup, rhs);
+ }
+ }
+ //TODO: also test equals
+ }
+
+ private void assertOrder(int less, int more, List<T> lessGroup, List<T> moreGroup) {
+ assertLess(less, more, lessGroup, moreGroup);
+ assertMore(more, less, moreGroup, lessGroup);
+ }
+
+ private void assertLess(int left, int right, Collection<T> leftGroup,
+ Collection<T> rightGroup) {
+ int leftSub = 0;
+ for (T leftItem : leftGroup) {
+ int rightSub = 0;
+ for (T rightItem : rightGroup) {
+ String leftName = "Item[" + left + "," + (leftSub++) + "]";
+ String rName = "Item[" + right + "," + (rightSub++) + "]";
+ assertEquals(leftName + " " + leftItem + " compareTo " + rName + " " + rightItem
+ + " is <0", true, comparator.compare(leftItem, rightItem) < 0);
+ }
+ }
+ }
+
+ private void assertMore(int left, int right, Collection<T> leftGroup,
+ Collection<T> rightGroup) {
+ int leftSub = 0;
+ for (T leftItem : leftGroup) {
+ int rightSub = 0;
+ for (T rightItem : rightGroup) {
+ String leftName = "Item[" + left + "," + (leftSub++) + "]";
+ String rName = "Item[" + right + "," + (rightSub++) + "]";
+ assertEquals(leftName + " " + leftItem + " compareTo " + rName + " " + rightItem
+ + " is >0", true, comparator.compare(leftItem, rightItem) > 0);
+ }
+ }
+ }
+
+ private void assertZero(Collection<T> group) {
+ // Test everything against everything in both directions, including against itself.
+ for (T leftItem : group) {
+ for (T rightItem : group) {
+ assertEquals(leftItem + " compareTo " + rightItem, 0,
+ comparator.compare(leftItem, rightItem));
+ }
+ }
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/Constants.java b/tests/common/src/com/android/tv/testing/Constants.java
new file mode 100644
index 00000000..6575fa9e
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/Constants.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing;
+
+import android.media.tv.TvTrackInfo;
+
+/**
+ * Constants for testing.
+ */
+public final class Constants {
+ public static final int FUNC_TEST_CHANNEL_COUNT = 100;
+ public static final int UNIT_TEST_CHANNEL_COUNT = 4;
+ public static final int JANK_TEST_CHANNEL_COUNT = 1000;
+
+ public static final TvTrackInfo EN_STEREO_AUDIO_TRACK = new TvTrackInfo.Builder(
+ TvTrackInfo.TYPE_AUDIO, "English Stereo Audio").setLanguage("en")
+ .setAudioChannelCount(2).build();
+ public static final TvTrackInfo GENERIC_AUDIO_TRACK = new TvTrackInfo.Builder(
+ TvTrackInfo.TYPE_AUDIO, "Generic Audio").build();
+
+ public static final TvTrackInfo FHD1080P50_VIDEO_TRACK = new TvTrackInfo.Builder(
+ TvTrackInfo.TYPE_VIDEO, "FHD Video").setVideoHeight(1080).setVideoWidth(1920)
+ .setVideoFrameRate(50).build();
+ public static final TvTrackInfo SVGA_VIDEO_TRACK = new TvTrackInfo.Builder(
+ TvTrackInfo.TYPE_VIDEO, "SVGA Video").setVideoHeight(600).setVideoWidth(800).build();
+ public static final TvTrackInfo GENERIC_VIDEO_TRACK = new TvTrackInfo.Builder(
+ TvTrackInfo.TYPE_VIDEO, "Generic Video").build();
+
+ private Constants() {
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/ProgramInfo.java b/tests/common/src/com/android/tv/testing/ProgramInfo.java
new file mode 100644
index 00000000..5b47d104
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/ProgramInfo.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvContract;
+
+import java.util.concurrent.TimeUnit;
+
+public final class ProgramInfo {
+ /**
+ * If this is specify for title, it will be generated by adding index.
+ */
+ public static final String GEN_TITLE = "";
+
+ /**
+ * If this is specify for episode title, it will be generated by adding index.
+ * Also, season and episode numbers would be generated, too.
+ * see: {@link #build} for detail.
+ */
+ public static final String GEN_EPISODE = "";
+ private static final int SEASON_MAX = 10;
+ private static final int EPISODE_MAX = 12;
+
+ /**
+ * If this is specify for poster art,
+ * it will be selected one of {@link #POSTER_ARTS_RES} in order.
+ */
+ public static final String GEN_POSTER = "GEN";
+ private static final int[] POSTER_ARTS_RES = {
+ 0,
+ R.drawable.blue,
+ R.drawable.red_large,
+ R.drawable.green,
+ R.drawable.red,
+ R.drawable.green_large,
+ R.drawable.blue_small};
+
+ /**
+ * If this is specified for duration,
+ * it will be selected one of {@link #DURATIONS_MS} in order.
+ */
+ public static final int GEN_DURATION = -1;
+ private static final long[] DURATIONS_MS = {
+ TimeUnit.MINUTES.toMillis(15),
+ TimeUnit.MINUTES.toMillis(45),
+ TimeUnit.MINUTES.toMillis(90),
+ TimeUnit.MINUTES.toMillis(60),
+ TimeUnit.MINUTES.toMillis(30),
+ TimeUnit.MINUTES.toMillis(45),
+ TimeUnit.MINUTES.toMillis(60),
+ TimeUnit.MINUTES.toMillis(90),
+ TimeUnit.HOURS.toMillis(5)};
+ private static long DURATIONS_SUM_MS;
+ static {
+ DURATIONS_SUM_MS = 0;
+ for (int i = 0; i < DURATIONS_MS.length; i++) {
+ DURATIONS_SUM_MS += DURATIONS_MS[i];
+ }
+ }
+
+ /**
+ * If this is specified for genre,
+ * it will be selected one of {@link #GENRES} in order.
+ */
+ public static final String GEN_GENRE = "GEN";
+ private static final String[] GENRES = {
+ "",
+ TvContract.Programs.Genres.SPORTS,
+ TvContract.Programs.Genres.NEWS,
+ TvContract.Programs.Genres.SHOPPING,
+ TvContract.Programs.Genres.DRAMA,
+ TvContract.Programs.Genres.ENTERTAINMENT};
+
+ public final String title;
+ public final String episode;
+ public final int seasonNumber;
+ public final int episodeNumber;
+ public final String posterArtUri;
+ public final String description;
+ public final long durationMs;
+ public final String genre;
+ public final TvContentRating[] contentRatings;
+ public final String resourceUri;
+
+ public static ProgramInfo fromCursor(Cursor c) {
+ // TODO: Fill other fields.
+ Builder builder = new Builder();
+ int index = c.getColumnIndex(TvContract.Programs.COLUMN_TITLE);
+ if (index >= 0) {
+ builder.setTitle(c.getString(index));
+ }
+ index = c.getColumnIndex(TvContract.Programs.COLUMN_SHORT_DESCRIPTION);
+ if (index >= 0) {
+ builder.setDescription(c.getString(index));
+ }
+ index = c.getColumnIndex(TvContract.Programs.COLUMN_EPISODE_TITLE);
+ if (index >= 0) {
+ builder.setEpisode(c.getString(index));
+ }
+ return builder.build();
+ }
+
+ public ProgramInfo(String title, String episode, int seasonNumber, int episodeNumber,
+ String posterArtUri, String description, long durationMs,
+ TvContentRating[] contentRatings, String genre, String resourceUri) {
+ this.title = title;
+ this.episode = episode;
+ this.seasonNumber = seasonNumber;
+ this.episodeNumber = episodeNumber;
+ this.posterArtUri = posterArtUri;
+ this.description = description;
+ this.durationMs = durationMs;
+ this.contentRatings = contentRatings;
+ this.genre = genre;
+ this.resourceUri = resourceUri;
+ }
+
+ /**
+ * Create a instance of {@link ProgramInfo} whose content will be generated as much as possible.
+ */
+ public static ProgramInfo create() {
+ return new Builder().build();
+ }
+
+ /**
+ * Get index of the program whose start time equals or less than {@code timeMs} and
+ * end time more than {@code timeMs}.
+ * @param timeMs target time in millis to find a program.
+ * @param channelId used to add complexity to the index between two consequence channels.
+ */
+ public int getIndex(long timeMs, long channelId) {
+ if (durationMs != GEN_DURATION) {
+ return Math.max((int) (timeMs / durationMs), 0);
+ }
+ long startTimeMs = channelId * DURATIONS_MS[((int) (channelId % DURATIONS_MS.length))];
+ int index = (int) ((timeMs - startTimeMs) / DURATIONS_SUM_MS) * DURATIONS_MS.length;
+ startTimeMs += (index / DURATIONS_MS.length) * DURATIONS_SUM_MS;
+ while (startTimeMs + DURATIONS_MS[index % DURATIONS_MS.length] < timeMs) {
+ startTimeMs += DURATIONS_MS[index % DURATIONS_MS.length];
+ index++;
+ }
+ return index;
+ }
+
+ /**
+ * Returns the start time for the program with the position.
+ * @param index index returned by {@link #getIndex}
+ */
+ public long getStartTimeMs(int index, long channelId) {
+ if (durationMs != GEN_DURATION) {
+ return index * durationMs;
+ }
+ long startTimeMs = channelId * DURATIONS_MS[((int) (channelId % DURATIONS_MS.length))]
+ + (index / DURATIONS_MS.length) * DURATIONS_SUM_MS;
+ for (int i = 0; i < index % DURATIONS_MS.length; i++) {
+ startTimeMs += DURATIONS_MS[i];
+ }
+ return startTimeMs;
+ }
+
+ /**
+ * Return complete {@link ProgramInfo} with the generated value.
+ * See: {@link #GEN_TITLE}, {@link #GEN_EPISODE}, {@link #GEN_POSTER}, {@link #GEN_DURATION},
+ * {@link #GEN_GENRE}.
+ * @param index index returned by {@link #getIndex}
+ */
+ public ProgramInfo build(Context context, int index) {
+ if (!GEN_TITLE.equals(title)
+ && !GEN_EPISODE.equals(episode)
+ && !GEN_POSTER.equals(posterArtUri)
+ && durationMs != GEN_DURATION
+ && !GEN_GENRE.equals(genre)) {
+ return this;
+ }
+ return new ProgramInfo(
+ GEN_TITLE.equals(title) ? "Title(" + index + ")" : title,
+ GEN_EPISODE.equals(episode) ? "Episode(" + index + ")" : episode,
+ GEN_EPISODE.equals(episode) ? (index % SEASON_MAX + 1) : seasonNumber,
+ GEN_EPISODE.equals(episode) ? (index % EPISODE_MAX + 1) : episodeNumber,
+ GEN_POSTER.equals(posterArtUri)
+ ? Utils.getUriStringForResource(context,
+ POSTER_ARTS_RES[index % POSTER_ARTS_RES.length])
+ : posterArtUri,
+ description,
+ durationMs == GEN_DURATION ? DURATIONS_MS[index % DURATIONS_MS.length] : durationMs,
+ contentRatings,
+ GEN_GENRE.equals(genre) ? GENRES[index % GENRES.length] : genre,
+ resourceUri);
+ }
+
+ @Override
+ public String toString() {
+ return "ProgramInfo{title=" + title
+ + ", episode=" + episode
+ + ", durationMs=" + durationMs + "}";
+ }
+
+ public static class Builder {
+ private String mTitle = GEN_TITLE;
+ private String mEpisode = GEN_EPISODE;
+ private int mSeasonNumber;
+ private int mEpisodeNumber;
+ private String mPosterArtUri = GEN_POSTER;
+ private String mDescription;
+ private long mDurationMs = GEN_DURATION;
+ private TvContentRating[] mContentRatings;
+ private String mGenre = GEN_GENRE;
+ private String mResourceUri;
+
+ public Builder setTitle(String title) {
+ mTitle = title;
+ return this;
+ }
+
+ public Builder setEpisode(String episode) {
+ mEpisode = episode;
+ return this;
+ }
+
+ public Builder setSeasonNumber(int seasonNumber) {
+ mSeasonNumber = seasonNumber;
+ return this;
+ }
+
+ public Builder setEpisodeNumber(int episodeNumber) {
+ mEpisodeNumber = episodeNumber;
+ return this;
+ }
+
+ public Builder setPosterArtUri(String posterArtUri) {
+ mPosterArtUri = posterArtUri;
+ return this;
+ }
+
+ public Builder setDescription(String description) {
+ mDescription = description;
+ return this;
+ }
+
+ public Builder setDurationMs(long durationMs) {
+ mDurationMs = durationMs;
+ return this;
+ }
+
+ public Builder setContentRatings(TvContentRating[] contentRatings) {
+ mContentRatings = contentRatings;
+ return this;
+ }
+
+ public Builder setGenre(String genre) {
+ mGenre = genre;
+ return this;
+ }
+
+ public Builder setResourceUri(String resourceUri) {
+ mResourceUri = resourceUri;
+ return this;
+ }
+
+ public ProgramInfo build() {
+ return new ProgramInfo(mTitle, mEpisode, mSeasonNumber, mEpisodeNumber, mPosterArtUri,
+ mDescription, mDurationMs, mContentRatings, mGenre, mResourceUri);
+ }
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/ProgramUtils.java b/tests/common/src/com/android/tv/testing/ProgramUtils.java
new file mode 100644
index 00000000..92fe9c82
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/ProgramUtils.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs;
+import android.net.Uri;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+public class ProgramUtils {
+ private static final String TAG = "ProgramUtils";
+
+ // Populate program data for a week.
+ private static final long PROGRAM_INSERT_DURATION_MS = TimeUnit.DAYS.toMillis(7);
+ private static final int MAX_DB_INSERT_COUNT_AT_ONCE = 500;
+
+ /**
+ * Populate programs by repeating given program information.
+ * This method will populate programs without any gap nor overlapping
+ * starting from the current time.
+ */
+ public static void populatePrograms(Context context, Uri channelUri, ProgramInfo program) {
+ ContentValues values = new ContentValues();
+ long channelId = ContentUris.parseId(channelUri);
+
+ values.put(Programs.COLUMN_CHANNEL_ID, channelId);
+ values.put(Programs.COLUMN_SHORT_DESCRIPTION, program.description);
+ values.put(Programs.COLUMN_CONTENT_RATING,
+ Utils.contentRatingsToString(program.contentRatings));
+
+ long currentTimeMs = System.currentTimeMillis();
+ long targetEndTimeMs = currentTimeMs + PROGRAM_INSERT_DURATION_MS;
+ long timeMs = getLastProgramEndTimeMs(context, channelUri, currentTimeMs, targetEndTimeMs);
+ if (timeMs <= 0) {
+ timeMs = currentTimeMs;
+ }
+ int index = program.getIndex(timeMs, channelId);
+ timeMs = program.getStartTimeMs(index, channelId);
+
+ ArrayList<ContentValues> list = new ArrayList<>();
+ while (timeMs < targetEndTimeMs) {
+ ProgramInfo programAt = program.build(context, index++);
+ values.put(Programs.COLUMN_TITLE, programAt.title);
+ values.put(Programs.COLUMN_EPISODE_TITLE, programAt.episode);
+ values.put(Programs.COLUMN_SEASON_NUMBER, programAt.seasonNumber);
+ values.put(Programs.COLUMN_EPISODE_NUMBER, programAt.episodeNumber);
+ values.put(Programs.COLUMN_POSTER_ART_URI, programAt.posterArtUri);
+ values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, timeMs);
+ values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, timeMs + programAt.durationMs);
+ values.put(Programs.COLUMN_CANONICAL_GENRE, programAt.genre);
+ values.put(Programs.COLUMN_POSTER_ART_URI, programAt.posterArtUri);
+ list.add(new ContentValues(values));
+ timeMs += programAt.durationMs;
+
+ if (list.size() >= MAX_DB_INSERT_COUNT_AT_ONCE
+ || timeMs >= targetEndTimeMs) {
+ context.getContentResolver().bulkInsert(Programs.CONTENT_URI,
+ list.toArray(new ContentValues[list.size()]));
+ Log.v(TAG, "Inserted " + list.size() + " programs for " + channelUri);
+ list.clear();
+ }
+ }
+ }
+
+ private static long getLastProgramEndTimeMs(
+ Context context, Uri channelUri, long startTimeMs, long endTimeMs) {
+ Uri uri = TvContract.buildProgramsUriForChannel(channelUri, startTimeMs, endTimeMs);
+ String[] projection = {Programs.COLUMN_END_TIME_UTC_MILLIS};
+ try (Cursor cursor =
+ context.getContentResolver().query(uri, projection, null, null, null)) {
+ if (cursor != null && cursor.moveToLast()) {
+ return cursor.getLong(0);
+ }
+ }
+ return 0;
+ }
+
+ private ProgramUtils() {}
+}
diff --git a/tests/common/src/com/android/tv/testing/TvContentRatingConstants.java b/tests/common/src/com/android/tv/testing/TvContentRatingConstants.java
new file mode 100644
index 00000000..0795a89c
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/TvContentRatingConstants.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing;
+
+import android.media.tv.TvContentRating;
+
+/**
+ * Constants for the content rating strings.
+ */
+public final class TvContentRatingConstants {
+ /**
+ * A content rating object.
+ *
+ * <p>Domain: com.android.tv
+ * <p>Rating system: US_TV
+ * <p>Rating: US_TV_Y7
+ * <p>Sub ratings: US_TV_FV
+ */
+ public static final TvContentRating CONTENT_RATING_US_TV_Y7_US_TV_FV =
+ TvContentRating.createRating("com.android.tv", "US_TV", "US_TV_Y7", "US_TV_FV");
+
+ /**
+ * A content rating object.
+ *
+ * <p>Domain: com.android.tv
+ * <p>Rating system: US_TV
+ * <p>Rating: US_TV_MA
+ */
+ public static final TvContentRating CONTENT_RATING_US_TV_MA =
+ TvContentRating.createRating("com.android.tv", "US_TV", "US_TV_MA");
+
+ /**
+ * A content rating object.
+ *
+ * <p>Domain: com.android.tv
+ * <p>Rating system: US_TV
+ * <p>Rating: US_TV_PG
+ * <p>Sub ratings: US_TV_L, US_TV_S
+ */
+ public static final TvContentRating CONTENT_RATING_US_TV_PG_US_TV_L_US_TV_S =
+ TvContentRating.createRating("com.android.tv", "US_TV", "US_TV_PG", "US_TV_L",
+ "US_TV_S");
+}
diff --git a/tests/common/src/com/android/tv/testing/Utils.java b/tests/common/src/com/android/tv/testing/Utils.java
new file mode 100644
index 00000000..af4dbd18
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/Utils.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing;
+
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager;
+import android.net.Uri;
+import android.os.Build;
+import android.text.TextUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class Utils {
+ public static String getUriStringForResource(Context context, int resId) {
+ if (resId == 0) {
+ return "";
+ }
+ Resources res = context.getResources();
+ return new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+ .authority(res.getResourcePackageName(resId))
+ .path(res.getResourceTypeName(resId))
+ .appendPath(res.getResourceEntryName(resId)).build().toString();
+ }
+
+ public static void copy(InputStream is, OutputStream os) throws IOException {
+ byte[] buffer = new byte[1024];
+ int len;
+ while ((len = is.read(buffer)) != -1) {
+ os.write(buffer, 0, len);
+ }
+ }
+
+ public static String getServiceNameFromInputId(Context context, String inputId) {
+ TvInputManager tim = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
+ for (TvInputInfo info : tim.getTvInputList()) {
+ if (info.getId().equals(inputId)) {
+ return info.getServiceInfo().name;
+ }
+ }
+ return null;
+ }
+
+ public static String getInputIdFromComponentName(Context context, ComponentName name) {
+ TvInputManager tim = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
+ for (TvInputInfo info : tim.getTvInputList()) {
+ ServiceInfo si = info.getServiceInfo();
+ if (new ComponentName(si.packageName, si.name).equals(name)) {
+ return info.getId();
+ }
+ }
+ return null;
+ }
+
+ public static TvContentRating[] stringToContentRatings(String commaSeparatedRatings) {
+ if (TextUtils.isEmpty(commaSeparatedRatings)) {
+ return null;
+ }
+ String[] ratings = commaSeparatedRatings.split("\\s*,\\s*");
+ TvContentRating[] contentRatings = new TvContentRating[ratings.length];
+ for (int i = 0; i < contentRatings.length; ++i) {
+ contentRatings[i] = TvContentRating.unflattenFromString(ratings[i]);
+ }
+ return contentRatings;
+ }
+
+ public static String contentRatingsToString(TvContentRating[] contentRatings) {
+ if (contentRatings == null || contentRatings.length == 0) {
+ return null;
+ }
+ final String DELIMITER = ",";
+ StringBuilder ratings = new StringBuilder(contentRatings[0].flattenToString());
+ for (int i = 1; i < contentRatings.length; ++i) {
+ ratings.append(DELIMITER);
+ ratings.append(contentRatings[i].flattenToString());
+ }
+ return ratings.toString();
+ }
+
+ private Utils() {}
+}
diff --git a/tests/common/src/com/android/tv/testing/testinput/ChannelState.java b/tests/common/src/com/android/tv/testing/testinput/ChannelState.java
new file mode 100644
index 00000000..3d234dac
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/testinput/ChannelState.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.testinput;
+
+import android.media.tv.TvTrackInfo;
+
+import com.android.tv.testing.Constants;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Versioned state information for a channel.
+ */
+public class ChannelState {
+
+ /**
+ * The video track a channel has by default.
+ */
+ public static TvTrackInfo DEFAULT_VIDEO_TRACK = Constants.FHD1080P50_VIDEO_TRACK;
+ /**
+ * The video track a channel has by default.
+ */
+ public static TvTrackInfo DEFAULT_AUDIO_TRACK = Constants.EN_STEREO_AUDIO_TRACK;
+ /**
+ * The channel is "tuned" and video availale.
+ *
+ * @see #getTuneStatus()
+ */
+ public static int TUNE_STATUS_VIDEO_AVAILABLE = -2;
+
+ private static int CHANNEL_VERSION_DEFAULT = 1;
+ /**
+ * Default ChannelState with version @{value #CHANNEL_VERSION_DEFAULT} and default {@link
+ * ChannelStateData}.
+ */
+ public static final ChannelState DEFAULT = new ChannelState(CHANNEL_VERSION_DEFAULT,
+ new ChannelStateData());
+ private final int mVersion;
+ private final ChannelStateData mData;
+
+
+ private ChannelState(int version, ChannelStateData channelStateData) {
+ mVersion = version;
+ mData = channelStateData;
+ }
+
+ /**
+ * Returns the id of the selected audio track, or null if none is selected.
+ */
+ public String getSelectedAudioTrackId() {
+ return mData.mSelectedAudioTrackId;
+ }
+
+ /**
+ * Returns the id of the selected audio track, or null if none is selected.
+ */
+ public String getSelectedVideoTrackId() {
+ return mData.mSelectedVideoTrackId;
+ }
+
+ /**
+ * The current version. Larger version numbers are newer.
+ *
+ * <p>The version is increased by {@link #next(ChannelStateData)}.
+ */
+ public int getVersion() {
+ return mVersion;
+ }
+
+ /**
+ * Tune status is either {@link #TUNE_STATUS_VIDEO_AVAILABLE} or a {@link
+ * android.media.tv.TvInputService.Session#notifyVideoUnavailable(int) video unavailable
+ * reason}
+ */
+ public int getTuneStatus() {
+ return mData.mTuneStatus;
+ }
+
+ /**
+ * An unmodifiable list of TvTrackInfo for a channel, suitable for {@link
+ * android.media.tv.TvInputService.Session#notifyTracksChanged(List)}
+ */
+ public List<TvTrackInfo> getTrackInfoList() {
+ return Collections.unmodifiableList(mData.mTvTrackInfos);
+ }
+
+ @Override
+ public String toString() {
+ return "v" + mVersion + ":" + mData;
+ }
+
+ /**
+ * Creates a new ChannelState, with an incremented version and {@code data} provided.
+ *
+ * @param data the data for the new ChannelState
+ */
+ public ChannelState next(ChannelStateData data) {
+ return new ChannelState(mVersion + 1, data);
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/testinput/ChannelStateData.aidl b/tests/common/src/com/android/tv/testing/testinput/ChannelStateData.aidl
new file mode 100644
index 00000000..cdf43adb
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/testinput/ChannelStateData.aidl
@@ -0,0 +1,3 @@
+package com.android.tv.testing.testinput;
+
+parcelable ChannelStateData;
diff --git a/tests/common/src/com/android/tv/testing/testinput/ChannelStateData.java b/tests/common/src/com/android/tv/testing/testinput/ChannelStateData.java
new file mode 100644
index 00000000..9bac9d12
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/testinput/ChannelStateData.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.testinput;
+
+import android.media.tv.TvTrackInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Mutable unversioned channel state.
+ */
+public final class ChannelStateData implements Parcelable {
+ public static final Creator<ChannelStateData> CREATOR = new Creator<ChannelStateData>() {
+ @Override
+ public ChannelStateData createFromParcel(Parcel in) {
+ return new ChannelStateData(in);
+ }
+
+ @Override
+ public ChannelStateData[] newArray(int size) {
+ return new ChannelStateData[size];
+ }
+ };
+
+ public final List<TvTrackInfo> mTvTrackInfos = new ArrayList<>();
+ public int mTuneStatus = ChannelState.TUNE_STATUS_VIDEO_AVAILABLE;
+ public String mSelectedAudioTrackId = ChannelState.DEFAULT_AUDIO_TRACK.getId();
+ public String mSelectedVideoTrackId = ChannelState.DEFAULT_VIDEO_TRACK.getId();
+
+ public ChannelStateData() {
+ mTvTrackInfos.add(ChannelState.DEFAULT_VIDEO_TRACK);
+ mTvTrackInfos.add(ChannelState.DEFAULT_AUDIO_TRACK);
+ }
+
+ private ChannelStateData(Parcel in) {
+ mTuneStatus = in.readInt();
+ in.readTypedList(mTvTrackInfos, TvTrackInfo.CREATOR);
+ mSelectedAudioTrackId = in.readString();
+ mSelectedVideoTrackId = in.readString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mTuneStatus);
+ dest.writeTypedList(mTvTrackInfos);
+ dest.writeString(mSelectedAudioTrackId);
+ dest.writeString(mSelectedVideoTrackId);
+ }
+
+ @Override
+ public String toString() {
+ return "{"
+ + "tune=" + mTuneStatus
+ + ", tracks=" + mTvTrackInfos
+ + "}";
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/testinput/ITestInputControl.aidl b/tests/common/src/com/android/tv/testing/testinput/ITestInputControl.aidl
new file mode 100644
index 00000000..a82f378b
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/testinput/ITestInputControl.aidl
@@ -0,0 +1,9 @@
+package com.android.tv.testing.testinput;
+
+
+import com.android.tv.testing.testinput.ChannelStateData;
+
+/** Remote interface for controlling the test TV Input Service */
+interface ITestInputControl {
+ void updateChannelState(int origId, in ChannelStateData data);
+}
diff --git a/tests/common/src/com/android/tv/testing/testinput/TestInputControlConnection.java b/tests/common/src/com/android/tv/testing/testinput/TestInputControlConnection.java
new file mode 100644
index 00000000..9b3f8835
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/testinput/TestInputControlConnection.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.testinput;
+
+import android.content.ComponentName;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.tv.testing.ChannelInfo;
+
+/**
+ * Connection for controlling the Test TV Input Service.
+ *
+ * <p>Wrapped methods for calling {@link ITestInputControl} that waits for a binding and rethrows
+ * {@link RemoteException} as {@link RuntimeException } are also included.
+ */
+public class TestInputControlConnection implements ServiceConnection {
+ private static final String TAG = "TestInputControlConn";
+ private static final int BOUND_CHECK_INTERVAL_MS = 10;
+
+ private ITestInputControl mControl;
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mControl = ITestInputControl.Stub.asInterface(service);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ Log.w(TAG, "TestInputControl service disconnected unexpectedly.");
+ mControl = null;
+ }
+
+ /**
+ * Is the service currently connected.
+ */
+ public boolean isBound() {
+ return mControl != null;
+ }
+
+ /**
+ * Update the state of the channel.
+ *
+ * @param channel the channel to update.
+ * @param data the new state for the channel.
+ */
+ public void updateChannelState(ChannelInfo channel, ChannelStateData data) {
+ waitUntilBound();
+ try {
+ mControl.updateChannelState(channel.originalNetworkId, data);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Sleep until {@link #isBound()} is true;
+ */
+ public void waitUntilBound() {
+ while (!isBound()) {
+ SystemClock.sleep(BOUND_CHECK_INTERVAL_MS);
+ }
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/testinput/TestInputControlUtils.java b/tests/common/src/com/android/tv/testing/testinput/TestInputControlUtils.java
new file mode 100644
index 00000000..54aacf20
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/testinput/TestInputControlUtils.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.testinput;
+
+import android.content.ComponentName;
+import android.content.Intent;
+
+/**
+ * Static utils for {@link ITestInputControl}.
+ */
+public final class TestInputControlUtils {
+
+ public static Intent createIntent() {
+ return new Intent().setComponent(new ComponentName("com.android.tv.testinput",
+ "com.android.tv.testinput.TestInputControlService"));
+ }
+
+ private TestInputControlUtils() {
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/testinput/TvTestInputConstants.java b/tests/common/src/com/android/tv/testing/testinput/TvTestInputConstants.java
new file mode 100644
index 00000000..e7ff4f5d
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/testinput/TvTestInputConstants.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.testinput;
+
+import com.android.tv.testing.ChannelInfo;
+
+/**
+ * Constants for interacting with TvTestInput.
+ */
+public final class TvTestInputConstants {
+
+ /**
+ * Channel 1.
+ *
+ * <p> By convention Channel 1 should not be changed. Test often start by tuning to this
+ * channel.
+ */
+ public static final ChannelInfo CH_1 = ChannelInfo.create(null, 1);
+ /**
+ * Channel 2.
+ *
+ * <p> By convention the state of Channel 2 is changed by tests. Testcases should explicitly
+ * set the state of this channel before using it in tests.
+ */
+ public static final ChannelInfo CH_2 = ChannelInfo.create(null, 2);
+}
diff --git a/tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java b/tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java
new file mode 100644
index 00000000..3a2f5509
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.uihelper;
+
+import android.content.res.Resources;
+import android.support.test.uiautomator.UiDevice;
+
+/**
+ * Base class for building UiAutomator Helper classes.
+ */
+public abstract class BaseUiDeviceHelper {
+ protected final UiDevice mUiDevice;
+ protected final Resources mTargetResources;
+
+ public BaseUiDeviceHelper(UiDevice uiDevice, Resources targetResources) {
+ this.mUiDevice = uiDevice;
+ this.mTargetResources = targetResources;
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/uihelper/ByResource.java b/tests/common/src/com/android/tv/testing/uihelper/ByResource.java
new file mode 100644
index 00000000..a76ee1d3
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/uihelper/ByResource.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.uihelper;
+
+import android.content.res.Resources;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+
+/**
+ * Convenience methods for creating {@link BySelector}s using resource ids.
+ */
+public final class ByResource {
+
+ /**
+ * Creates a BySelector for the {@code resId} from {@code resources}
+ *
+ * @see By#res(String)
+ */
+ public static BySelector id(Resources resources, int resId) {
+ String id = resources.getResourceName(resId);
+ return By.res(id);
+ }
+
+ /**
+ * Creates a BySelector for the text of {@code stringRes} from {@code resources}.
+ *
+ * @see By#text(String)
+ */
+ public static BySelector text(Resources resources, int stringRes) {
+ String text = resources.getString(stringRes);
+ return By.text(text);
+ }
+
+ private ByResource() {
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/uihelper/Constants.java b/tests/common/src/com/android/tv/testing/uihelper/Constants.java
new file mode 100644
index 00000000..0297f3db
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/uihelper/Constants.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.uihelper;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+
+public final class Constants {
+
+ public static final double EXTRA_TIMEOUT_PRECENT = .05;
+ public static final int MIN_EXTRA_TIMEOUT = 10;
+ public static final long MAX_SHOW_DELAY_MILLIS = 200;
+ public static final String TV_APP_PACKAGE = "com.android.tv";
+ public static final BySelector CHANNEL_BANNER = By.res(TV_APP_PACKAGE, "channel_banner_view");
+ public static final BySelector MENU = By.res(TV_APP_PACKAGE, "menu_list");
+ public static final BySelector SIDE_PANEL = By.res(TV_APP_PACKAGE, "side_panel");
+ public static final BySelector PROGRAM_GUIDE = By.res(TV_APP_PACKAGE, "program_guide");
+ public static final BySelector FOCUSED_VIEW = By.focused(true);
+
+ private Constants() {
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java b/tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java
new file mode 100644
index 00000000..a2476a68
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.uihelper;
+
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition;
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.waitForCondition;
+
+import android.app.DialogFragment;
+import android.content.res.Resources;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+
+import com.android.tv.R;
+import com.android.tv.testing.uihelper.BaseUiDeviceHelper;
+import com.android.tv.testing.uihelper.ByResource;
+import com.android.tv.testing.uihelper.Constants;
+
+/**
+ * Helper for testing {@link DialogFragment}s.
+ */
+public class DialogHelper extends BaseUiDeviceHelper {
+ private final BySelector byPinDialog;
+
+ public DialogHelper(UiDevice uiDevice, Resources targetResources) {
+ super(uiDevice, targetResources);
+ byPinDialog = ByResource.id(mTargetResources, R.id.enter_pin);
+ }
+
+ public void assertWaitForPinDialogOpen() {
+ assertWaitForCondition(mUiDevice, Until.hasObject(byPinDialog),
+ Constants.MAX_SHOW_DELAY_MILLIS
+ + mTargetResources.getInteger(R.integer.pin_dialog_anim_duration));
+ }
+
+ public void assertWaitForPinDialogClose() {
+ assertWaitForCondition(mUiDevice, Until.gone(byPinDialog));
+ }
+
+ public void enterPinCodes() {
+ // Enter PIN code '0000' by pressing ENTER key four times.
+ mUiDevice.pressEnter();
+ mUiDevice.pressEnter();
+ mUiDevice.pressEnter();
+ mUiDevice.pressEnter();
+ boolean result = waitForCondition(mUiDevice, Until.gone(byPinDialog));
+ if (!result) {
+ // It's the first time. Confirm the PIN code.
+ mUiDevice.pressEnter();
+ mUiDevice.pressEnter();
+ mUiDevice.pressEnter();
+ mUiDevice.pressEnter();
+ }
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java b/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java
new file mode 100644
index 00000000..949e0c9b
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java
@@ -0,0 +1,43 @@
+package com.android.tv.testing.uihelper;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+
+import junit.framework.Assert;
+
+/**
+ * Helper for testing the Live Channels Application.
+ */
+public class LiveChannelsUiDeviceHelper extends BaseUiDeviceHelper {
+ private static final int APPLICATION_START_TIMEOUT_MSEC = 500;
+
+ private final Context mContext;
+
+ public LiveChannelsUiDeviceHelper(UiDevice uiDevice, Resources targetResources,
+ Context context) {
+ super(uiDevice, targetResources);
+ mContext = context;
+ }
+
+ public void assertAppStarted() {
+ Intent intent = mContext.getPackageManager()
+ .getLaunchIntentForPackage(Constants.TV_APP_PACKAGE);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); // Clear out any previous instances
+ mContext.startActivity(intent);
+ mUiDevice.waitForIdle();
+
+ Assert.assertTrue(Constants.TV_APP_PACKAGE + " did not start", mUiDevice
+ .wait(Until.hasObject(By.pkg(Constants.TV_APP_PACKAGE).depth(0)),
+ APPLICATION_START_TIMEOUT_MSEC));
+
+ BySelector welcome = ByResource.id(mTargetResources, com.android.tv.R.id.intro);
+ if (mUiDevice.hasObject(welcome)) {
+ mUiDevice.pressBack();
+ }
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java b/tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java
new file mode 100644
index 00000000..fe16ec27
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.uihelper;
+
+import static com.android.tv.testing.uihelper.Constants.MENU;
+
+import android.content.res.Resources;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import com.android.tv.R;
+import com.android.tv.common.TvCommonConstants;
+import com.android.tv.menu.MenuView;
+
+import junit.framework.Assert;
+
+/**
+ * Helper for testing {@link MenuView}.
+ */
+public class MenuHelper extends BaseUiDeviceHelper {
+ private final BySelector byChannels;
+
+ public MenuHelper(UiDevice uiDevice, Resources targetResources) {
+ super(uiDevice, targetResources);
+ byChannels = ByResource.id(mTargetResources, R.id.item_list)
+ .hasDescendant(ByResource.text(mTargetResources, getMenuTitleChannelsResId()));
+ }
+
+ public int getMenuTitleChannelsResId() {
+ return TvCommonConstants.IS_MNC_OR_HIGHER ? R.string.menu_title_channels
+ : R.string.menu_title_channels_legacy;
+ }
+
+ public BySelector getByChannels() {
+ return byChannels;
+ }
+
+
+ /**
+ * Navigate to the menu item with the text {@code itemTextResId} in the row with text
+ * {@code rowTitleResId}.
+ * <p>
+ * Fails if the menu item can not be navigated to.
+ *
+ * @param rowTitleResId the resource id of the string in the desired row title.
+ * @param itemTextResId the resource id of the string in the desired item.
+ * @return the item navigated to.
+ */
+ public UiObject2 assertNavigateToMenuItem(int rowTitleResId, int itemTextResId) {
+ UiObject2 row = assertNavigateToRow(rowTitleResId);
+ BySelector byListView = ByResource.id(mTargetResources, R.id.list_view);
+ UiObject2 listView = row.findObject(byListView);
+ Assert.assertNotNull(
+ "Menu row '" + mTargetResources.getString(rowTitleResId) + "' does not have a "
+ + byListView, listView);
+ return assertNavigateToRowItem(listView, itemTextResId);
+ }
+
+ /**
+ * Navigate to the menu row with the text title {@code rowTitleResId}.
+ * <p>
+ * Fails if the menu row can not be navigated to.
+ * We can't navigate to the Play controls row with this method, because the row doesn't have the
+ * title when it is selected. Use {@link #assertNavigateToPlayControlsRow} for the row instead.
+ *
+ * @param rowTitleResId the resource id of the string in the desired row title.
+ * @return the row navigated to.
+ */
+ public UiObject2 assertNavigateToRow(int rowTitleResId) {
+ UiDeviceAsserts.assertHas(mUiDevice, MENU, true);
+ UiObject2 menu = mUiDevice.findObject(MENU);
+ // TODO: handle play controls. They have a different dom structure and navigation sometimes
+ // can get stuck on that row.
+ return UiDeviceAsserts.assertNavigateTo(mUiDevice, menu,
+ By.hasDescendant(ByResource.text(mTargetResources, rowTitleResId)), Direction.DOWN);
+ }
+
+ /**
+ * Navigate to the Play controls row.
+ * <p>
+ * Fails if the row can not be navigated to.
+ *
+ * @see #assertNavigateToRow
+ */
+ public void assertNavigateToPlayControlsRow() {
+ UiDeviceAsserts.assertHas(mUiDevice, MENU, true);
+ // The play controls row doesn't have title when selected, so can't use
+ // MenuHelper.assertNavigateToRow().
+ assertNavigateToRow(getMenuTitleChannelsResId());
+ mUiDevice.pressDPadUp();
+ }
+
+ /**
+ * Navigate to the menu item in the given {@code row} with the text {@code itemTextResId} .
+ * <p>
+ * Fails if the menu item can not be navigated to.
+ *
+ * @param row the container to look for menu items in.
+ * @param itemTextResId the resource id of the string in the desired item.
+ * @return the item navigated to.
+ */
+ public UiObject2 assertNavigateToRowItem(UiObject2 row, int itemTextResId) {
+ return UiDeviceAsserts.assertNavigateTo(mUiDevice, row,
+ By.hasDescendant(ByResource.text(mTargetResources, itemTextResId)),
+ Direction.RIGHT);
+ }
+
+ public UiObject2 assertPressOptionsChannelSources() {
+ return assertPressMenuItem(R.string.menu_title_options,
+ R.string.options_item_channel_sources);
+ }
+
+
+ public UiObject2 assertPressOptionsClosedCaptions() {
+ return assertPressMenuItem(R.string.menu_title_options,
+ R.string.options_item_closed_caption);
+ }
+
+ public UiObject2 assertPressOptionsDisplayMode() {
+ return assertPressMenuItem(R.string.menu_title_options, R.string.options_item_display_mode);
+ }
+
+ public UiObject2 assertPressOptionsMultiAudio() {
+ return assertPressMenuItem(R.string.menu_title_options, R.string.options_item_multi_audio);
+ }
+
+ public UiObject2 assertPressOptionsParentalControls() {
+ return assertPressMenuItem(R.string.menu_title_options,
+ R.string.options_item_parental_controls);
+ }
+
+ public UiObject2 assertPressProgramGuide() {
+ return assertPressMenuItem(getMenuTitleChannelsResId(),
+ R.string.channels_item_program_guide);
+ }
+
+ /**
+ * Navigate to the menu item with the text {@code itemTextResId} in the row with text
+ * {@code rowTitleResId}.
+ * <p>
+ * Fails if the menu item can not be navigated to.
+ *
+ * @param rowTitleResId the resource id of the string in the desired row title.
+ * @param itemTextResId the resource id of the string in the desired item.
+ * @return the item navigated to.
+ */
+ public UiObject2 assertPressMenuItem(int rowTitleResId, int itemTextResId) {
+ showMenu();
+ UiObject2 item = assertNavigateToMenuItem(rowTitleResId, itemTextResId);
+ mUiDevice.pressDPadCenter();
+ return item;
+ }
+
+ /**
+ * Waits until the menu is visible.
+ */
+ public void assertWaitForMenu() {
+ UiDeviceAsserts.assertWaitForCondition(mUiDevice, Until.hasObject(MENU));
+ }
+
+ /**
+ * Show the menu.
+ * <p>
+ * Fails if the menu does not appear in {@link Constants#MAX_SHOW_DELAY_MILLIS}.
+ */
+ public void showMenu() {
+ if (!mUiDevice.hasObject(MENU)) {
+ mUiDevice.pressMenu();
+ UiDeviceAsserts.assertWaitForCondition(mUiDevice, Until.hasObject(MENU));
+ }
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java b/tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java
new file mode 100644
index 00000000..d342723d
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.uihelper;
+
+import android.content.res.Resources;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+
+import com.android.tv.R;
+import com.android.tv.testing.uihelper.BaseUiDeviceHelper;
+import com.android.tv.testing.uihelper.ByResource;
+import com.android.tv.testing.uihelper.Constants;
+import com.android.tv.testing.uihelper.UiDeviceAsserts;
+import com.android.tv.ui.sidepanel.SideFragment;
+
+import junit.framework.Assert;
+
+/**
+ * Helper for testing {@link SideFragment}s.
+ */
+public class SidePanelHelper extends BaseUiDeviceHelper {
+
+ public SidePanelHelper(UiDevice uiDevice, Resources targetResources) {
+ super(uiDevice, targetResources);
+ }
+
+ public BySelector bySidePanelTitled(int titleResId) {
+ return By.copy(Constants.SIDE_PANEL)
+ .hasDescendant(ByResource.text(mTargetResources, titleResId));
+ }
+
+ public UiObject2 assertNavigateToItem(int resId) {
+ String title = mTargetResources.getString(resId);
+ return assertNavigateToItem(title);
+ }
+
+ public UiObject2 assertNavigateToItem(String title) {
+ BySelector sidePanelSelector = ByResource.id(mTargetResources, R.id.side_panel_list);
+ UiObject2 sidePanelList = mUiDevice.findObject(sidePanelSelector);
+ Assert.assertNotNull(sidePanelSelector + " not found", sidePanelList);
+
+ return UiDeviceAsserts
+ .assertNavigateTo(mUiDevice, sidePanelList, By.hasDescendant(By.text(title)),
+ Direction.DOWN);
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java b/tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java
new file mode 100644
index 00000000..90d5a297
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.uihelper;
+
+import static com.android.tv.testing.uihelper.Constants.FOCUSED_VIEW;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.SearchCondition;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import junit.framework.Assert;
+
+/**
+ * Asserts for {@link UiDevice}s.
+ */
+public final class UiDeviceAsserts {
+
+ public static void assertHas(UiDevice uiDevice, BySelector bySelector, boolean expected) {
+ assertEquals("Has " + bySelector, expected, uiDevice.hasObject(bySelector));
+ }
+
+ /**
+ * Assert that {@code searchCondition} becomes true within
+ * {@value Constants#MAX_SHOW_DELAY_MILLIS} milliseconds.
+ *
+ * @param uiDevice the device under test.
+ * @param searchCondition the condition to wait for.
+ */
+ public static void assertWaitForCondition(UiDevice uiDevice,
+ SearchCondition<Boolean> searchCondition) {
+ assertWaitForCondition(uiDevice, searchCondition, Constants.MAX_SHOW_DELAY_MILLIS);
+ }
+
+ /**
+ * Assert that {@code searchCondition} becomes true within {@code timeout} milliseconds.
+ *
+ * @param uiDevice the device under test.
+ * @param searchCondition the condition to wait for.
+ */
+ public static void assertWaitForCondition(UiDevice uiDevice,
+ SearchCondition<Boolean> searchCondition, long timeout) {
+ boolean result = waitForCondition(uiDevice, searchCondition, timeout);
+ assertTrue(searchCondition + " not true after " + timeout / 1000.0 + " seconds.", result);
+ }
+
+ /**
+ * Wait until {@code searchCondition} becomes true.
+ *
+ * @param uiDevice The device under test.
+ * @param searchCondition The condition to wait for.
+ * @return {@true} if the condition is met, otherwise {@code false}.
+ */
+ public static boolean waitForCondition(UiDevice uiDevice,
+ SearchCondition<Boolean> searchCondition) {
+ return waitForCondition(uiDevice, searchCondition, Constants.MAX_SHOW_DELAY_MILLIS);
+ }
+
+ private static boolean waitForCondition(UiDevice uiDevice,
+ SearchCondition<Boolean> searchCondition, long timeout) {
+ long adjustedTimeout = timeout + Math.max(Constants.MIN_EXTRA_TIMEOUT,
+ (long) (timeout * Constants.EXTRA_TIMEOUT_PRECENT));
+ return uiDevice.wait(searchCondition, adjustedTimeout);
+ }
+
+ /**
+ * Navigates through the focus items in a container returning the container child that has a
+ * descendant matching the {@code selector}.
+ * <p>
+ * The navigation starts in the {@code direction} specified and
+ * {@link Direction#reverse(Direction) reverses} once if needed. Fails if there is not a
+ * focused
+ * descendant, or if after completing both directions no focused child has a descendant
+ * matching
+ * {@code selector}.
+ * <p>
+ * Fails if the menu item can not be navigated to.
+ *
+ * @param uiDevice the device under test.
+ * @param container contains children to navigate over.
+ * @param selector the selector for the object to navigate to.
+ * @param direction the direction to start navigating.
+ * @return the object navigated to.
+ */
+ public static UiObject2 assertNavigateTo(UiDevice uiDevice, UiObject2 container,
+ BySelector selector, Direction direction) {
+ int count = 0;
+ while (count < 2) {
+ BySelector hasFocusedDescendant = By.hasDescendant(FOCUSED_VIEW);
+ UiObject2 focusedChild = null;
+ SearchCondition<Boolean> untilHasFocusedDescendant = Until
+ .hasObject(hasFocusedDescendant);
+
+ boolean result = container.wait(untilHasFocusedDescendant,
+ UiObject2Asserts.getAdjustedTimeout(Constants.MAX_SHOW_DELAY_MILLIS));
+ if (!result) {
+ // HACK: Try direction anyways because play control does not always have a
+ // focused item.
+ UiDeviceUtils.pressDpad(uiDevice, direction);
+ UiObject2Asserts.assertWaitForCondition(container, untilHasFocusedDescendant);
+ }
+
+ for (UiObject2 c : container.getChildren()) {
+ if (c.isFocused() || c.hasObject(hasFocusedDescendant)) {
+ focusedChild = c;
+ break;
+ }
+ }
+ if (focusedChild == null) {
+ Assert.fail("No focused item found in container " + container);
+ }
+ if (focusedChild.hasObject(selector)) {
+ return focusedChild;
+ }
+ if (!UiObject2Utils.hasSiblingInDirection(focusedChild, direction)) {
+ direction = Direction.reverse(direction);
+ count++;
+ }
+ UiDeviceUtils.pressDpad(uiDevice, direction);
+ }
+ Assert.fail("Could not find item with " + selector);
+ return null;
+ }
+
+ private UiDeviceAsserts() {
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java b/tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java
new file mode 100644
index 00000000..420e0824
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.uihelper;
+
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.view.KeyEvent;
+
+/**
+ * Static utility methods for {@link UiDevice}.
+ */
+public final class UiDeviceUtils {
+
+ public static boolean pressDpad(UiDevice uiDevice, Direction direction) {
+ switch (direction) {
+ case UP:
+ return uiDevice.pressDPadUp();
+ case DOWN:
+ return uiDevice.pressDPadDown();
+ case LEFT:
+ return uiDevice.pressDPadLeft();
+ case RIGHT:
+ return uiDevice.pressDPadRight();
+ default:
+ throw new IllegalArgumentException(direction.toString());
+ }
+ }
+
+
+ public static void pressKeys(UiDevice uiDevice, int... keyCodes) {
+ for (int k : keyCodes) {
+ uiDevice.pressKeyCode(k);
+ }
+ }
+
+ /**
+ * Parses the string and sends the corresponding individual key preses.
+ * <p>
+ * <b>Note:</b> only handles 0-9, '.', and '-'.
+ */
+ public static void pressKeys(UiDevice uiDevice, String keys) {
+ for (char c : keys.toCharArray()) {
+ if (c >= '0' && c <= '9') {
+ uiDevice.pressKeyCode(KeyEvent.KEYCODE_0 + c - '0');
+ } else if (c == '-') {
+ uiDevice.pressKeyCode(KeyEvent.KEYCODE_MINUS);
+ } else if (c == '.') {
+ uiDevice.pressKeyCode(KeyEvent.KEYCODE_PERIOD);
+ } else {
+ throw new IllegalArgumentException(c + " is not supported");
+ }
+ }
+ }
+
+ private UiDeviceUtils() {
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java b/tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java
new file mode 100644
index 00000000..9db70930
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.uihelper;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.support.test.uiautomator.SearchCondition;
+import android.support.test.uiautomator.UiObject2;
+
+/**
+ * Asserts for {@link UiObject2}s.
+ */
+public final class UiObject2Asserts {
+
+ /**
+ * Assert that {@code searchCondition} becomes true within
+ * {@value Constants#MAX_SHOW_DELAY_MILLIS} milliseconds.
+ *
+ * @param uiObject the device under test.
+ * @param searchCondition the condition to wait for.
+ */
+ public static void assertWaitForCondition(UiObject2 uiObject,
+ SearchCondition<Boolean> searchCondition) {
+ assertWaitForCondition(uiObject, searchCondition, Constants.MAX_SHOW_DELAY_MILLIS);
+ }
+
+ /**
+ * Assert that {@code searchCondition} becomes true within {@code timeout} milliseconds.
+ *
+ * @param uiObject the device under test.
+ * @param searchCondition the condition to wait for.
+ */
+ public static void assertWaitForCondition(UiObject2 uiObject,
+ SearchCondition<Boolean> searchCondition, long timeout) {
+ long adjustedTimeout = getAdjustedTimeout(timeout);
+ boolean result = uiObject.wait(searchCondition, adjustedTimeout);
+ assertTrue(searchCondition + " not true after " + timeout / 1000.0 + " seconds.", result);
+ }
+
+ public static long getAdjustedTimeout(long timeout) {
+ return timeout + Math.max(
+ Constants.MIN_EXTRA_TIMEOUT, (long) (timeout * Constants.EXTRA_TIMEOUT_PRECENT));
+ }
+
+ private UiObject2Asserts() {
+ }
+}
diff --git a/tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java b/tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java
new file mode 100644
index 00000000..2a997a67
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2015 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 com.android.tv.testing.uihelper;
+
+import android.graphics.Point;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+
+/**
+ * Static utility methods for {@link UiObject2}s.
+ */
+public class UiObject2Utils {
+
+ public static boolean hasSiblingInDirection(UiObject2 theUiObject, Direction direction) {
+ Point myCenter = theUiObject.getVisibleCenter();
+ for (UiObject2 sibling : theUiObject.getParent().getChildren()) {
+ Point siblingCenter = sibling.getVisibleCenter();
+ switch (direction) {
+ case UP:
+ if (myCenter.y > siblingCenter.y) {
+ return true;
+ }
+ break;
+ case DOWN:
+ if (myCenter.y < siblingCenter.y) {
+ return true;
+ }
+ break;
+ case LEFT:
+ if (myCenter.x > siblingCenter.x) {
+ return true;
+ }
+ break;
+ case RIGHT:
+ if (myCenter.x < siblingCenter.x) {
+ return true;
+ }
+ break;
+ default:
+ throw new IllegalArgumentException(direction.toString());
+ }
+ }
+ return false;
+ }
+
+ private UiObject2Utils() {
+ }
+}