aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2015-08-03 15:39:56 -0700
committerNick Chalko <nchalko@google.com>2015-08-03 15:53:37 -0700
commit816a4be1a0f34f6a48877c8afd3dbbca19eac435 (patch)
tree4f18dda269764494942f5313acc93db4a35d47db /tests
parent6edd2b09e5d16a29c703a5fcbd2e88c5cf5e55b7 (diff)
downloadTV-816a4be1a0f34f6a48877c8afd3dbbca19eac435.tar.gz
Migrate Live Channels App Src to AOSP branch
Bug: 21625152 Change-Id: I07e2830b27440556dc757e6340b4f77d1c0cbc66
Diffstat (limited to 'tests')
-rw-r--r--tests/Android.mk16
-rw-r--r--tests/AndroidManifest.xml15
-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
-rw-r--r--tests/func/Android.mk21
-rw-r--r--tests/func/AndroidManifest.xml32
-rw-r--r--tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java52
-rw-r--r--tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java58
-rw-r--r--tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java115
-rw-r--r--tests/func/src/com/android/tv/tests/ui/LiveChannelsTestCase.java96
-rw-r--r--tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java159
-rw-r--r--tests/func/src/com/android/tv/tests/ui/ProgramGuidePerformanceTest.java59
-rw-r--r--tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java42
-rw-r--r--tests/func/src/com/android/tv/tests/ui/TimeoutTest.java56
-rw-r--r--tests/input/Android.mk25
-rw-r--r--tests/input/AndroidManifest.xml70
-rw-r--r--tests/input/res/drawable-xhdpi/android_48dp.pngbin0 -> 5985 bytes
-rw-r--r--tests/input/res/drawable-xhdpi/icon.pngbin0 -> 3641 bytes
-rw-r--r--tests/input/res/values/strings.xml24
-rw-r--r--tests/input/res/xml/testtvinputservice.xml18
-rw-r--r--tests/input/src/com/android/tv/testinput/TestInputControl.java98
-rw-r--r--tests/input/src/com/android/tv/testinput/TestInputControlService.java32
-rw-r--r--tests/input/src/com/android/tv/testinput/TestTvInputService.java345
-rw-r--r--tests/input/src/com/android/tv/testinput/TestTvInputSetupActivity.java119
-rw-r--r--tests/input/src/com/android/tv/testinput/instrument/TestSetupInstrumentation.java134
-rw-r--r--tests/jank/Android.mk22
-rw-r--r--tests/jank/AndroidManifest.xml32
-rw-r--r--tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java184
-rw-r--r--tests/unit/Android.mk21
-rw-r--r--tests/unit/AndroidManifest.xml32
-rw-r--r--tests/unit/src/com/android/tv/BaseMainActivityTestCase.java135
-rw-r--r--tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java79
-rw-r--r--tests/unit/src/com/android/tv/MainActivityTest.java106
-rw-r--r--tests/unit/src/com/android/tv/TimeShiftManagerTest.java100
-rw-r--r--tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java646
-rw-r--r--tests/unit/src/com/android/tv/data/ChannelNumberTest.java87
-rw-r--r--tests/unit/src/com/android/tv/data/ChannelTest.java222
-rw-r--r--tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java533
-rw-r--r--tests/unit/src/com/android/tv/data/ProgramTest.java98
-rw-r--r--tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java109
-rw-r--r--tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java118
-rw-r--r--tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java128
-rw-r--r--tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java144
-rw-r--r--tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java140
-rw-r--r--tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java180
-rw-r--r--tests/unit/src/com/android/tv/recommendation/RecommenderTest.java324
-rw-r--r--tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java205
-rw-r--r--tests/unit/src/com/android/tv/tests/TvActivityTest.java (renamed from tests/src/com/android/tv/tests/TvActivityTest.java)4
-rw-r--r--tests/unit/src/com/android/tv/ui/SetupViewTest.java86
-rw-r--r--tests/unit/src/com/android/tv/util/FakeClock.java34
-rw-r--r--tests/unit/src/com/android/tv/util/ImageCacheTest.java75
-rw-r--r--tests/unit/src/com/android/tv/util/ScaledBitmapInfoTest.java52
-rw-r--r--tests/unit/src/com/android/tv/util/TestUtils.java65
-rw-r--r--tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java72
-rw-r--r--tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java98
-rw-r--r--tests/unit/src/com/android/tv/util/UtilsTest_GetDurationString.java250
-rw-r--r--tests/unit/src/com/android/tv/util/UtilsTest_IsInGivenDay.java61
91 files changed, 8502 insertions, 31 deletions
diff --git a/tests/Android.mk b/tests/Android.mk
index cc2fff01..5053e7d6 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -1,15 +1 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-
-LOCAL_JAVA_LIBRARIES := android.test.runner
-
-# Include all test java files.
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := TVTests
-
-LOCAL_INSTRUMENTATION_FOR := TV
-
-include $(BUILD_PACKAGE)
+include $(call all-subdir-makefiles)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
deleted file mode 100644
index 7c590925..00000000
--- a/tests/AndroidManifest.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.tv.tests" >
-
- <uses-sdk android:minSdkVersion="19" />
-
- <instrumentation
- android:name="android.test.InstrumentationTestRunner"
- android:targetPackage="com.android.tv" />
-
- <application android:label="TVTest" >
- <uses-library android:name="android.test.runner" />
- </application>
-
-</manifest> \ No newline at end of file
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() {
+ }
+}
diff --git a/tests/func/Android.mk b/tests/func/Android.mk
new file mode 100644
index 00000000..53ed5e72
--- /dev/null
+++ b/tests/func/Android.mk
@@ -0,0 +1,21 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := TVFuncTests
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-test \
+ tv-test-common \
+ ub-uiautomator \
+
+LOCAL_INSTRUMENTATION_FOR := TV
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/func/AndroidManifest.xml b/tests/func/AndroidManifest.xml
new file mode 100644
index 00000000..29b018e1
--- /dev/null
+++ b/tests/func/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tv.tests.ui" >
+
+ <uses-sdk android:targetSdkVersion="23" android:minSdkVersion="21" />
+
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:label="Live Channel Functional Tests"
+ android:targetPackage="com.android.tv" />
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+</manifest>
diff --git a/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java b/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java
new file mode 100644
index 00000000..61939fdb
--- /dev/null
+++ b/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.tests.ui;
+
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition;
+
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.tv.R;
+import com.android.tv.testing.uihelper.Constants;
+
+@SmallTest
+public class ChannelBannerViewTest extends LiveChannelsTestCase {
+ // Channel banner show duration with the grace period.
+ private long mShowDurationMillis;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mLiveChannelsHelper.assertAppStarted();
+ mShowDurationMillis = mTargetResources.getInteger(R.integer.channel_banner_show_duration)
+ + Constants.MAX_SHOW_DELAY_MILLIS;
+ }
+
+ public void testChannelBannerAppearDisappear() {
+ mDevice.pressDPadCenter();
+ assertWaitForCondition(mDevice, Until.hasObject(Constants.CHANNEL_BANNER));
+ assertWaitForCondition(mDevice, Until.gone(Constants.CHANNEL_BANNER), mShowDurationMillis);
+ }
+
+ public void testChannelBannerShownWhenTune() {
+ mDevice.pressDPadDown();
+ assertWaitForCondition(mDevice, Until.hasObject(Constants.CHANNEL_BANNER));
+ mDevice.pressDPadUp();
+ assertWaitForCondition(mDevice, Until.hasObject(Constants.CHANNEL_BANNER));
+ }
+}
diff --git a/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java b/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java
new file mode 100644
index 00000000..9959185f
--- /dev/null
+++ b/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.tests.ui;
+
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition;
+
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.tv.R;
+import com.android.tv.testing.uihelper.ByResource;
+import com.android.tv.testing.uihelper.SidePanelHelper;
+
+/**
+ * Tests for channel sources.
+ */
+@LargeTest
+public class ChannelSourcesTest extends LiveChannelsTestCase {
+ private SidePanelHelper mSidePanelHelper;
+ private BySelector mByChannelSourceSidePanel;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mSidePanelHelper = new SidePanelHelper(mDevice, mTargetResources);
+ mByChannelSourceSidePanel = mSidePanelHelper
+ .bySidePanelTitled(R.string.side_panel_title_channel_sources);
+ }
+
+ //TODO: create a cancelable test channel setup.
+
+ public void testSetup_cancel() {
+ mLiveChannelsHelper.assertAppStarted();
+ mMenuHelper.assertPressOptionsChannelSources();
+ assertWaitForCondition(mDevice, Until.hasObject(mByChannelSourceSidePanel));
+
+ mSidePanelHelper.assertNavigateToItem(R.string.channel_source_item_setup);
+ mDevice.pressDPadCenter();
+
+ assertWaitForCondition(mDevice,
+ Until.hasObject(ByResource.text(mTargetResources, R.string.setup_title)));
+ mDevice.pressBack();
+ }
+}
diff --git a/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java b/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java
new file mode 100644
index 00000000..8190d79d
--- /dev/null
+++ b/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.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.tests.ui;
+
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertHas;
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition;
+
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.tv.R;
+import com.android.tv.testing.testinput.ChannelStateData;
+import com.android.tv.testing.testinput.TvTestInputConstants;
+import com.android.tv.testing.uihelper.Constants;
+import com.android.tv.testing.uihelper.DialogHelper;
+import com.android.tv.testing.uihelper.SidePanelHelper;
+
+/**
+ * Basic tests for the LiveChannels app.
+ */
+@LargeTest
+public class LiveChannelsAppTest extends LiveChannelsTestCase {
+ private SidePanelHelper mSidePanelHelper;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mSidePanelHelper = new SidePanelHelper(mDevice, mTargetResources);
+ mLiveChannelsHelper.assertAppStarted();
+ pressKeysForChannel(TvTestInputConstants.CH_1);
+ getInstrumentation().waitForIdleSync();
+ }
+
+ public void testChannelSourcesCancel() {
+ mMenuHelper.assertPressOptionsChannelSources();
+ BySelector byChannelSourcesSidePanel = mSidePanelHelper
+ .bySidePanelTitled(R.string.channel_source_item_customize_channels);
+ assertWaitForCondition(mDevice, Until.hasObject(byChannelSourcesSidePanel));
+ mDevice.pressBack();
+ assertWaitForCondition(mDevice, Until.gone(byChannelSourcesSidePanel));
+ assertHas(mDevice, Constants.MENU, false);
+ }
+
+ public void testClosedCaptionsCancel() {
+ mMenuHelper.assertPressOptionsClosedCaptions();
+ BySelector byClosedCaptionSidePanel = mSidePanelHelper
+ .bySidePanelTitled(R.string.side_panel_title_closed_caption);
+ assertWaitForCondition(mDevice, Until.hasObject(byClosedCaptionSidePanel));
+ mDevice.pressBack();
+ assertWaitForCondition(mDevice, Until.gone(byClosedCaptionSidePanel));
+ assertHas(mDevice, Constants.MENU, false);
+ }
+
+ public void testDisplayModeCancel() {
+ ChannelStateData data = new ChannelStateData();
+ data.mTvTrackInfos.add(com.android.tv.testing.Constants.SVGA_VIDEO_TRACK);
+ data.mSelectedVideoTrackId = com.android.tv.testing.Constants.SVGA_VIDEO_TRACK
+ .getId();
+ updateThenTune(data, TvTestInputConstants.CH_2);
+
+ mMenuHelper.assertPressOptionsDisplayMode();
+ BySelector byDisplayModeSidelPanel = mSidePanelHelper
+ .bySidePanelTitled(R.string.side_panel_title_display_mode);
+ assertWaitForCondition(mDevice, Until.hasObject(byDisplayModeSidelPanel));
+ mDevice.pressBack();
+ assertWaitForCondition(mDevice, Until.gone(byDisplayModeSidelPanel));
+ assertHas(mDevice, Constants.MENU, false);
+ }
+
+ public void testMenu() {
+ mDevice.pressMenu();
+
+ assertWaitForCondition(mDevice, Until.hasObject(Constants.MENU));
+ assertHas(mDevice, mMenuHelper.getByChannels(), true);
+ }
+
+ public void testMultiAudioCancel() {
+ ChannelStateData data = new ChannelStateData();
+ data.mTvTrackInfos.add(com.android.tv.testing.Constants.GENERIC_AUDIO_TRACK);
+ updateThenTune(data, TvTestInputConstants.CH_2);
+
+ mMenuHelper.assertPressOptionsMultiAudio();
+ BySelector byMultiAudioSidelPanel = mSidePanelHelper
+ .bySidePanelTitled(R.string.side_panel_title_multi_audio);
+ assertWaitForCondition(mDevice, Until.hasObject(byMultiAudioSidelPanel));
+ mDevice.pressBack();
+ assertWaitForCondition(mDevice, Until.gone(byMultiAudioSidelPanel));
+ assertHas(mDevice, Constants.MENU, false);
+ }
+
+ public void testPinCancel() {
+ mMenuHelper.showMenu();
+ mMenuHelper.assertPressOptionsParentalControls();
+ DialogHelper dialogHelper = new DialogHelper(mDevice, mTargetResources);
+ dialogHelper.assertWaitForPinDialogOpen();
+ mDevice.pressBack();
+ dialogHelper.assertWaitForPinDialogClose();
+ assertHas(mDevice, Constants.MENU, false);
+ }
+}
diff --git a/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestCase.java b/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestCase.java
new file mode 100644
index 00000000..af61abc4
--- /dev/null
+++ b/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestCase.java
@@ -0,0 +1,96 @@
+/*
+ * 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.tests.ui;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.test.uiautomator.UiDevice;
+import android.test.InstrumentationTestCase;
+
+import com.android.tv.testing.ChannelInfo;
+import com.android.tv.testing.testinput.ChannelStateData;
+import com.android.tv.testing.testinput.TestInputControlConnection;
+import com.android.tv.testing.testinput.TestInputControlUtils;
+import com.android.tv.testing.uihelper.Constants;
+import com.android.tv.testing.uihelper.LiveChannelsUiDeviceHelper;
+import com.android.tv.testing.uihelper.MenuHelper;
+import com.android.tv.testing.uihelper.UiDeviceUtils;
+
+/**
+ * Base test case for LiveChannel UI tests.
+ */
+public abstract class LiveChannelsTestCase extends InstrumentationTestCase {
+ protected final TestInputControlConnection mConnection = new TestInputControlConnection();
+
+ protected UiDevice mDevice;
+ protected Resources mTargetResources;
+ protected MenuHelper mMenuHelper;
+ protected LiveChannelsUiDeviceHelper mLiveChannelsHelper;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ Context context = getInstrumentation().getContext();
+ context.bindService(TestInputControlUtils.createIntent(), mConnection,
+ Context.BIND_AUTO_CREATE);
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mTargetResources = getInstrumentation().getTargetContext().getResources();
+ mMenuHelper = new MenuHelper(mDevice, mTargetResources);
+ mLiveChannelsHelper = new LiveChannelsUiDeviceHelper(mDevice, mTargetResources, context);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (mConnection.isBound()) {
+ getInstrumentation().getContext().unbindService(mConnection);
+ }
+
+ // TODO: robustly handle left over pops from failed tests.
+ // Clear any side panel, menu, ...
+ // Scene container should not be checked here because pressing the BACK key in some scenes
+ // might launch the home screen.
+ if (mDevice.hasObject(Constants.SIDE_PANEL) || mDevice.hasObject(Constants.MENU) || mDevice
+ .hasObject(Constants.PROGRAM_GUIDE)) {
+ mDevice.pressBack();
+ }
+ super.tearDown();
+ }
+
+ /**
+ * Send the keys for the channel number of {@code channel} and press the DPAD
+ * center.
+ *
+ * <p>Usually this will tune to the given channel.
+ */
+ protected void pressKeysForChannel(ChannelInfo channel) {
+ UiDeviceUtils.pressKeys(mDevice, channel.number);
+ // TODO: Sometimes, next statement doesn't close the SelectInputView. It makes the tests
+ // which use this method flaky. b/22509489
+ mDevice.pressDPadCenter();
+ }
+
+ /**
+ * Update the channel state to {@code data} then tune to that channel.
+ *
+ * @param data the state to update the channel with.
+ * @param channel the channel to tune to
+ */
+ protected void updateThenTune(ChannelStateData data, ChannelInfo channel) {
+ mConnection.updateChannelState(channel, data);
+ pressKeysForChannel(channel);
+ }
+}
diff --git a/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java b/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java
new file mode 100644
index 00000000..b96ecf18
--- /dev/null
+++ b/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.tests.ui;
+
+import static com.android.tv.testing.uihelper.Constants.FOCUSED_VIEW;
+import static com.android.tv.testing.uihelper.Constants.MENU;
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition;
+
+import android.os.Build;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.KeyEvent;
+
+import com.android.tv.R;
+import com.android.tv.testing.testinput.TvTestInputConstants;
+import com.android.tv.testing.uihelper.DialogHelper;
+import com.android.tv.testing.uihelper.SidePanelHelper;
+
+@SmallTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.MNC)
+public class PlayControlsRowViewTest extends LiveChannelsTestCase {
+ private static final int BUTTON_INDEX_PLAY_PAUSE = 2;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mLiveChannelsHelper.assertAppStarted();
+ pressKeysForChannel(TvTestInputConstants.CH_1);
+ // Tune to a new channel to ensure that the channel is changed.
+ mDevice.pressDPadUp();
+ getInstrumentation().waitForIdleSync();
+ }
+
+ /**
+ * Test the normal case. The play/pause button should have focus initially.
+ */
+ public void testFocusedViewInNormalCase() {
+ mMenuHelper.showMenu();
+ mMenuHelper.assertNavigateToPlayControlsRow();
+ assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE);
+ mDevice.pressBack();
+ }
+
+ /**
+ * Tests the case when the forwarding action is disabled.
+ * In this case, the button corresponding to the action is disabled, so play/pause button should
+ * have the focus.
+ */
+ public void testFocusedViewWithDisabledActionForward() {
+ // Fast forward button
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD);
+ mMenuHelper.assertWaitForMenu();
+ assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE);
+ mDevice.pressBack();
+
+ // Next button
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_NEXT);
+ mMenuHelper.assertWaitForMenu();
+ assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE);
+ mDevice.pressBack();
+ }
+
+ /**
+ * Tests the case when the rewinding action is disabled.
+ * In this case, the button corresponding to the action is disabled, so play/pause button should
+ * have the focus.
+ */
+ public void testFocusedViewWithDisabledActionBackward() {
+ // Previous button
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
+ mMenuHelper.assertWaitForMenu();
+ assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE);
+ mDevice.pressBack();
+
+ // Rewind button
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_REWIND);
+ mMenuHelper.assertWaitForMenu();
+ assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE);
+ mDevice.pressBack();
+ }
+
+ public void testFocusedViewInMenu() {
+ mMenuHelper.showMenu();
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_PLAY);
+ assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE);
+ mMenuHelper.assertNavigateToRow(R.string.menu_title_channels);
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_NEXT);
+ assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE);
+ }
+
+ public void testKeepPausedWhileParentalControlChange() {
+ // Pause the playback.
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_PAUSE);
+ mMenuHelper.assertWaitForMenu();
+ assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE);
+ // Show parental controls fragment.
+ mMenuHelper.assertPressOptionsParentalControls();
+ DialogHelper dialogHelper = new DialogHelper(mDevice, mTargetResources);
+ dialogHelper.assertWaitForPinDialogOpen();
+ dialogHelper.enterPinCodes();
+ dialogHelper.assertWaitForPinDialogClose();
+ SidePanelHelper sidePanelHelper = new SidePanelHelper(mDevice, mTargetResources);
+ BySelector bySidePanel = sidePanelHelper.bySidePanelTitled(R.string.menu_parental_controls);
+ assertWaitForCondition(mDevice, Until.hasObject(bySidePanel));
+ mDevice.pressEnter();
+ mDevice.pressEnter();
+ mDevice.pressBack();
+ // Return to the main menu.
+ mMenuHelper.assertWaitForMenu();
+ assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE);
+ }
+
+ public void testKeepPausedAfterVisitingHome() {
+ // Pause the playback.
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_MEDIA_PAUSE);
+ mMenuHelper.assertWaitForMenu();
+ assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE);
+ // Press HOME twice to visit the home screen and return to Live Channels.
+ mDevice.pressHome();
+ // Wait until home screen is shown.
+ mDevice.waitForIdle();
+ mDevice.pressHome();
+ // Wait until TV is resumed.
+ mDevice.waitForIdle();
+ // Return to the main menu.
+ mMenuHelper.assertWaitForMenu();
+ assertButtonHasFocus(BUTTON_INDEX_PLAY_PAUSE);
+ }
+
+ private void assertButtonHasFocus(int expectedButtonIndex) {
+ UiObject2 menu = mDevice.findObject(MENU);
+ UiObject2 focusedView = menu.findObject(FOCUSED_VIEW);
+ assertNotNull("Play controls row doesn't have a focused child.", focusedView);
+ UiObject2 focusedButtonGroup = focusedView.getParent();
+ assertNotNull("The focused item should have parent", focusedButtonGroup);
+ UiObject2 controlBar = focusedButtonGroup.getParent();
+ assertNotNull("The focused item should have grandparent", controlBar);
+ assertTrue("The grandparent should have more than five children",
+ controlBar.getChildCount() >= 5);
+ assertEquals(controlBar.getChildren().get(expectedButtonIndex), focusedButtonGroup);
+ }
+}
diff --git a/tests/func/src/com/android/tv/tests/ui/ProgramGuidePerformanceTest.java b/tests/func/src/com/android/tv/tests/ui/ProgramGuidePerformanceTest.java
new file mode 100644
index 00000000..833ed954
--- /dev/null
+++ b/tests/func/src/com/android/tv/tests/ui/ProgramGuidePerformanceTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.tests.ui;
+
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import com.android.tv.R;
+import com.android.tv.testing.uihelper.Constants;
+
+/**
+ * Tests for {@link com.android.tv.MainActivity}.
+ */
+@LargeTest
+public class ProgramGuidePerformanceTest extends LiveChannelsTestCase {
+ private static final String TAG = "ProgramGuidePerformance";
+
+ public static final int SHOW_MENU_MAX_DURATION_MS = 1500;
+ public void testShowMenu() {
+ mLiveChannelsHelper.assertAppStarted();
+ mMenuHelper.showMenu();
+ mMenuHelper.assertNavigateToMenuItem(mMenuHelper.getMenuTitleChannelsResId(),
+ R.string.channels_item_program_guide);
+ //TODO: build a simple performance framework like JankTest
+ long start = SystemClock.elapsedRealtime();
+ Log.v(TAG, "start " + start + " milliSeconds");
+ mDevice.pressDPadCenter();
+ assertWaitForCondition(mDevice, Until.hasObject(Constants.PROGRAM_GUIDE));
+ long end = SystemClock.elapsedRealtime();
+ Log.v(TAG, "end " + end + " milliSeconds");
+ long duration = end - start;
+ assertDuration("ShowMenu", SHOW_MENU_MAX_DURATION_MS, duration);
+ mDevice.pressBack();
+ }
+
+ private void assertDuration(String msg, long expectedMaxMilliSeconds, long actualMilliSeconds) {
+ Log.d(TAG, msg + " duration " + actualMilliSeconds + " milliSeconds");
+ assertTrue(msg + " duration expected to be <= " + expectedMaxMilliSeconds
+ + " milliSeconds but was " + actualMilliSeconds + " milliSeconds.",
+ actualMilliSeconds <= expectedMaxMilliSeconds);
+ }
+}
diff --git a/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java b/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java
new file mode 100644
index 00000000..2778e54a
--- /dev/null
+++ b/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.tests.ui;
+
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.tv.guide.ProgramGuide;
+import com.android.tv.testing.uihelper.Constants;
+
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertHas;
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition;
+
+/**
+ * Tests for {@link ProgramGuide}.
+ */
+@LargeTest
+public class ProgramGuideTest extends LiveChannelsTestCase {
+
+ public void testCancel() {
+ mLiveChannelsHelper.assertAppStarted();
+ mMenuHelper.assertPressProgramGuide();
+ assertWaitForCondition(mDevice,
+ Until.hasObject(Constants.PROGRAM_GUIDE));
+ mDevice.pressBack();
+ assertWaitForCondition(mDevice, Until.gone(Constants.PROGRAM_GUIDE));
+ assertHas(mDevice, Constants.MENU, false);
+ }
+}
diff --git a/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java b/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java
new file mode 100644
index 00000000..dd727eaa
--- /dev/null
+++ b/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.tests.ui;
+
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertHas;
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition;
+
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.Suppress;
+
+import com.android.tv.R;
+import com.android.tv.testing.uihelper.Constants;
+
+/**
+ * Test timeout events like the menu despairing after no input.
+ * <p>
+ * <b>WARNING</b> some of these timeouts are 60 seconds. These tests will take a long time
+ * complete.
+ */
+@Suppress // TODO: b/20111741
+@LargeTest
+public class TimeoutTest extends LiveChannelsTestCase {
+
+ public void testMenu() {
+ mLiveChannelsHelper.assertAppStarted();
+ mDevice.pressMenu();
+
+ assertWaitForCondition(mDevice, Until.hasObject(Constants.MENU));
+ assertWaitForCondition(mDevice, Until.gone(Constants.MENU),
+ mTargetResources.getInteger(R.integer.menu_show_duration));
+ }
+
+ public void testProgramGuide() {
+ mLiveChannelsHelper.assertAppStarted();
+ mMenuHelper.assertPressProgramGuide();
+ assertWaitForCondition(mDevice,
+ Until.hasObject(Constants.PROGRAM_GUIDE));
+ assertWaitForCondition(mDevice, Until.gone(Constants.PROGRAM_GUIDE),
+ mTargetResources.getInteger(R.integer.program_guide_show_duration));
+ assertHas(mDevice, Constants.MENU, false);
+ }
+}
diff --git a/tests/input/Android.mk b/tests/input/Android.mk
new file mode 100644
index 00000000..ea1d0f71
--- /dev/null
+++ b/tests/input/Android.mk
@@ -0,0 +1,25 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := TVTestInput
+LOCAL_MODULE_TAGS := optional
+LOCAL_PROGUARD_ENABLED := disabled
+# Overlay view related functionality requires system APIs.
+LOCAL_SDK_VERSION := system_current
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ tv-test-common \
+ tv-common
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/../common/res $(LOCAL_PATH)/res
+LOCAL_AAPT_FLAGS := --auto-add-overlay \
+ --extra-packages com.android.tv.testing
+
+include $(BUILD_PACKAGE)
+
+ifneq ($(filter TV,$(TARGET_BUILD_APPS)),)
+ $(call dist-for-goals,apps_only,$(LOCAL_BUILT_MODULE):$(LOCAL_PACKAGE_NAME).apk)
+endif
+
diff --git a/tests/input/AndroidManifest.xml b/tests/input/AndroidManifest.xml
new file mode 100644
index 00000000..e02924ea
--- /dev/null
+++ b/tests/input/AndroidManifest.xml
@@ -0,0 +1,70 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tv.testinput">
+
+ <uses-sdk android:targetSdkVersion="23" android:minSdkVersion="21"/>
+
+ <!-- Required to update or read existing channel and program information in TvProvider. -->
+ <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
+ <!-- Required to update channel and program information in TvProvider. -->
+ <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
+
+ <application android:label="@string/sample_tv_input"
+ android:icon="@drawable/android_48dp"
+ android:theme="@android:style/Theme.Holo.Light.NoActionBar" >
+ <!-- Launched by the TV app before it uses TestTvInputService to set up channels for this
+ input. -->
+ <activity android:name=".TestTvInputSetupActivity" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+ <service android:name=".TestTvInputService"
+ android:permission="android.permission.BIND_TV_INPUT"
+ android:label="@string/simple_input_label">
+ <!-- Required filter used by the system to launch our account service. -->
+ <intent-filter>
+ <action android:name="android.media.tv.TvInputService" />
+ </intent-filter>
+ <!-- An XML file which describes this input. This provides a pointer to the
+ TestTvInputSetupActivity to the system/TV app. -->
+ <meta-data android:name="android.media.tv.input"
+ android:resource="@xml/testtvinputservice" />
+ </service>
+ <service android:name=".TestInputControlService" android:exported="true"/>
+
+ </application>
+
+ <instrumentation
+ android:name=".instrument.TestSetupInstrumentation"
+ android:label="Test Setup Instrument"
+ android:targetPackage="com.android.tv.testinput" />
+
+ <uses-feature
+ android:name="android.hardware.touchscreen"
+ android:required="false" />
+ <uses-feature
+ android:name="android.software.leanback"
+ android:required="true" />
+ <!-- Required to expose this app in the store only when the device has TV input framework
+ with the TV app. -->
+ <uses-feature
+ android:name="android.software.live_tv"
+ android:required="true" />
+</manifest>
diff --git a/tests/input/res/drawable-xhdpi/android_48dp.png b/tests/input/res/drawable-xhdpi/android_48dp.png
new file mode 100644
index 00000000..9ea1cd14
--- /dev/null
+++ b/tests/input/res/drawable-xhdpi/android_48dp.png
Binary files differ
diff --git a/tests/input/res/drawable-xhdpi/icon.png b/tests/input/res/drawable-xhdpi/icon.png
new file mode 100644
index 00000000..8497c28c
--- /dev/null
+++ b/tests/input/res/drawable-xhdpi/icon.png
Binary files differ
diff --git a/tests/input/res/values/strings.xml b/tests/input/res/values/strings.xml
new file mode 100644
index 00000000..3f2ab3f7
--- /dev/null
+++ b/tests/input/res/values/strings.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ 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.
+ -->
+<resources>
+ <string name="app_name" translatable="false">Test TV Inputs</string>
+ <string name="about_app" translatable="false">About TV Test Inputs</string>
+ <string name="version" translatable="false">Version: %1$s</string>
+ <string name="sample_tv_input" translatable="false">Test TV Input</string>
+ <string name="simple_input_label" translatable="false">Test Input</string>
+ <string name="simple_setup_title" translatable="false">Setup Test Input</string>
+ <string name="simple_setup_message" translatable="false">Do you want to register detected channels?</string>
+</resources>
diff --git a/tests/input/res/xml/testtvinputservice.xml b/tests/input/res/xml/testtvinputservice.xml
new file mode 100644
index 00000000..74be3892
--- /dev/null
+++ b/tests/input/res/xml/testtvinputservice.xml
@@ -0,0 +1,18 @@
+<?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.
+ -->
+<tv-input xmlns:android="http://schemas.android.com/apk/res/android"
+ android:setupActivity="com.android.tv.testinput.TestTvInputSetupActivity" />
diff --git a/tests/input/src/com/android/tv/testinput/TestInputControl.java b/tests/input/src/com/android/tv/testinput/TestInputControl.java
new file mode 100644
index 00000000..cd85c86e
--- /dev/null
+++ b/tests/input/src/com/android/tv/testinput/TestInputControl.java
@@ -0,0 +1,98 @@
+/*
+ * 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.testinput;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.LongSparseArray;
+
+import com.android.tv.testing.ChannelInfo;
+import com.android.tv.testing.ChannelUtils;
+import com.android.tv.testing.testinput.ChannelState;
+import com.android.tv.testing.testinput.ChannelStateData;
+import com.android.tv.testing.testinput.ITestInputControl;
+
+import java.util.Map;
+
+/**
+ * Maintains state for the {@link TestTvInputService}.
+ *
+ * <p>Maintains the current state for every channel. A default is sent if the state is not
+ * explicitly set. The state is versioned so TestTvInputService can tell if onNotifyXXX events need
+ * to be sent.
+ *
+ * <p> Test update the state using @{link ITestInputControl} via {@link TestInputControlService}.
+ */
+class TestInputControl extends ITestInputControl.Stub {
+
+ private final static String TAG = "TestInputControl";
+ private final static TestInputControl INSTANCE = new TestInputControl();
+
+ private final LongSparseArray<ChannelInfo> mId2ChannelInfoMap = new LongSparseArray<>();
+ private final LongSparseArray<ChannelState> mOrigId2StateMap = new LongSparseArray<>();
+
+ private java.lang.String mInputId;
+ private boolean initialized;
+
+ private TestInputControl() {
+ }
+
+ public static TestInputControl getInstance() {
+ return INSTANCE;
+ }
+
+ public synchronized void init(Context context, String inputId) {
+ if (!initialized) {
+ // TODO run initialization in a separate thread.
+ mInputId = inputId;
+ updateChannelMap(context);
+ initialized = true;
+ }
+ }
+
+ private void updateChannelMap(Context context) {
+ mId2ChannelInfoMap.clear();
+ Map<Long, ChannelInfo> channelIdToInfoMap =
+ ChannelUtils.queryChannelInfoMapForTvInput(context, mInputId);
+ for (Long channelId : channelIdToInfoMap.keySet()) {
+ mId2ChannelInfoMap.put(channelId, channelIdToInfoMap.get(channelId));
+ }
+ Log.i(TAG, "Initialized channel map for " + mInputId + " with " + mId2ChannelInfoMap.size()
+ + " channels");
+ }
+
+ public ChannelInfo getChannelInfo(Uri channelUri) {
+ return mId2ChannelInfoMap.get(ContentUris.parseId(channelUri));
+ }
+
+ public ChannelState getChannelState(int originalNetworkId) {
+ return mOrigId2StateMap.get(originalNetworkId, ChannelState.DEFAULT);
+ }
+
+ @Override
+ public synchronized void updateChannelState(int origId, ChannelStateData data)
+ throws RemoteException {
+ ChannelState state;
+ ChannelState orig = getChannelState(origId);
+ state = orig.next(data);
+ mOrigId2StateMap.put(origId, state);
+
+ Log.i(TAG, "Setting channel " + origId + " state to " + state);
+ }
+}
diff --git a/tests/input/src/com/android/tv/testinput/TestInputControlService.java b/tests/input/src/com/android/tv/testinput/TestInputControlService.java
new file mode 100644
index 00000000..4a5668cc
--- /dev/null
+++ b/tests/input/src/com/android/tv/testinput/TestInputControlService.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.testinput;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Testcases communicate to the {@link TestInputControl} via
+ * {@link com.android.tv.testing.testinput.ITestInputControl}.
+ */
+public class TestInputControlService extends Service {
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return TestInputControl.getInstance();
+ }
+}
diff --git a/tests/input/src/com/android/tv/testinput/TestTvInputService.java b/tests/input/src/com/android/tv/testinput/TestTvInputService.java
new file mode 100644
index 00000000..98ac9438
--- /dev/null
+++ b/tests/input/src/com/android/tv/testinput/TestTvInputService.java
@@ -0,0 +1,345 @@
+/*
+ * 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.testinput;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.media.PlaybackParams;
+import android.media.tv.TvContract;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvInputService;
+import android.media.tv.TvTrackInfo;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Surface;
+
+import com.android.tv.common.TvCommonConstants;
+import com.android.tv.testing.ChannelInfo;
+import com.android.tv.testing.testinput.ChannelState;
+
+import java.util.Date;
+
+/**
+ * Simple TV input service which provides test channels.
+ */
+public class TestTvInputService extends TvInputService {
+ private static final String TAG = "TestTvInputServices";
+ private static final int REFRESH_DELAY_MS = 1000 / 5;
+ private static final boolean DEBUG = false;
+ private TestInputControl mBackend = TestInputControl.getInstance();
+
+ public static String buildInputId(Context context) {
+ return TvContract.buildInputId(new ComponentName(context, TestTvInputService.class));
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mBackend.init(this, buildInputId(this));
+ }
+
+ @Override
+ public Session onCreateSession(String inputId) {
+ Log.v(TAG, "Creating session for " + inputId);
+ return new SimpleSessionImpl(this);
+ }
+
+ /**
+ * Simple session implementation that just display some text.
+ */
+ private class SimpleSessionImpl extends Session {
+ private static final int MSG_SEEK = 1000;
+ private static final int SEEK_DELAY_MS = 300;
+
+ private final Paint mTextPaint = new Paint();
+ private final DrawRunnable mDrawRunnable = new DrawRunnable();
+ private Surface mSurface = null;
+ private ChannelInfo mChannel = null;
+ private ChannelState mCurrentState = null;
+ private String mCurrentVideoTrackId = null;
+ private String mCurrentAudioTrackId = null;
+
+ private long mRecordStartTimeMs;
+ private long mPausedTimeMs;
+ // The time in milliseconds when the current position is lastly updated.
+ private long mLastCurrentPositionUpdateTimeMs;
+ // The current playback position.
+ private long mCurrentPositionMs;
+ // The current playback speed rate.
+ private float mSpeed;
+
+ private final Handler mHandler = new Handler(Looper.myLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_SEEK) {
+ // Actually, this input doesn't play any videos, it just shows the image.
+ // So we should simulate the playback here by changing the current playback
+ // position periodically in order to test the time shift.
+ // If the playback is paused, the current playback position doesn't need to be
+ // changed.
+ if (mPausedTimeMs == 0) {
+ long currentTimeMs = System.currentTimeMillis();
+ mCurrentPositionMs += (long) ((currentTimeMs
+ - mLastCurrentPositionUpdateTimeMs) * mSpeed);
+ mCurrentPositionMs = Math.max(mRecordStartTimeMs,
+ Math.min(mCurrentPositionMs, currentTimeMs));
+ mLastCurrentPositionUpdateTimeMs = currentTimeMs;
+ }
+ sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS);
+ }
+ super.handleMessage(msg);
+ }
+ };
+
+ SimpleSessionImpl(Context context) {
+ super(context);
+ mTextPaint.setColor(Color.BLACK);
+ mTextPaint.setTextSize(150);
+ mHandler.post(mDrawRunnable);
+ if (DEBUG) {
+ Log.v(TAG, "Created session " + this);
+ }
+ }
+
+ private void setAudioTrack(String selectedAudioTrackId) {
+ Log.i(TAG, "Set audio track to " + selectedAudioTrackId);
+ mCurrentAudioTrackId = selectedAudioTrackId;
+ notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, mCurrentAudioTrackId);
+ }
+
+ private void setVideoTrack(String selectedVideoTrackId) {
+ Log.i(TAG, "Set video track to " + selectedVideoTrackId);
+ mCurrentVideoTrackId = selectedVideoTrackId;
+ notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, mCurrentVideoTrackId);
+ }
+
+ @Override
+ public void onRelease() {
+ if (DEBUG) {
+ Log.v(TAG, "Releasing session " + this);
+ }
+ mDrawRunnable.cancel();
+ mHandler.removeCallbacks(mDrawRunnable);
+ mSurface = null;
+ mChannel = null;
+ mCurrentState = null;
+ }
+
+ @Override
+ public boolean onSetSurface(Surface surface) {
+ synchronized (mDrawRunnable) {
+ mSurface = surface;
+ }
+ if (surface != null) {
+ if (DEBUG) {
+ Log.v(TAG, "Surface set");
+ }
+ } else {
+ if (DEBUG) {
+ Log.v(TAG, "Surface unset");
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onSurfaceChanged(int format, int width, int height) {
+ super.onSurfaceChanged(format, width, height);
+ Log.d(TAG, "format=" + format + " width=" + width + " height=" + height);
+ }
+
+ @Override
+ public void onSetStreamVolume(float volume) {
+ // No-op
+ }
+
+ @Override
+ public boolean onTune(Uri channelUri) {
+ Log.i(TAG, "Tune to " + channelUri);
+ ChannelInfo info = mBackend.getChannelInfo(channelUri);
+ synchronized (mDrawRunnable) {
+ if (info == null || mChannel == null
+ || mChannel.originalNetworkId != info.originalNetworkId) {
+ mCurrentState = null;
+ }
+ mChannel = info;
+ mCurrentVideoTrackId = null;
+ mCurrentAudioTrackId = null;
+ }
+ if (mChannel == null) {
+ Log.i(TAG, "Channel not found for " + channelUri);
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ } else {
+ Log.i(TAG, "Tuning to " + mChannel);
+ }
+ if (TvCommonConstants.HAS_TIME_SHIFT_API) {
+ notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE);
+ mRecordStartTimeMs = mCurrentPositionMs = mLastCurrentPositionUpdateTimeMs
+ = System.currentTimeMillis();
+ mPausedTimeMs = 0;
+ mHandler.sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS);
+ mSpeed = 1;
+ }
+ return true;
+ }
+
+ @Override
+ public void onSetCaptionEnabled(boolean enabled) {
+ // No-op
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ Log.d(TAG, "onKeyDown (keyCode=" + keyCode + ", event=" + event + ")");
+ return true;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ Log.d(TAG, "onKeyUp (keyCode=" + keyCode + ", event=" + event + ")");
+ return true;
+ }
+
+ @Override
+ public long onTimeShiftGetCurrentPosition() {
+ Log.d(TAG, "currentPositionMs=" + mCurrentPositionMs);
+ return mCurrentPositionMs;
+ }
+
+ @Override
+ public long onTimeShiftGetStartPosition() {
+ return mRecordStartTimeMs;
+ }
+
+ @Override
+ public void onTimeShiftPause() {
+ mCurrentPositionMs = mPausedTimeMs = mLastCurrentPositionUpdateTimeMs
+ = System.currentTimeMillis();
+ }
+
+ @Override
+ public void onTimeShiftResume() {
+ mSpeed = 1;
+ mPausedTimeMs = 0;
+ mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
+ }
+
+ @Override
+ public void onTimeShiftSeekTo(long timeMs) {
+ mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
+ mCurrentPositionMs = Math.max(mRecordStartTimeMs,
+ Math.min(timeMs, mLastCurrentPositionUpdateTimeMs));
+ }
+
+ @Override
+ public void onTimeShiftSetPlaybackParams(PlaybackParams params) {
+ mSpeed = params.getSpeed();
+ }
+
+ private final class DrawRunnable implements Runnable {
+ private volatile boolean mIsCanceled = false;
+
+ @Override
+ public void run() {
+ if (mIsCanceled) {
+ return;
+ }
+ if (DEBUG) {
+ Log.v(TAG, "Draw task running");
+ }
+ boolean updatedState = false;
+ ChannelState oldState;
+ ChannelState newState = null;
+ Surface currentSurface;
+ ChannelInfo currentChannel;
+
+ synchronized (this) {
+ oldState = mCurrentState;
+ currentSurface = mSurface;
+ currentChannel = mChannel;
+ if (currentChannel != null) {
+ newState = mBackend.getChannelState(currentChannel.originalNetworkId);
+ if (oldState == null || newState.getVersion() > oldState.getVersion()) {
+ mCurrentState = newState;
+ updatedState = true;
+ }
+ } else {
+ mCurrentState = null;
+ }
+ }
+
+ draw(currentSurface, currentChannel);
+ if (updatedState) {
+ update(oldState, newState, currentChannel);
+ }
+
+ if (!mIsCanceled) {
+ mHandler.postDelayed(this, REFRESH_DELAY_MS);
+ }
+ }
+
+ private void update(ChannelState oldState, ChannelState newState,
+ ChannelInfo currentChannel) {
+ Log.i(TAG, "Updating channel " + currentChannel.number + " state to " + newState);
+ notifyTracksChanged(newState.getTrackInfoList());
+ if (oldState == null || oldState.getTuneStatus() != newState.getTuneStatus()) {
+ if (newState.getTuneStatus() == ChannelState.TUNE_STATUS_VIDEO_AVAILABLE) {
+ notifyVideoAvailable();
+ //TODO handle parental controls.
+ notifyContentAllowed();
+ setAudioTrack(newState.getSelectedAudioTrackId());
+ setVideoTrack(newState.getSelectedVideoTrackId());
+ } else {
+ notifyVideoUnavailable(newState.getTuneStatus());
+ }
+ }
+ }
+
+ private void draw(Surface surface, ChannelInfo currentChannel) {
+ if (surface != null) {
+ String now = TvCommonConstants.HAS_TIME_SHIFT_API
+ ? new Date(mCurrentPositionMs).toString() : new Date().toString();
+ String name = currentChannel == null ? "Null" : currentChannel.name;
+ Canvas c = surface.lockCanvas(null);
+ c.drawColor(0xFF888888);
+ c.drawText(name, 100f, 200f, mTextPaint);
+ c.drawText(now, 100f, 400f, mTextPaint);
+ surface.unlockCanvasAndPost(c);
+ if (DEBUG) {
+ Log.v(TAG, "Post to canvas");
+ }
+ } else {
+ if (DEBUG) {
+ Log.v(TAG, "No surface");
+ }
+ }
+ }
+
+ public void cancel() {
+ mIsCanceled = true;
+ }
+ }
+ }
+}
diff --git a/tests/input/src/com/android/tv/testinput/TestTvInputSetupActivity.java b/tests/input/src/com/android/tv/testinput/TestTvInputSetupActivity.java
new file mode 100644
index 00000000..732972cc
--- /dev/null
+++ b/tests/input/src/com/android/tv/testinput/TestTvInputSetupActivity.java
@@ -0,0 +1,119 @@
+/*
+ * 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.testinput;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.media.tv.TvContract;
+import android.media.tv.TvInputInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.tv.testing.ChannelInfo;
+import com.android.tv.testing.ChannelUtils;
+import com.android.tv.testing.Constants;
+import com.android.tv.testing.ProgramInfo;
+import com.android.tv.testing.ProgramUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The setup activity for {@link TestTvInputService}.
+ */
+public class TestTvInputSetupActivity extends Activity {
+ private static final String TAG = "TestTvInputSetup";
+ private String mInputId;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mInputId = getIntent().getStringExtra(TvInputInfo.EXTRA_INPUT_ID);
+
+ DialogFragment newFragment = new MyAlertDialogFragment();
+ newFragment.show(getFragmentManager(), "dialog");
+ }
+
+ private void registerChannels(int channelCount) {
+ TestTvInputSetupActivity context = this;
+ registerChannels(context, mInputId, false, channelCount);
+ }
+
+ public static void registerChannels(Context context, String inputId, boolean updateBrowsable,
+ int channelCount) {
+ Log.i(TAG, "Registering " + channelCount + " channels");
+ List<ChannelInfo> channels = new ArrayList<>();
+ for (int i = 1; i <= channelCount; i++) {
+ channels.add(ChannelInfo.create(context, i));
+ }
+ ChannelUtils.updateChannels(context, inputId, channels);
+ if (updateBrowsable) {
+ updateChannelsBrowsable(context.getContentResolver(), inputId);
+ }
+
+ // Reload channels so we have the ids.
+ Map<Long, ChannelInfo> channelIdToInfoMap =
+ ChannelUtils.queryChannelInfoMapForTvInput(context, inputId);
+ for (Long channelId : channelIdToInfoMap.keySet()) {
+ // TODO: http://b/21705569 Create better program info for tests
+ ProgramInfo programInfo = ProgramInfo.create();
+ ProgramUtils.populatePrograms(context, TvContract.buildChannelUri(channelId),
+ programInfo);
+ }
+ }
+
+ private static void updateChannelsBrowsable(ContentResolver contentResolver, String inputId) {
+ Uri uri = TvContract.buildChannelsUriForInput(inputId);
+ ContentValues values = new ContentValues();
+ values.put(TvContract.Channels.COLUMN_BROWSABLE, 1);
+ contentResolver.update(uri, values, null, null);
+ }
+
+ public static class MyAlertDialogFragment extends DialogFragment {
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return new AlertDialog.Builder(getActivity()).setTitle(R.string.simple_setup_title)
+ .setMessage(R.string.simple_setup_message)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // TODO: add UI to ask how many channels
+ ((TestTvInputSetupActivity) getActivity())
+ .registerChannels(Constants.UNIT_TEST_CHANNEL_COUNT);
+ // Sets the results so that the application can process the
+ // registered channels properly.
+ getActivity().setResult(Activity.RESULT_OK);
+ getActivity().finish();
+ }
+ }).setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ getActivity().finish();
+ }
+ }).create();
+ }
+ }
+}
diff --git a/tests/input/src/com/android/tv/testinput/instrument/TestSetupInstrumentation.java b/tests/input/src/com/android/tv/testinput/instrument/TestSetupInstrumentation.java
new file mode 100644
index 00000000..379bce86
--- /dev/null
+++ b/tests/input/src/com/android/tv/testinput/instrument/TestSetupInstrumentation.java
@@ -0,0 +1,134 @@
+/*
+ * 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.testinput.instrument;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.testing.Constants;
+import com.android.tv.testinput.TestTvInputService;
+import com.android.tv.testinput.TestTvInputSetupActivity;
+
+/**
+ * An instrumentation utility to set up the needed inputs, channels, programs and other settings
+ * for automated unit tests.
+ *
+ * <p><pre>{@code
+ * adb shell am instrument \
+ * -e testSetupMode {func,jank,unit} \
+ * -w com.android.tv.testinput/.instrument.TestSetupInstrumentation
+ * }</pre>
+ *
+ * <p>Optional arguments are:
+ * <pre>
+ * -e channelCount number
+ * </pre>
+ */
+public class TestSetupInstrumentation extends Instrumentation {
+ private static final String TAG = "TestSetupInstrument";
+ private static final String TEST_SETUP_MODE_ARG = "testSetupMode";
+ private static final String CHANNEL_COUNT_ARG = "channelCount";
+ private Bundle mArguments;
+ private String mInputId;
+
+ /**
+ * Fails an instrumentation request.
+ *
+ * @param errMsg an error message
+ */
+ protected void fail(String errMsg) {
+ Log.e(TAG, errMsg);
+ Bundle result = new Bundle();
+ result.putString("error", errMsg);
+ finish(Activity.RESULT_CANCELED, result);
+ }
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+ mArguments = arguments;
+ start();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ try {
+ mInputId = TestTvInputService.buildInputId(getContext());
+ setup();
+ finish(Activity.RESULT_OK, new Bundle());
+ } catch (TestSetupException e) {
+ fail(e.getMessage());
+ }
+ }
+
+ private void setup() throws TestSetupException {
+ final String testSetupMode = mArguments.getString(TEST_SETUP_MODE_ARG);
+ if (TextUtils.isEmpty(testSetupMode)) {
+ Log.i(TAG, "Performing no setup actions because " + TEST_SETUP_MODE_ARG
+ + " was not passed as an argument");
+ } else {
+ Log.i(TAG, "Running setup for " + testSetupMode + " tests.");
+ int channelCount;
+ switch (testSetupMode) {
+ case "func":
+ channelCount = getArgumentAsInt(CHANNEL_COUNT_ARG,
+ Constants.FUNC_TEST_CHANNEL_COUNT);
+ break;
+ case "jank":
+ channelCount = getArgumentAsInt(CHANNEL_COUNT_ARG,
+ Constants.JANK_TEST_CHANNEL_COUNT);
+ break;
+ case "unit":
+ channelCount = getArgumentAsInt(CHANNEL_COUNT_ARG,
+ Constants.UNIT_TEST_CHANNEL_COUNT);
+ break;
+ default:
+ throw new TestSetupException(
+ "Unknown " + TEST_SETUP_MODE_ARG + " of " + testSetupMode);
+ }
+ TestTvInputSetupActivity.registerChannels(getContext(), mInputId, true, channelCount);
+ }
+ }
+
+ private int getArgumentAsInt(String arg, int defaultValue) {
+ String stringValue = mArguments.getString(arg);
+ if (stringValue != null) {
+ try {
+ return Integer.parseInt(stringValue);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Unable to parse arg " + arg + " with value " + stringValue
+ + " to a integer.", e);
+ }
+ }
+ return defaultValue;
+ }
+
+ static class TestSetupException extends Exception {
+ public TestSetupException(String msg) {
+ super(msg);
+ }
+
+ public static TestSetupException fromMissingArg(String arg) {
+ return new TestSetupException(
+ String.format("Error: missing mandatory argument '%s'", arg));
+ }
+ }
+}
diff --git a/tests/jank/Android.mk b/tests/jank/Android.mk
new file mode 100644
index 00000000..28f8ddf0
--- /dev/null
+++ b/tests/jank/Android.mk
@@ -0,0 +1,22 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := TVJankTests
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-test \
+ tv-test-common \
+ ub-janktesthelper \
+ ub-uiautomator \
+
+LOCAL_INSTRUMENTATION_FOR := TV
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/jank/AndroidManifest.xml b/tests/jank/AndroidManifest.xml
new file mode 100644
index 00000000..fa09917d
--- /dev/null
+++ b/tests/jank/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tv.tests.jank" >
+
+ <uses-sdk android:targetSdkVersion="23" android:minSdkVersion="21" />
+
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:label="Live Channel Jank Tests"
+ android:targetPackage="com.android.tv" />
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+</manifest>
diff --git a/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java b/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java
new file mode 100644
index 00000000..096fb727
--- /dev/null
+++ b/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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.tests.jank;
+
+import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition;
+
+import android.content.res.Resources;
+import android.os.SystemClock;
+import android.support.test.jank.JankTest;
+import android.support.test.jank.JankTestBase;
+import android.support.test.jank.WindowContentFrameStatsMonitor;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+import com.android.tv.R;
+import com.android.tv.common.TvCommonConstants;
+import com.android.tv.testing.uihelper.ByResource;
+import com.android.tv.testing.uihelper.Constants;
+import com.android.tv.testing.uihelper.LiveChannelsUiDeviceHelper;
+import com.android.tv.testing.uihelper.MenuHelper;
+import com.android.tv.testing.uihelper.UiDeviceUtils;
+
+/**
+ * Jank tests for the program guide.
+ */
+@MediumTest
+public class ProgramGuideJankTest extends JankTestBase {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "ProgramGuideJank";
+
+ private static final String STARTING_CHANNEL = "13";
+ private static final int EXPECTED_FRAMES = 5;
+
+ protected UiDevice mDevice;
+
+ protected Resources mTargetResources;
+ protected MenuHelper mMenuHelper;
+ protected LiveChannelsUiDeviceHelper mLiveChannelsHelper;
+
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mTargetResources = getInstrumentation().getTargetContext().getResources();
+ mMenuHelper = new MenuHelper(mDevice, mTargetResources);
+ mLiveChannelsHelper = new LiveChannelsUiDeviceHelper(mDevice, mTargetResources,
+ getInstrumentation().getContext());
+ mLiveChannelsHelper.assertAppStarted();
+ pressKeysForChannelNumber(STARTING_CHANNEL);
+ }
+
+ @JankTest(expectedFrames = 7,
+ beforeTest = "warmProgramGuide",
+ beforeLoop = "selectProgramGuideMenuItem",
+ afterLoop = "clearProgramGuide")
+ @WindowContentFrameStatsMonitor
+ public void testShowProgramGuide() {
+ mDevice.pressDPadCenter();
+
+ // Full show has two animations.
+ long delay = mTargetResources.getInteger(R.integer.program_guide_anim_duration) * 2;
+ waitForIdleAtLeast(delay);
+ }
+
+ @JankTest(expectedFrames = EXPECTED_FRAMES,
+ beforeLoop = "showProgramGuide")
+ @WindowContentFrameStatsMonitor
+ public void testClearProgramGuide() {
+ mDevice.pressBack();
+ // Full show has two animations.
+ waitForIdleAtLeast(mTargetResources.getInteger(R.integer.program_guide_anim_duration) * 2);
+ }
+
+ @JankTest(expectedFrames = EXPECTED_FRAMES,
+ beforeLoop = "showProgramGuide",
+ afterLoop = "clearProgramGuide")
+ @WindowContentFrameStatsMonitor
+ public void testScrollDown() {
+ mDevice.pressDPadDown();
+ waitForIdleAtLeast(mTargetResources
+ .getInteger(R.integer.program_guide_table_detail_toggle_anim_duration));
+ }
+
+ @JankTest(expectedFrames = EXPECTED_FRAMES,
+ beforeLoop = "showProgramGuide",
+ afterLoop = "clearProgramGuide")
+ @WindowContentFrameStatsMonitor
+ public void testScrollRight() {
+ mDevice.pressDPadRight();
+ waitForIdleAtLeast(mTargetResources
+ .getInteger(R.integer.program_guide_table_detail_toggle_anim_duration));
+ }
+
+ /**
+ * {@link UiDevice#waitForIdle() Wait for idle} , then sleep if needed, then wait for idle
+ * again.
+ *
+ * @param delayInMillis The minimum amount of time to delay. This is usually the expected
+ * duration of the animation.
+ */
+ private void waitForIdleAtLeast(long delayInMillis) {
+
+ // This seems to give the most reliable numbers.
+ // The first wait until idle usually returned in 1ms.
+ // Sometimes it would take the whole duration. If we sleep after that we get bad fps
+ // because nothing is happening after the idle ends.
+ //
+ // So sleeping only for the remaining about ensure there is at least enough time for the
+ // animation to complete. If we sleep then wait for idle again. This will usually allow
+ // the animation to complete.
+
+ long startTime = SystemClock.uptimeMillis();
+ mDevice.waitForIdle();
+
+ long idle = SystemClock.uptimeMillis() - startTime;
+ if (DEBUG) {
+ Log.d(TAG, "Waited for idle " + (idle) / 1000.0 + " sec");
+ }
+ if (idle < delayInMillis) {
+ long more = delayInMillis - idle;
+ SystemClock.sleep(more);
+ Log.d(TAG, "Slept " + (more) / 1000.0 + " sec");
+ mDevice.waitForIdle();
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Total wait " + (SystemClock.uptimeMillis() - startTime) / 1000.0 + " sec");
+ }
+ }
+
+ //TODO: move to a mixin/helper
+ protected void pressKeysForChannelNumber(String channel) {
+ UiDeviceUtils.pressKeys(mDevice, channel);
+ mDevice.pressDPadCenter();
+ }
+
+ public void selectProgramGuideMenuItem() {
+ mMenuHelper.showMenu();
+ int rowTitleResId = TvCommonConstants.IS_MNC_OR_HIGHER ? R.string.menu_title_channels
+ : R.string.menu_title_channels_legacy;
+ mMenuHelper.assertNavigateToMenuItem(rowTitleResId, R.string.channels_item_program_guide);
+ mDevice.waitForIdle();
+ }
+
+ public void warmProgramGuide() {
+ // TODO: b/21078199 First time Program Guide is opened there is a noticeable delay
+ selectProgramGuideMenuItem();
+ mDevice.pressDPadCenter();
+ assertWaitForCondition(mDevice, Until.hasObject(Constants.PROGRAM_GUIDE));
+ mDevice.pressBack();
+
+ }
+
+ public void clearProgramGuide() {
+ mDevice.pressBack();
+ assertWaitForCondition(mDevice, Until.gone(Constants.PROGRAM_GUIDE));
+ }
+
+ public void showProgramGuide() {
+ selectProgramGuideMenuItem();
+ mDevice.pressDPadCenter();
+ assertWaitForCondition(mDevice, Until.hasObject(Constants.PROGRAM_GUIDE));
+ // If the side panel grid is visible (and thus has focus), move right to clear it.
+ if (mDevice.hasObject(
+ ByResource.id(mTargetResources, R.id.program_guide_side_panel_grid_view))) {
+ mDevice.pressDPadRight();
+ }
+ }
+}
diff --git a/tests/unit/Android.mk b/tests/unit/Android.mk
new file mode 100644
index 00000000..1cd4f153
--- /dev/null
+++ b/tests/unit/Android.mk
@@ -0,0 +1,21 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-test \
+ mockito-target \
+ tv-test-common \
+
+LOCAL_PACKAGE_NAME := TVUnitTests
+
+LOCAL_INSTRUMENTATION_FOR := TV
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
new file mode 100644
index 00000000..aebb3772
--- /dev/null
+++ b/tests/unit/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tv.tests" >
+
+ <uses-sdk android:targetSdkVersion="23" android:minSdkVersion="21" />
+
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:label="Live Channel Unit Tests"
+ android:targetPackage="com.android.tv" />
+
+ <application android:label="TVTest" >
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+</manifest>
diff --git a/tests/unit/src/com/android/tv/BaseMainActivityTestCase.java b/tests/unit/src/com/android/tv/BaseMainActivityTestCase.java
new file mode 100644
index 00000000..99d5ea3e
--- /dev/null
+++ b/tests/unit/src/com/android/tv/BaseMainActivityTestCase.java
@@ -0,0 +1,135 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.test.ActivityInstrumentationTestCase2;
+
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.testing.ChannelInfo;
+import com.android.tv.testing.testinput.ChannelStateData;
+import com.android.tv.testing.testinput.TestInputControlConnection;
+import com.android.tv.testing.testinput.TestInputControlUtils;
+
+import java.util.List;
+
+/**
+ * Base TestCase for tests that need a {@link MainActivity}.
+ */
+public abstract class BaseMainActivityTestCase
+ extends ActivityInstrumentationTestCase2<MainActivity> {
+ private static final String TAG = "BaseMainActivityTest";
+ private static final int CHANNEL_LOADING_CHECK_INTERVAL_MS = 10;
+
+ protected final TestInputControlConnection mConnection = new TestInputControlConnection();
+
+ protected MainActivity mActivity;
+
+ public BaseMainActivityTestCase(Class<MainActivity> activityClass) {
+ super(activityClass);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ // TODO: ensure the SampleInputs are setup.
+ setActivityInitialTouchMode(false);
+ mActivity = getActivity();
+ getInstrumentation().getContext()
+ .bindService(TestInputControlUtils.createIntent(), mConnection,
+ Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (mConnection.isBound()) {
+ getInstrumentation().getContext().unbindService(mConnection);
+ }
+ super.tearDown();
+ }
+
+ /**
+ * Tune to {@code channel}.
+ *
+ * @param channel the channel to tune to.
+ */
+ protected void tuneToChannel(final Channel channel) {
+ // Run on UI thread so views can be modified
+ try {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.tuneToChannel(channel);
+ }
+ });
+ } catch (Throwable throwable) {
+ throw new RuntimeException(throwable);
+ }
+ }
+
+ /**
+ * Sleep until @{@link ChannelDataManager#isDbLoadFinished()} is true.
+ */
+ protected void waitUntilChannelLoadingFinish() {
+ ChannelDataManager channelDataManager = mActivity.getChannelDataManager();
+ while (!channelDataManager.isDbLoadFinished()) {
+ getInstrumentation().waitForIdleSync();
+ SystemClock.sleep(CHANNEL_LOADING_CHECK_INTERVAL_MS);
+ }
+ }
+
+ /**
+ * Tune to the channel with {@code name}.
+ *
+ * @param name the name of the channel to find.
+ */
+ protected void tuneToChannel(String name) {
+ Channel c = findChannelWithName(name);
+ tuneToChannel(c);
+ }
+
+ /**
+ * Tune to channel.
+ */
+ protected void tuneToChannel(ChannelInfo channel) {
+ tuneToChannel(channel.name);
+ }
+
+ /**
+ * Update the channel state to {@code data} then tune to that channel.
+ *
+ * @param data the state to update the channel with.
+ * @param channel the channel to tune to
+ */
+ protected void updateThenTune(ChannelStateData data, ChannelInfo channel) {
+ mConnection.updateChannelState(channel, data);
+ tuneToChannel(channel);
+ }
+
+ private Channel findChannelWithName(String displayName) {
+ waitUntilChannelLoadingFinish();
+ List<Channel> channelList = mActivity.getChannelDataManager().getChannelList();
+ for (Channel c : channelList) {
+ if (c.getDisplayName().equals(displayName)) {
+ return c;
+ }
+ }
+ throw new AssertionError("'" + displayName + "' channel not found");
+ }
+
+}
diff --git a/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java b/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java
new file mode 100644
index 00000000..6e01f423
--- /dev/null
+++ b/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.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;
+
+import static com.android.tv.TimeShiftManager.INVALID_TIME;
+import static com.android.tv.TimeShiftManager.REQUEST_TIMEOUT_MS;
+
+import android.test.UiThreadTest;
+import android.test.suitebuilder.annotation.MediumTest;
+
+@MediumTest
+public class CurrentPositionMediatorTest extends BaseMainActivityTestCase {
+ private TimeShiftManager.CurrentPositionMediator mMediator;
+
+ public CurrentPositionMediatorTest() {
+ super(MainActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mMediator = mActivity.getTimeShiftManager().mCurrentPositionMediator;
+ }
+
+ @UiThreadTest
+ public void testInitialize() throws Throwable {
+ long currentTimeMs = System.currentTimeMillis();
+ mMediator.initialize(currentTimeMs);
+ assertCurrentPositionMediator(INVALID_TIME, currentTimeMs);
+ }
+
+ @UiThreadTest
+ public void testOnSeekRequested() throws Throwable {
+ long seekToTimeMs = System.currentTimeMillis() - REQUEST_TIMEOUT_MS * 3;
+ mMediator.onSeekRequested(seekToTimeMs);
+ assertNotSame("Seek request time", INVALID_TIME, mMediator.mSeekRequestTimeMs);
+ assertEquals("Current position", seekToTimeMs, mMediator.mCurrentPositionMs);
+ }
+
+ @UiThreadTest
+ public void testOnCurrentPositionChangedInvalidInput() throws Throwable {
+ long seekToTimeMs = System.currentTimeMillis() - REQUEST_TIMEOUT_MS * 3;
+ long newCurrentTimeMs = seekToTimeMs + REQUEST_TIMEOUT_MS;
+ mMediator.onSeekRequested(seekToTimeMs);
+ mMediator.onCurrentPositionChanged(newCurrentTimeMs);
+ assertNotSame("Seek request time", INVALID_TIME, mMediator.mSeekRequestTimeMs);
+ assertNotSame("Current position", seekToTimeMs, mMediator.mCurrentPositionMs);
+ assertNotSame("Current position", newCurrentTimeMs, mMediator.mCurrentPositionMs);
+ }
+
+ @UiThreadTest
+ public void testOnCurrentPositionChangedValidInput() throws Throwable {
+ long seekToTimeMs = System.currentTimeMillis() - REQUEST_TIMEOUT_MS * 3;
+ long newCurrentTimeMs = seekToTimeMs + REQUEST_TIMEOUT_MS - 1;
+ mMediator.onSeekRequested(seekToTimeMs);
+ mMediator.onCurrentPositionChanged(newCurrentTimeMs);
+ assertCurrentPositionMediator(INVALID_TIME, newCurrentTimeMs);
+ }
+
+ private void assertCurrentPositionMediator(long expectedSeekRequestTimeMs,
+ long expectedCurrentPositionMs) {
+ assertEquals("Seek request time", expectedSeekRequestTimeMs, mMediator.mSeekRequestTimeMs);
+ assertEquals("Current position", expectedCurrentPositionMs, mMediator.mCurrentPositionMs);
+ }
+}
diff --git a/tests/unit/src/com/android/tv/MainActivityTest.java b/tests/unit/src/com/android/tv/MainActivityTest.java
new file mode 100644
index 00000000..ae789895
--- /dev/null
+++ b/tests/unit/src/com/android/tv/MainActivityTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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;
+
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.tv.data.Channel;
+import com.android.tv.testing.testinput.TvTestInputConstants;
+import com.android.tv.ui.ChannelBannerView;
+
+import java.util.List;
+
+/**
+ * Tests for {@link MainActivity}.
+ */
+@MediumTest
+public class MainActivityTest extends BaseMainActivityTestCase {
+
+ public MainActivityTest() {
+ super(MainActivity.class);
+ }
+
+ public void testInitialConditions() {
+ waitUntilChannelLoadingFinish();
+ List<Channel> channelList = mActivity.getChannelDataManager().getChannelList();
+ assertTrue("Expected at least one channel", channelList.size() > 0);
+ assertFalse("PIP disabled", mActivity.isPipEnabled());
+ }
+
+ public void testTuneToChannel() throws Throwable {
+ tuneToChannel(TvTestInputConstants.CH_2);
+ assertChannelBannerShown(true);
+ assertChannelName(TvTestInputConstants.CH_2.name);
+ }
+
+ public void testShowProgramGuide() throws Throwable {
+ tuneToChannel(TvTestInputConstants.CH_2);
+ showProgramGuide();
+ assertChannelBannerShown(false);
+ assertProgramGuide(true);
+ }
+
+ private void showProgramGuide() throws Throwable {
+ // Run on UI thread so views can be modified
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.getOverlayManager().showProgramGuide();
+ }
+ });
+ }
+
+ private void assertChannelName(String displayName) {
+ TextView channelNameView = (TextView) mActivity.findViewById(R.id.channel_name);
+ assertEquals("Channel Name", displayName, channelNameView.getText());
+ }
+
+ private View assertProgramGuide(boolean isShown) {
+ return assertViewIsShown("Program Guide", R.id.program_guide, isShown);
+ }
+
+ private ChannelBannerView assertChannelBannerShown(boolean isShown) {
+ View v = assertExpectedBannerSceneClassShown(ChannelBannerView.class, isShown);
+ return (ChannelBannerView) v;
+ }
+
+ private View assertExpectedBannerSceneClassShown(Class<ChannelBannerView> expectedClass,
+ boolean expectedShown) throws AssertionError {
+ View v = assertViewIsShown(expectedClass.getSimpleName(), R.id.scene_transition_common,
+ expectedShown);
+ if (v != null) {
+ assertEquals(expectedClass, v.getClass());
+ }
+ return v;
+ }
+
+ private View assertViewIsShown(String viewName, int viewId, boolean expected)
+ throws AssertionError {
+ View view = mActivity.findViewById(viewId);
+ if (view == null) {
+ if (expected) {
+ throw new AssertionError("View " + viewName + " not found");
+ } else {
+ return null;
+ }
+ }
+ assertEquals(viewName + " shown", expected, view.isShown());
+ return view;
+ }
+
+}
diff --git a/tests/unit/src/com/android/tv/TimeShiftManagerTest.java b/tests/unit/src/com/android/tv/TimeShiftManagerTest.java
new file mode 100644
index 00000000..878d4293
--- /dev/null
+++ b/tests/unit/src/com/android/tv/TimeShiftManagerTest.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;
+
+import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD;
+import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT;
+import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS;
+import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_PAUSE;
+import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY;
+import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND;
+
+import android.test.suitebuilder.annotation.MediumTest;
+
+@MediumTest
+public class TimeShiftManagerTest extends BaseMainActivityTestCase {
+ private TimeShiftManager mTimeShiftManager;
+
+ public TimeShiftManagerTest() {
+ super(MainActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mTimeShiftManager = mActivity.getTimeShiftManager();
+ }
+
+ public void testDisableActions() {
+ enableAllActions(true);
+ assertActionState(true, true, true, true, true, true);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_PLAY, false);
+ assertActionState(false, true, true, true, true, true);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_PAUSE, false);
+ assertActionState(false, false, true, true, true, true);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_REWIND, false);
+ assertActionState(false, false, false, true, true, true);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, false);
+ assertActionState(false, false, false, false, true, true);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, false);
+ assertActionState(false, false, false, false, false, true);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT, false);
+ assertActionState(false, false, false, false, false, false);
+ }
+
+ public void testEnableActions() {
+ enableAllActions(false);
+ assertActionState(false, false, false, false, false, false);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_PLAY, true);
+ assertActionState(true, false, false, false, false, false);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_PAUSE, true);
+ assertActionState(true, true, false, false, false, false);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_REWIND, true);
+ assertActionState(true, true, true, false, false, false);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, true);
+ assertActionState(true, true, true, true, false, false);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, true);
+ assertActionState(true, true, true, true, true, false);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT, true);
+ assertActionState(true, true, true, true, true, true);
+ }
+
+ private void enableAllActions(boolean enabled) {
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_PLAY, enabled);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_PAUSE, enabled);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_REWIND, enabled);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, enabled);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, enabled);
+ mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT, enabled);
+ }
+
+ private void assertActionState(boolean playEnabled, boolean pauseEnabled, boolean rewindEnabled,
+ boolean fastForwardEnabled, boolean jumpToPreviousEnabled, boolean jumpToNextEnabled) {
+ assertEquals("Play Action", playEnabled,
+ mTimeShiftManager.isActionEnabled(TIME_SHIFT_ACTION_ID_PLAY));
+ assertEquals("Pause Action", pauseEnabled,
+ mTimeShiftManager.isActionEnabled(TIME_SHIFT_ACTION_ID_PAUSE));
+ assertEquals("Rewind Action", rewindEnabled,
+ mTimeShiftManager.isActionEnabled(TIME_SHIFT_ACTION_ID_REWIND));
+ assertEquals("Fast Forward Action", fastForwardEnabled,
+ mTimeShiftManager.isActionEnabled(TIME_SHIFT_ACTION_ID_FAST_FORWARD));
+ assertEquals("Jump To Previous Action", jumpToPreviousEnabled,
+ mTimeShiftManager.isActionEnabled(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS));
+ assertEquals("Jump To Next Action", jumpToNextEnabled,
+ mTimeShiftManager.isActionEnabled(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT));
+ }
+}
diff --git a/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java b/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java
new file mode 100644
index 00000000..38ccdfb6
--- /dev/null
+++ b/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java
@@ -0,0 +1,646 @@
+/*
+ * 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.data;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.media.tv.TvContract.Channels;
+import android.net.Uri;
+import android.os.HandlerThread;
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockCursor;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.tv.testing.ChannelInfo;
+import com.android.tv.testing.Constants;
+import com.android.tv.util.TvInputManagerHelper;
+
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test for {@link com.android.tv.data.ChannelDataManager}
+ *
+ * A test method may include tests for multiple methods to minimize the DB access.
+ */
+@SmallTest
+public class ChannelDataManagerTest extends AndroidTestCase {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "ChannelDataManagerTest";
+
+ // Wait time for expected success.
+ private static final long WAIT_TIME_OUT_MS = 1000L;
+ private static final String DUMMY_INPUT_ID = "dummy";
+ // TODO: Use Channels.COLUMN_BROWSABLE and Channels.COLUMN_LOCKED instead.
+ private static final String COLUMN_BROWSABLE = "browsable";
+ private static final String COLUMN_LOCKED = "locked";
+
+ private ChannelDataManager mChannelDataManager;
+ private HandlerThread mHandlerThread;
+ private TestChannelDataManagerListener mListener;
+ private FakeContentResolver mContentResolver;
+ private FakeContentProvider mContentProvider;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ assertTrue("More than 2 channels to test", Constants.UNIT_TEST_CHANNEL_COUNT > 2);
+ TvInputManagerHelper mockHelper = Mockito.mock(TvInputManagerHelper.class);
+ Mockito.when(mockHelper.hasTvInputInfo(Matchers.anyString())).thenReturn(true);
+
+ mContentProvider = new FakeContentProvider(getContext());
+ mContentResolver = new FakeContentResolver();
+ mContentResolver.addProvider(TvContract.AUTHORITY, mContentProvider);
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+ mChannelDataManager = new ChannelDataManager(
+ getContext(), mockHelper, mContentResolver, mHandlerThread.getLooper());
+ mListener = new TestChannelDataManagerListener();
+ mChannelDataManager.addListener(mListener);
+
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ mHandlerThread.quitSafely();
+ mChannelDataManager.stop();
+ }
+
+ private void startAndWaitForComplete() throws Exception {
+ mChannelDataManager.start();
+ try {
+ assertTrue(mListener.loadFinishedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ throw e;
+ }
+ }
+
+ private void restart() throws Exception {
+ mChannelDataManager.stop();
+ mListener.reset();
+ startAndWaitForComplete();
+ }
+
+ public void testIsDbLoadFinished() throws Exception {
+ startAndWaitForComplete();
+ assertTrue(mChannelDataManager.isDbLoadFinished());
+ }
+
+ /**
+ * Test for following methods
+ * - {@link ChannelDataManager#getChannelCount}
+ * - {@link ChannelDataManager#getChannelList}
+ * - {@link ChannelDataManager#getChannel}
+ */
+ public void testGetChannels() throws Exception {
+ startAndWaitForComplete();
+
+ // Test {@link ChannelDataManager#getChannelCount}
+ assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT, mChannelDataManager.getChannelCount());
+
+ // Test {@link ChannelDataManager#getChannelList}
+ List<ChannelInfo> channelInfoList = new ArrayList<>();
+ for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) {
+ channelInfoList.add(ChannelInfo.create(getContext(), i));
+ }
+ List<Channel> channelList = mChannelDataManager.getChannelList();
+ for (Channel channel : channelList) {
+ boolean found = false;
+ for (ChannelInfo channelInfo : channelInfoList) {
+ if (TextUtils.equals(channelInfo.name, channel.getDisplayName())
+ && TextUtils.equals(channelInfo.name, channel.getDisplayName())) {
+ found = true;
+ channelInfoList.remove(channelInfo);
+ break;
+ }
+ }
+ assertTrue("Cannot find (" + channel + ")", found);
+ }
+
+ // Test {@link ChannelDataManager#getChannelIndex()}
+ for (Channel channel : channelList) {
+ assertEquals(channel, mChannelDataManager.getChannel(channel.getId()));
+ }
+ }
+
+ /**
+ * Test for {@link ChannelDataManager#getChannelCount} when no channel is available.
+ */
+ public void testGetChannels_noChannels() throws Exception {
+ mContentProvider.clear();
+ startAndWaitForComplete();
+ assertEquals(0, mChannelDataManager.getChannelCount());
+ }
+
+ /**
+ * Test for following methods and channel listener with notifying change.
+ * - {@link ChannelDataManager#updateBrowsable}
+ * - {@link ChannelDataManager#applyUpdatedValuesToDb}
+ */
+ public void testBrowsable() throws Exception {
+ startAndWaitForComplete();
+
+ // Test if all channels are browable
+ List<Channel> channelList = new ArrayList<>(mChannelDataManager.getChannelList());
+ List<Channel> browsableChannelList = mChannelDataManager.getBrowsableChannelList();
+ for (Channel browsableChannel : browsableChannelList) {
+ boolean found = channelList.remove(browsableChannel);
+ assertTrue("Cannot find (" + browsableChannel + ")", found);
+ }
+ assertEquals(0, channelList.size());
+
+ // Prepare for next tests.
+ TestChannelDataManagerChannelListener channelListener =
+ new TestChannelDataManagerChannelListener();
+ Channel channel1 = mChannelDataManager.getChannelList().get(0);
+ mChannelDataManager.addChannelListener(channel1.getId(), channelListener);
+
+ // Test {@link ChannelDataManager#updateBrowsable} & notification.
+ mChannelDataManager.updateBrowsable(channel1.getId(), false, false);
+ assertTrue(mListener.channelBrowsableChangedCalled);
+ assertFalse(mChannelDataManager.getBrowsableChannelList().contains(channel1));
+ MoreAsserts.assertContentsInAnyOrder(channelListener.updatedChannels, channel1);
+ channelListener.reset();
+
+ // Test {@link ChannelDataManager#applyUpdatedValuesToDb}
+ mChannelDataManager.applyUpdatedValuesToDb();
+ restart();
+ browsableChannelList = mChannelDataManager.getBrowsableChannelList();
+ assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT - 1, browsableChannelList.size());
+ assertFalse(browsableChannelList.contains(channel1));
+ }
+
+ /**
+ * Test for following methods and channel listener without notifying change.
+ * - {@link ChannelDataManager#updateBrowsable}
+ * - {@link ChannelDataManager#applyUpdatedValuesToDb}
+ */
+ public void testBrowsable_skipNotification() throws Exception {
+ startAndWaitForComplete();
+
+ // Prepare for next tests.
+ TestChannelDataManagerChannelListener channelListener =
+ new TestChannelDataManagerChannelListener();
+ Channel channel1 = mChannelDataManager.getChannelList().get(0);
+ Channel channel2 = mChannelDataManager.getChannelList().get(1);
+ mChannelDataManager.addChannelListener(channel1.getId(), channelListener);
+ mChannelDataManager.addChannelListener(channel2.getId(), channelListener);
+
+ // Test {@link ChannelDataManager#updateBrowsable} & skip notification.
+ mChannelDataManager.updateBrowsable(channel1.getId(), false, true);
+ mChannelDataManager.updateBrowsable(channel2.getId(), false, true);
+ mChannelDataManager.updateBrowsable(channel1.getId(), true, true);
+ assertFalse(mListener.channelBrowsableChangedCalled);
+ List<Channel> browsableChannelList = mChannelDataManager.getBrowsableChannelList();
+ assertTrue(browsableChannelList.contains(channel1));
+ assertFalse(browsableChannelList.contains(channel2));
+
+ // Test {@link ChannelDataManager#applyUpdatedValuesToDb}
+ mChannelDataManager.applyUpdatedValuesToDb();
+ restart();
+ browsableChannelList = mChannelDataManager.getBrowsableChannelList();
+ assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT - 1, browsableChannelList.size());
+ assertFalse(browsableChannelList.contains(channel2));
+ }
+
+ /**
+ * Test for following methods and channel listener.
+ * - {@link ChannelDataManager#updateLocked}
+ * - {@link ChannelDataManager#applyUpdatedValuesToDb}
+ */
+ public void testLocked() throws Exception {
+ startAndWaitForComplete();
+
+ // Test if all channels aren't locked at the first time.
+ List<Channel> channelList = mChannelDataManager.getChannelList();
+ for (Channel channel : channelList) {
+ assertFalse(channel + " is locked", channel.isLocked());
+ }
+
+ // Prepare for next tests.
+ Channel channel = mChannelDataManager.getChannelList().get(0);
+
+ // Test {@link ChannelDataManager#updateLocked}
+ mChannelDataManager.updateLocked(channel.getId(), true);
+ assertTrue(mChannelDataManager.getChannel(channel.getId()).isLocked());
+
+ // Test {@link ChannelDataManager#applyUpdatedValuesToDb}.
+ mChannelDataManager.applyUpdatedValuesToDb();
+ restart();
+ assertTrue(mChannelDataManager.getChannel(channel.getId()).isLocked());
+
+ // Cleanup
+ mChannelDataManager.updateLocked(channel.getId(), false);
+ }
+
+ /**
+ * Test ChannelDataManager when channels in TvContract are updated, removed, or added.
+ */
+ public void testChannelListChanged() throws Exception {
+ startAndWaitForComplete();
+
+ // Test channel add.
+ mListener.reset();
+ long testChannelId = Constants.UNIT_TEST_CHANNEL_COUNT + 1;
+ ChannelInfo testChannelInfo = ChannelInfo.create(getContext(), (int) testChannelId);
+ testChannelId = Constants.UNIT_TEST_CHANNEL_COUNT + 1;
+ mContentProvider.simulateInsert(testChannelInfo);
+ assertTrue(mListener.channeListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT + 1, mChannelDataManager.getChannelCount());
+
+ // Test channel update
+ mListener.reset();
+ TestChannelDataManagerChannelListener channelListener =
+ new TestChannelDataManagerChannelListener();
+ mChannelDataManager.addChannelListener(testChannelId, channelListener);
+ String newName = testChannelInfo.name + "_test";
+ mContentProvider.simulateUpdate(testChannelId, newName);
+ assertTrue(mListener.channeListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(channelListener.channelChangedLatch.await(
+ WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(0, channelListener.removedChannels.size());
+ assertEquals(1, channelListener.updatedChannels.size());
+ Channel updatedChannel = channelListener.updatedChannels.get(0);
+ assertEquals(testChannelId, updatedChannel.getId());
+ assertEquals(testChannelInfo.number, updatedChannel.getDisplayNumber());
+ assertEquals(newName, updatedChannel.getDisplayName());
+ assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT + 1,
+ mChannelDataManager.getChannelCount());
+
+ // Test channel remove.
+ mListener.reset();
+ channelListener.reset();
+ mContentProvider.simulateDelete(testChannelId);
+ assertTrue(mListener.channeListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(channelListener.channelChangedLatch.await(
+ WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(1, channelListener.removedChannels.size());
+ assertEquals(0, channelListener.updatedChannels.size());
+ Channel removedChannel = channelListener.removedChannels.get(0);
+ assertEquals(newName, removedChannel.getDisplayName());
+ assertEquals(testChannelInfo.number, removedChannel.getDisplayNumber());
+ assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT, mChannelDataManager.getChannelCount());
+ }
+
+ private class ChannelInfoWrapper {
+ public ChannelInfo channelInfo;
+ public boolean browsable;
+ public boolean locked;
+ public ChannelInfoWrapper(ChannelInfo channelInfo) {
+ this.channelInfo = channelInfo;
+ browsable = true;
+ locked = false;
+ }
+ }
+
+ private class FakeContentResolver extends MockContentResolver {
+ @Override
+ public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
+ super.notifyChange(uri, observer, syncToNetwork);
+ if (DEBUG) {
+ Log.d(TAG, "onChanged(uri=" + uri + ", observer=" + observer + ")");
+ }
+ // Do not call {@link ContentObserver#onChange} directly
+ // to run it on the {@link #mHandlerThread}.
+ if (observer != null) {
+ observer.dispatchChange(false, uri);
+ } else {
+ mChannelDataManager.getContentObserver().dispatchChange(false, uri);
+ }
+ }
+ }
+
+ // This implements the minimal methods in content resolver
+ // and detailed assumptions are written in each method.
+ private class FakeContentProvider extends MockContentProvider {
+ private SparseArray<ChannelInfoWrapper> mChannelInfoList = new SparseArray<>();
+
+ public FakeContentProvider(Context context) {
+ super(context);
+ for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) {
+ mChannelInfoList.put(i,
+ new ChannelInfoWrapper(ChannelInfo.create(getContext(), i)));
+ }
+ }
+
+ /**
+ * Implementation of {@link ContentProvider#query}.
+ * This assumes that {@link ChannelDataManager} queries channels
+ * with empty {@code selection}. (i.e. channels are always queries for all)
+ */
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[]
+ selectionArgs, String sortOrder) {
+ if (DEBUG) {
+ Log.d(TAG, "dump query");
+ Log.d(TAG, " uri=" + uri);
+ if (projection == null || projection.length == 0) {
+ Log.d(TAG, " projection=" + projection);
+ } else {
+ for (int i = 0; i < projection.length; i++) {
+ Log.d(TAG, " projection=" + projection[i]);
+ }
+ }
+ Log.d(TAG," selection=" + selection);
+ }
+ assertChannelUri(uri);
+ return new FakeCursor(projection);
+ }
+
+ /**
+ * Implementation of {@link ContentProvider#update}.
+ * This assumes that {@link ChannelDataManager} update channels
+ * only for changing browsable and locked.
+ */
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ if (DEBUG) Log.d(TAG, "update(uri=" + uri + ", selection=" + selection);
+ assertChannelUri(uri);
+ List<Long> channelIds = new ArrayList<>();
+ try {
+ long channelId = ContentUris.parseId(uri);
+ channelIds.add(channelId);
+ } catch (NumberFormatException e) {
+ // Update for multiple channels.
+ if (TextUtils.isEmpty(selection)) {
+ for (int i = 0; i < mChannelInfoList.size(); i++) {
+ channelIds.add((long) mChannelInfoList.keyAt(i));
+ }
+ } else {
+ // See {@link Utils#buildSelectionForIds} for the syntax.
+ String selectionForId = selection.substring(
+ selection.indexOf("(") + 1, selection.lastIndexOf(")"));
+ String[] ids = selectionForId.split(", ");
+ if (ids != null) {
+ for (String id : ids) {
+ channelIds.add(Long.parseLong(id));
+ }
+ }
+ }
+ }
+ int updateCount = 0;
+ for (long channelId : channelIds) {
+ boolean updated = false;
+ ChannelInfoWrapper channel = mChannelInfoList.get((int) channelId);
+ if (channel == null) {
+ return 0;
+ }
+ if (values.containsKey(COLUMN_BROWSABLE)) {
+ updated = true;
+ channel.browsable = (values.getAsInteger(COLUMN_BROWSABLE) == 1);
+ }
+ if (values.containsKey(COLUMN_LOCKED)) {
+ updated = true;
+ channel.locked = (values.getAsInteger(COLUMN_LOCKED) == 1);
+ }
+ updateCount += updated ? 1 : 0;
+ }
+ if (updateCount > 0) {
+ if (channelIds.size() == 1) {
+ mContentResolver.notifyChange(uri, null);
+ } else {
+ mContentResolver.notifyChange(Channels.CONTENT_URI, null);
+ }
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Update to channel(uri=" + uri + ") is ignored for " + values);
+ }
+ }
+ return updateCount;
+ }
+
+ /**
+ * Simulates channel data insert.
+ * This assigns original network ID (the same with channel number) to channel ID.
+ */
+ public void simulateInsert(ChannelInfo testChannelInfo) {
+ long channelId = testChannelInfo.originalNetworkId;
+ mChannelInfoList.put((int) channelId,
+ new ChannelInfoWrapper(ChannelInfo.create(getContext(), (int) channelId)));
+ mContentResolver.notifyChange(TvContract.buildChannelUri(channelId), null);
+ }
+
+ /**
+ * Simulates channel data delete.
+ */
+ public void simulateDelete(long channelId) {
+ mChannelInfoList.remove((int) channelId);
+ mContentResolver.notifyChange(TvContract.buildChannelUri(channelId), null);
+ }
+
+ /**
+ * Simulates channel data update.
+ */
+ public void simulateUpdate(long channelId, String newName) {
+ ChannelInfoWrapper channel = mChannelInfoList.get((int) channelId);
+ ChannelInfo.Builder builder = new ChannelInfo.Builder(channel.channelInfo);
+ builder.setName(newName);
+ channel.channelInfo = builder.build();
+ mContentResolver.notifyChange(TvContract.buildChannelUri(channelId), null);
+ }
+
+ private void assertChannelUri(Uri uri) {
+ assertTrue("Uri(" + uri + ") isn't channel uri",
+ uri.toString().startsWith(Channels.CONTENT_URI.toString()));
+ }
+
+ public void clear() {
+ mChannelInfoList.clear();
+ }
+
+ public ChannelInfoWrapper get(int position) {
+ return mChannelInfoList.get(mChannelInfoList.keyAt(position));
+ }
+
+ public int getCount() {
+ return mChannelInfoList.size();
+ }
+
+ public long keyAt(int position) {
+ return mChannelInfoList.keyAt(position);
+ }
+ }
+
+ private class FakeCursor extends MockCursor {
+ private String[] ALL_COLUMNS = {
+ Channels._ID,
+ Channels.COLUMN_DISPLAY_NAME,
+ Channels.COLUMN_DISPLAY_NUMBER,
+ Channels.COLUMN_INPUT_ID,
+ Channels.COLUMN_VIDEO_FORMAT,
+ Channels.COLUMN_ORIGINAL_NETWORK_ID,
+ COLUMN_BROWSABLE,
+ COLUMN_LOCKED};
+ private String[] mColumns;
+ private int mPosition;
+
+ public FakeCursor(String[] columns) {
+ mColumns = (columns == null) ? ALL_COLUMNS : columns;
+ mPosition = -1;
+ }
+
+ @Override
+ public String getColumnName(int columnIndex) {
+ return mColumns[columnIndex];
+ }
+
+ @Override
+ public int getColumnIndex(String columnName) {
+ for (int i = 0; i < mColumns.length; i++) {
+ if (mColumns[i].equalsIgnoreCase(columnName)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public long getLong(int columnIndex) {
+ String columnName = getColumnName(columnIndex);
+ switch (columnName) {
+ case Channels._ID:
+ return mContentProvider.keyAt(mPosition);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Column (" + columnName + ") is ignored in getLong()");
+ }
+ return 0;
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ String columnName = getColumnName(columnIndex);
+ ChannelInfoWrapper channel = mContentProvider.get(mPosition);
+ switch (columnName) {
+ case Channels.COLUMN_DISPLAY_NAME:
+ return channel.channelInfo.name;
+ case Channels.COLUMN_DISPLAY_NUMBER:
+ return channel.channelInfo.number;
+ case Channels.COLUMN_INPUT_ID:
+ return DUMMY_INPUT_ID;
+ case Channels.COLUMN_VIDEO_FORMAT:
+ return channel.channelInfo.getVideoFormat();
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Column (" + columnName + ") is ignored in getString()");
+ }
+ return null;
+ }
+
+ @Override
+ public int getInt(int columnIndex) {
+ String columnName = getColumnName(columnIndex);
+ ChannelInfoWrapper channel = mContentProvider.get(mPosition);
+ switch (columnName) {
+ case Channels.COLUMN_ORIGINAL_NETWORK_ID:
+ return channel.channelInfo.originalNetworkId;
+ case COLUMN_BROWSABLE:
+ return channel.browsable ? 1 : 0;
+ case COLUMN_LOCKED:
+ return channel.locked ? 1 : 0;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Column (" + columnName + ") is ignored in getInt()");
+ }
+ return 0;
+ }
+
+ @Override
+ public int getCount() {
+ return mContentProvider.getCount();
+ }
+
+ @Override
+ public boolean moveToNext() {
+ return ++mPosition < mContentProvider.getCount();
+ }
+
+ @Override
+ public void close() {
+ // No-op.
+ }
+ }
+
+ private class TestChannelDataManagerListener implements ChannelDataManager.Listener {
+ public CountDownLatch loadFinishedLatch = new CountDownLatch(1);
+ public CountDownLatch channeListUpdatedLatch = new CountDownLatch(1);
+ public boolean channelBrowsableChangedCalled;
+
+ @Override
+ public void onLoadFinished() {
+ loadFinishedLatch.countDown();
+ }
+
+ @Override
+ public void onChannelListUpdated() {
+ channeListUpdatedLatch.countDown();
+ }
+
+ @Override
+ public void onChannelBrowsableChanged() {
+ channelBrowsableChangedCalled = true;
+ }
+
+ public void reset() {
+ loadFinishedLatch = new CountDownLatch(1);
+ channeListUpdatedLatch = new CountDownLatch(1);
+ channelBrowsableChangedCalled = false;
+ }
+ }
+
+ private class TestChannelDataManagerChannelListener
+ implements ChannelDataManager.ChannelListener {
+ public CountDownLatch channelChangedLatch = new CountDownLatch(1);
+ public List<Channel> removedChannels = new ArrayList<>();
+ public List<Channel> updatedChannels = new ArrayList<>();
+
+ @Override
+ public void onChannelRemoved(Channel channel) {
+ removedChannels.add(channel);
+ channelChangedLatch.countDown();
+ }
+
+ @Override
+ public void onChannelUpdated(Channel channel) {
+ updatedChannels.add(channel);
+ channelChangedLatch.countDown();
+ }
+
+ public void reset() {
+ channelChangedLatch = new CountDownLatch(1);
+ removedChannels.clear();
+ updatedChannels.clear();
+ }
+ }
+}
diff --git a/tests/unit/src/com/android/tv/data/ChannelNumberTest.java b/tests/unit/src/com/android/tv/data/ChannelNumberTest.java
new file mode 100644
index 00000000..9914f75e
--- /dev/null
+++ b/tests/unit/src/com/android/tv/data/ChannelNumberTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.data;
+
+import static com.android.tv.data.ChannelNumber.parseChannelNumber;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.tv.testing.ComparableTester;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link ChannelNumber}.
+ */
+@SmallTest
+public class ChannelNumberTest extends TestCase {
+
+ /**
+ * Test method for {@link ChannelNumber#ChannelNumber()}.
+ */
+ public void testChannelNumber() {
+ assertChannelEquals(new ChannelNumber(), "", false, "");
+ }
+
+ /**
+ * Test method for
+ * {@link com.android.tv.data.ChannelNumber#parseChannelNumber(java.lang.String)}.
+ */
+ public void testParseChannelNumber() {
+ assertNull(parseChannelNumber(""));
+ assertNull(parseChannelNumber(" "));
+ assertChannelEquals(parseChannelNumber("1"), "1", false, "");
+ assertChannelEquals(parseChannelNumber("1234 4321"), "1234", true, "4321");
+ assertChannelEquals(parseChannelNumber("3-4"), "3", true, "4");
+ assertChannelEquals(parseChannelNumber("5.6"), "5", true, "6");
+ }
+
+ /**
+ * Test method for {@link ChannelNumber#compareTo(com.android.tv.data.ChannelNumber)}.
+ */
+ public void testCompareTo() {
+ new ComparableTester<ChannelNumber>()
+ .addEquivelentGroup(parseChannelNumber("1"), parseChannelNumber("1"))
+ .addEquivelentGroup(parseChannelNumber("2"))
+ .addEquivelentGroup(parseChannelNumber("2 1"), parseChannelNumber("2.1"),
+ parseChannelNumber("2-1"))
+ .addEquivelentGroup(parseChannelNumber("2-2"))
+ .addEquivelentGroup(parseChannelNumber("2-10"))
+ .addEquivelentGroup(parseChannelNumber("3"))
+ .addEquivelentGroup(parseChannelNumber("10"))
+ .addEquivelentGroup(parseChannelNumber("100"))
+ .test();
+ }
+
+ /**
+ * Test method for {@link ChannelNumber#compare(java.lang.String, java.lang.String)}.
+ */
+ public void testCompare() {
+ // Only need to test nulls, the reset is tested by testComparteTo
+ assertEquals("compareTo(null,null)", 0, ChannelNumber.compare(null, null));
+ assertEquals("compareTo(1,1)", 0, ChannelNumber.compare("1", "1"));
+ assertEquals("compareTo(null,1)<0", true, ChannelNumber.compare(null, "1") < 0);
+ assertEquals("compareTo(1,null)>0", true, ChannelNumber.compare("1", null) > 0);
+ }
+
+ private void assertChannelEquals(ChannelNumber actual, String expectedMajor,
+ boolean expectedHasDelimiter, String expectedMinor) {
+ assertEquals(actual + " major", actual.majorNumber, expectedMajor);
+ assertEquals(actual + " hasDelimiter", actual.hasDelimiter, expectedHasDelimiter);
+ assertEquals(actual + " minor", actual.minorNumber, expectedMinor);
+ }
+
+}
diff --git a/tests/unit/src/com/android/tv/data/ChannelTest.java b/tests/unit/src/com/android/tv/data/ChannelTest.java
new file mode 100644
index 00000000..dc41fda0
--- /dev/null
+++ b/tests/unit/src/com/android/tv/data/ChannelTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.data;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.test.AndroidTestCase;
+
+import com.android.tv.testing.ComparatorTester;
+import com.android.tv.util.TvInputManagerHelper;
+
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.Comparator;
+
+/**
+ * Tests for {@link Channel}.
+ */
+public class ChannelTest extends AndroidTestCase {
+ // Used for testing TV inputs with invalid input package. This could happen when a TV input is
+ // uninstalled while drawing an app link card.
+ private static final String INVALID_TV_INPUT_PACKAGE_NAME =
+ "com.android.tv.invalid_tv_input";
+ // Used for testing TV inputs defined inside of Live Channels.
+ private static final String LIVE_CHANNELS_PACKAGE_NAME = "com.android.tv";
+ // Used for testing a TV input which doesn't have its leanback launcher activity.
+ private static final String NONE_LEANBACK_TV_INPUT_PACKAGE_NAME =
+ "com.android.tv.none_leanback_tv_input";
+ // Used for testing a TV input which has its leanback launcher activity.
+ private static final String LEANBACK_TV_INPUT_PACKAGE_NAME =
+ "com.android.tv.leanback_tv_input";
+ private static final String TEST_APP_LINK_TEXT = "test_app_link_text";
+ private static final ActivityInfo TEST_ACTIVITY_INFO = new ActivityInfo();
+
+ private Context mMockContext;
+ private Intent mInvalidIntent;
+ private Intent mValidIntent;
+ private Intent mLiveChannelsIntent;
+ private Intent mLeanbackTvInputIntent;
+
+ public void setUp() throws Exception {
+ super.setUp();
+ mInvalidIntent = new Intent(Intent.ACTION_VIEW);
+ mInvalidIntent.setComponent(new ComponentName(INVALID_TV_INPUT_PACKAGE_NAME, ".test"));
+ mValidIntent = new Intent(Intent.ACTION_VIEW);
+ mValidIntent.setComponent(new ComponentName(LEANBACK_TV_INPUT_PACKAGE_NAME, ".test"));
+ mLiveChannelsIntent = new Intent(Intent.ACTION_VIEW);
+ mLiveChannelsIntent.setComponent(
+ new ComponentName(LIVE_CHANNELS_PACKAGE_NAME, ".MainActivity"));
+ mLeanbackTvInputIntent = new Intent(Intent.ACTION_VIEW);
+ mLeanbackTvInputIntent.setComponent(
+ new ComponentName(LEANBACK_TV_INPUT_PACKAGE_NAME, ".test"));
+
+ PackageManager mockPackageManager = Mockito.mock(PackageManager.class);
+ Mockito.when(mockPackageManager.getLeanbackLaunchIntentForPackage(
+ INVALID_TV_INPUT_PACKAGE_NAME)).thenReturn(null);
+ Mockito.when(mockPackageManager.getLeanbackLaunchIntentForPackage(
+ LIVE_CHANNELS_PACKAGE_NAME)).thenReturn(mLiveChannelsIntent);
+ Mockito.when(mockPackageManager.getLeanbackLaunchIntentForPackage(
+ NONE_LEANBACK_TV_INPUT_PACKAGE_NAME)).thenReturn(null);
+ Mockito.when(mockPackageManager.getLeanbackLaunchIntentForPackage(
+ LEANBACK_TV_INPUT_PACKAGE_NAME)).thenReturn(mLeanbackTvInputIntent);
+
+ // Channel.getAppLinkIntent() calls initAppLinkTypeAndIntent() which calls
+ // Intent.resolveActivityInfo() which calls PackageManager.getActivityInfo().
+ Mockito.doAnswer(new Answer<ActivityInfo>() {
+ public ActivityInfo answer(InvocationOnMock invocation) {
+ // We only check the package name, since the class name can be changed
+ // when an intent is changed to an uri and created from the uri.
+ // (ex, ".className" -> "packageName.className")
+ return mValidIntent.getComponent().getPackageName().equals(
+ ((ComponentName)invocation.getArguments()[0]).getPackageName())
+ ? TEST_ACTIVITY_INFO : null;
+ }
+ }).when(mockPackageManager).getActivityInfo(Mockito.<ComponentName>any(), Mockito.anyInt());
+
+ mMockContext = Mockito.mock(Context.class);
+ Mockito.when(mMockContext.getApplicationContext()).thenReturn(mMockContext);
+ Mockito.when(mMockContext.getPackageName()).thenReturn(LIVE_CHANNELS_PACKAGE_NAME);
+ Mockito.when(mMockContext.getPackageManager()).thenReturn(mockPackageManager);
+ }
+
+ public void testGetAppLinkType_NoText_NoIntent() {
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME, null, null);
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME, null, null);
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME, null,
+ null);
+ assertAppLinkType(Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME, null, null);
+ }
+
+ public void testGetAppLinkType_NoText_InvalidIntent() {
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME, null,
+ mInvalidIntent);
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME, null,
+ mInvalidIntent);
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME, null,
+ mInvalidIntent);
+ assertAppLinkType(Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME, null,
+ mInvalidIntent);
+ }
+
+ public void testGetAppLinkType_NoText_ValidIntent() {
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME, null,
+ mValidIntent);
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME, null,
+ mValidIntent);
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME, null,
+ mValidIntent);
+ assertAppLinkType(Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME, null,
+ mValidIntent);
+ }
+
+ public void testGetAppLinkType_HasText_NoIntent() {
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME,
+ TEST_APP_LINK_TEXT, null);
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME,
+ TEST_APP_LINK_TEXT, null);
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME,
+ TEST_APP_LINK_TEXT, null);
+ assertAppLinkType(Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME,
+ TEST_APP_LINK_TEXT, null);
+ }
+
+ public void testGetAppLinkType_HasText_InvalidIntent() {
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME,
+ TEST_APP_LINK_TEXT, mInvalidIntent);
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME,
+ TEST_APP_LINK_TEXT, mInvalidIntent);
+ assertAppLinkType(Channel.APP_LINK_TYPE_NONE, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME,
+ TEST_APP_LINK_TEXT, mInvalidIntent);
+ assertAppLinkType(Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME,
+ TEST_APP_LINK_TEXT, mInvalidIntent);
+ }
+
+ public void testGetAppLinkType_HasText_ValidIntent() {
+ assertAppLinkType(Channel.APP_LINK_TYPE_CHANNEL, INVALID_TV_INPUT_PACKAGE_NAME,
+ TEST_APP_LINK_TEXT, mValidIntent);
+ assertAppLinkType(Channel.APP_LINK_TYPE_CHANNEL, LIVE_CHANNELS_PACKAGE_NAME,
+ TEST_APP_LINK_TEXT, mValidIntent);
+ assertAppLinkType(Channel.APP_LINK_TYPE_CHANNEL, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME,
+ TEST_APP_LINK_TEXT, mValidIntent);
+ assertAppLinkType(Channel.APP_LINK_TYPE_CHANNEL, LEANBACK_TV_INPUT_PACKAGE_NAME,
+ TEST_APP_LINK_TEXT, mValidIntent);
+ }
+
+ private void assertAppLinkType(int expectedType, String inputPackageName, String appLinkText,
+ Intent appLinkIntent) {
+ Channel testChannel = new Channel.Builder()
+ .setPackageName(inputPackageName)
+ .setAppLinkText(appLinkText)
+ .setAppLinkIntentUri(appLinkIntent == null ? null : appLinkIntent.toUri(0))
+ .build();
+ assertEquals("Unexpected app-link type for for " + testChannel,
+ expectedType, testChannel.getAppLinkType(mMockContext));
+ }
+
+ public void testComparator() {
+ final String PARTNER_INPUT_ID = "partner";
+
+ TvInputManagerHelper manager = Mockito.mock(TvInputManagerHelper.class);
+ Mockito.when(manager.isPartnerInput(Matchers.anyString())).thenAnswer(
+ new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ String inputId = (String) invocation.getArguments()[0];
+ return PARTNER_INPUT_ID.equals(inputId);
+ }
+ });
+ Comparator<Channel> comparator = new TestChannelComparator(manager);
+ ComparatorTester<Channel> comparatorTester =
+ ComparatorTester.withoutEqualsTest(comparator);
+ comparatorTester.addComparableGroup(
+ new Channel.Builder().setInputId(PARTNER_INPUT_ID).setDisplayNumber("100").build());
+ comparatorTester.addComparableGroup(
+ new Channel.Builder().setInputId("1").setDisplayNumber("2").build());
+ comparatorTester.addComparableGroup(
+ new Channel.Builder().setInputId("2").setDisplayNumber("1.0").build());
+ comparatorTester.addComparableGroup(
+ new Channel.Builder().setInputId("2").setDisplayNumber("1.62")
+ .setDisplayName("test1").build(),
+ new Channel.Builder().setInputId("2").setDisplayNumber("1.62")
+ .setDisplayName("test2").build(),
+ new Channel.Builder().setInputId("2").setDisplayNumber("1.62")
+ .setDisplayName("test3").build());
+ comparatorTester.addComparableGroup(
+ new Channel.Builder().setInputId("2").setDisplayNumber("2.0").build());
+ comparatorTester.addComparableGroup(
+ new Channel.Builder().setInputId("2").setDisplayNumber("12.2").build());
+ comparatorTester.test();
+ }
+
+ private class TestChannelComparator extends Channel.DefaultComparator {
+ public TestChannelComparator(TvInputManagerHelper manager) {
+ super(null, manager);
+ }
+
+ @Override
+ public String getInputLabelForChannel(Channel channel) {
+ return channel.getInputId();
+ }
+ };
+}
diff --git a/tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java b/tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java
new file mode 100644
index 00000000..31ad54f0
--- /dev/null
+++ b/tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java
@@ -0,0 +1,533 @@
+/*
+ * 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.data;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.HandlerThread;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockCursor;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.tv.testing.Constants;
+import com.android.tv.testing.ProgramInfo;
+import com.android.tv.util.FakeClock;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test for {@link com.android.tv.data.ProgramDataManager}
+ */
+@SmallTest
+public class ProgramDataManagerTest extends AndroidTestCase {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "ProgramDataManagerTest";
+
+ // Wait time for expected success.
+ private static final long WAIT_TIME_OUT_MS = 1000L;
+ // Wait time for expected failure.
+ private static final long FAILURE_TIME_OUT_MS = 300L;
+
+ // TODO: Use TvContract constants, once they become public.
+ private static final String PARAM_CHANNEL = "channel";
+ private static final String PARAM_START_TIME = "start_time";
+ private static final String PARAM_END_TIME = "end_time";
+
+ private ProgramDataManager mProgramDataManager;
+ private FakeClock mClock;
+ private HandlerThread mHandlerThread;
+ private TestProgramDataManagerListener mListener;
+ private FakeContentResolver mContentResolver;
+ private FakeContentProvider mContentProvider;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mClock = new FakeClock();
+ mListener = new TestProgramDataManagerListener();
+ mContentProvider = new FakeContentProvider(getContext());
+ mContentResolver = new FakeContentResolver();
+ mContentResolver.addProvider(TvContract.AUTHORITY, mContentProvider);
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+ mProgramDataManager = new ProgramDataManager(
+ mContentResolver, mClock, mHandlerThread.getLooper());
+ mProgramDataManager.addListener(mListener);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ mHandlerThread.quitSafely();
+ mProgramDataManager.stop();
+ }
+
+ private void startAndWaitForComplete() throws Exception {
+ mProgramDataManager.start();
+ assertTrue(mListener.programUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ private static boolean equals(ProgramInfo lhs, long lhsStarTimeMs, Program rhs) {
+ return TextUtils.equals(lhs.title, rhs.getTitle())
+ && TextUtils.equals(lhs.episode, rhs.getEpisodeTitle())
+ && TextUtils.equals(lhs.description, rhs.getDescription())
+ && lhsStarTimeMs == rhs.getStartTimeUtcMillis()
+ && lhsStarTimeMs + lhs.durationMs == rhs.getEndTimeUtcMillis();
+ }
+
+ /**
+ * Test for {@link ProgramInfo#getIndex} and {@link ProgramInfo#getStartTimeMs}.
+ */
+ public void testProgramUtils() {
+ ProgramInfo stub = ProgramInfo.create();
+ for (long channelId = 1; channelId < Constants.UNIT_TEST_CHANNEL_COUNT; channelId++) {
+ int index = stub.getIndex(mClock.currentTimeMillis(), channelId);
+ long startTimeMs = stub.getStartTimeMs(index, channelId);
+ ProgramInfo programAt = stub.build(getContext(), index);
+ assertTrue(startTimeMs <= mClock.currentTimeMillis());
+ assertTrue(mClock.currentTimeMillis() < startTimeMs + programAt.durationMs);
+ }
+ }
+
+ /**
+ * Test for following methods.
+ *
+ * <p>
+ * {@link ProgramDataManager#getCurrentProgram(long)},
+ * {@link ProgramDataManager#getPrograms(long, long)},
+ * {@link ProgramDataManager#setPrefetchTimeRange(long)}.
+ * </p>
+ */
+ public void testGetPrograms() throws Exception {
+ // Initial setup to test {@link ProgramDataManager#setPrefetchTimeRange(long)}.
+ long preventSnapDelayMs = ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS * 2;
+ long prefetchTimeRangeStartMs = System.currentTimeMillis() + preventSnapDelayMs;
+ mClock.setCurrentTimeMillis(prefetchTimeRangeStartMs + preventSnapDelayMs);
+ mProgramDataManager.setPrefetchTimeRange(prefetchTimeRangeStartMs);
+
+ startAndWaitForComplete();
+
+ for (long channelId = 1; channelId <= Constants.UNIT_TEST_CHANNEL_COUNT; channelId++) {
+ Program currentProgram = mProgramDataManager.getCurrentProgram(channelId);
+ // Test {@link ProgramDataManager#getCurrentProgram(long)}.
+ assertTrue(currentProgram.getStartTimeUtcMillis() <= mClock.currentTimeMillis()
+ && mClock.currentTimeMillis() <= currentProgram.getEndTimeUtcMillis());
+
+ // Test {@link ProgramDataManager#getPrograms(long)}.
+ // Case #1: Normal case
+ List<Program> programs =
+ mProgramDataManager.getPrograms(channelId, mClock.currentTimeMillis());
+ ProgramInfo stub = ProgramInfo.create();
+ int index = stub.getIndex(mClock.currentTimeMillis(), channelId);
+ for (Program program : programs) {
+ ProgramInfo programInfoAt = stub.build(getContext(), index);
+ long startTimeMs = stub.getStartTimeMs(index, channelId);
+ assertTrue(program.toString() + " differ from " + programInfoAt,
+ equals(programInfoAt, startTimeMs, program));
+ index++;
+ }
+ // Case #2: Corner cases where there's a program that starts at the start of the range.
+ long startTimeMs = programs.get(0).getStartTimeUtcMillis();
+ programs = mProgramDataManager.getPrograms(channelId, startTimeMs);
+ assertEquals(startTimeMs, programs.get(0).getStartTimeUtcMillis());
+
+ // Test {@link ProgramDataManager#setPrefetchTimeRange(long)}.
+ programs = mProgramDataManager.getPrograms(channelId,
+ prefetchTimeRangeStartMs - TimeUnit.HOURS.toMillis(1));
+ for (Program program : programs) {
+ assertTrue(program.getEndTimeUtcMillis() >= prefetchTimeRangeStartMs);
+ }
+ }
+ }
+
+ /**
+ * Test for following methods.
+ *
+ * <p>
+ * {@link ProgramDataManager#addOnCurrentProgramUpdatedListener},
+ * {@link ProgramDataManager#removeOnCurrentProgramUpdatedListener}.
+ * </p>
+ */
+ public void testCurrentProgramListener() throws Exception {
+ final long testChannelId = 1;
+ ProgramInfo stub = ProgramInfo.create();
+ int index = stub.getIndex(mClock.currentTimeMillis(), testChannelId);
+ // Set current time to few seconds before the current program ends,
+ // so we can see if callback is called as expected.
+ long nextProgramStartTimeMs = stub.getStartTimeMs(index + 1, testChannelId);
+ ProgramInfo nextProgramInfo = stub.build(getContext(), index + 1);
+ mClock.setCurrentTimeMillis(nextProgramStartTimeMs - (WAIT_TIME_OUT_MS / 2));
+
+ startAndWaitForComplete();
+ // Note that changing current time doesn't affect the current program
+ // because current program is updated after waiting for the program's duration.
+ // See {@link ProgramDataManager#updateCurrentProgram}.
+ mClock.setCurrentTimeMillis(mClock.currentTimeMillis() + WAIT_TIME_OUT_MS);
+ TestProgramDataManagerOnCurrentProgramUpdatedListener listener =
+ new TestProgramDataManagerOnCurrentProgramUpdatedListener();
+ mProgramDataManager.addOnCurrentProgramUpdatedListener(testChannelId, listener);
+ assertTrue(listener.currentProgramUpdatedLatch.await(WAIT_TIME_OUT_MS,
+ TimeUnit.MILLISECONDS));
+ assertEquals(testChannelId, listener.updatedChannelId);
+ assertTrue(ProgramDataManagerTest.equals(
+ nextProgramInfo, nextProgramStartTimeMs,
+ mProgramDataManager.getCurrentProgram(testChannelId)));
+ assertEquals(listener.updatedProgram, mProgramDataManager.getCurrentProgram(testChannelId));
+ }
+
+ /**
+ * Test if program data is refreshed after the program insertion.
+ */
+ public void testContentProviderUpdate() throws Exception {
+ final long testChannelId = 1;
+ startAndWaitForComplete();
+ // Force program data manager to update program data whenever it's changes.
+ mProgramDataManager.setProgramPrefetchUpdateWait(0);
+ mListener.reset();
+ List<Program> programList =
+ mProgramDataManager.getPrograms(testChannelId, mClock.currentTimeMillis());
+ assertNotNull(programList);
+ long lastProgramEndTime = programList.get(programList.size() - 1).getEndTimeUtcMillis();
+ // Make change in content provider
+ mContentProvider.simulateAppend(testChannelId);
+ assertTrue(mListener.programUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
+ programList = mProgramDataManager.getPrograms(testChannelId, mClock.currentTimeMillis());
+ assertTrue(lastProgramEndTime
+ < programList.get(programList.size() - 1).getEndTimeUtcMillis());
+ }
+
+ /**
+ * Test for {@link ProgramDataManager#setPauseProgramUpdate(boolean)}.
+ */
+ public void testSetPauseProgramUpdate() throws Exception {
+ final long testChannelId = 1;
+ startAndWaitForComplete();
+ // Force program data manager to update program data whenever it's changes.
+ mProgramDataManager.setProgramPrefetchUpdateWait(0);
+ mListener.reset();
+ mProgramDataManager.setPauseProgramUpdate(true);
+ mContentProvider.simulateAppend(testChannelId);
+ assertFalse(mListener.programUpdatedLatch.await(FAILURE_TIME_OUT_MS,
+ TimeUnit.MILLISECONDS));
+ }
+
+ private class FakeContentResolver extends MockContentResolver {
+ @Override
+ public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
+ super.notifyChange(uri, observer, syncToNetwork);
+ if (DEBUG) {
+ Log.d(TAG, "onChanged(uri=" + uri + ")");
+ }
+ if (observer != null) {
+ observer.dispatchChange(false, uri);
+ } else {
+ mProgramDataManager.getContentObserver().dispatchChange(false, uri);
+ }
+ }
+ }
+
+ private static class ProgramInfoWrapper {
+ private final int index;
+ private final long startTimeMs;
+ private final ProgramInfo programInfo;
+ public ProgramInfoWrapper(int index, long startTimeMs, ProgramInfo programInfo) {
+ this.index = index;
+ this.startTimeMs = startTimeMs;
+ this.programInfo = programInfo;
+ }
+ }
+
+ // This implements the minimal methods in content resolver
+ // and detailed assumptions are written in each method.
+ private class FakeContentProvider extends MockContentProvider {
+ private SparseArray<List<ProgramInfoWrapper>> mProgramInfoList = new SparseArray<>();
+
+ /**
+ * Constructor for FakeContentProvider
+ * <p>
+ * This initializes program info assuming that
+ * channel IDs are 1, 2, 3, ... {@link Constants#UNIT_TEST_CHANNEL_COUNT}.
+ * </p>
+ */
+ public FakeContentProvider(Context context) {
+ super(context);
+ long startTimeMs = Utils.floorTime(
+ mClock.currentTimeMillis() - ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS,
+ ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS);
+ long endTimeMs = startTimeMs + (ProgramDataManager.PROGRAM_GUIDE_MAX_TIME_RANGE / 2);
+ for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) {
+ List<ProgramInfoWrapper> programInfoList = new ArrayList<>();
+ ProgramInfo stub = ProgramInfo.create();
+ int index = stub.getIndex(startTimeMs, i);
+ long programStartTimeMs = stub.getStartTimeMs(index, i);
+ while (programStartTimeMs < endTimeMs) {
+ ProgramInfo programAt = stub.build(getContext(), index);
+ programInfoList.add(
+ new ProgramInfoWrapper(index, programStartTimeMs, programAt));
+ index++;
+ programStartTimeMs += programAt.durationMs;
+ }
+ mProgramInfoList.put(i, programInfoList);
+ }
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ if (DEBUG) {
+ Log.d(TAG, "dump query");
+ Log.d(TAG, " uri=" + uri);
+ if (projection == null || projection.length == 0) {
+ Log.d(TAG, " projection=" + projection);
+ } else {
+ for (int i = 0; i < projection.length; i++) {
+ Log.d(TAG, " projection=" + projection[i]);
+ }
+ }
+ Log.d(TAG," selection=" + selection);
+ }
+ long startTimeMs = Long.parseLong(uri.getQueryParameter(PARAM_START_TIME));
+ long endTimeMs = Long.parseLong(uri.getQueryParameter(PARAM_END_TIME));
+ if (startTimeMs == 0 || endTimeMs == 0) {
+ throw new UnsupportedOperationException();
+ }
+ assertProgramUri(uri);
+ long channelId;
+ try {
+ channelId = Long.parseLong(uri.getQueryParameter(PARAM_CHANNEL));
+ } catch (NumberFormatException e) {
+ channelId = -1;
+ }
+ return new FakeCursor(projection, channelId, startTimeMs, endTimeMs);
+ }
+
+ /**
+ * Simulate program data appends at the end of the existing programs.
+ * This appends programs until the maximum program query range
+ * ({@link ProgramDataManager#PROGRAM_GUIDE_MAX_TIME_RANGE})
+ * where we started with the inserting half of it.
+ */
+ public void simulateAppend(long channelId) {
+ long endTimeMs =
+ mClock.currentTimeMillis() + ProgramDataManager.PROGRAM_GUIDE_MAX_TIME_RANGE;
+ List<ProgramInfoWrapper> programList = mProgramInfoList.get((int) channelId);
+ if (mProgramInfoList == null) {
+ return;
+ }
+ ProgramInfo stub = ProgramInfo.create();
+ ProgramInfoWrapper last = programList.get(programList.size() - 1);
+ while (last.startTimeMs < endTimeMs) {
+ ProgramInfo nextProgramInfo = stub.build(getContext(), last.index + 1);
+ ProgramInfoWrapper next = new ProgramInfoWrapper(last.index + 1,
+ last.startTimeMs + last.programInfo.durationMs, nextProgramInfo);
+ programList.add(next);
+ last = next;
+ }
+ mContentResolver.notifyChange(TvContract.Programs.CONTENT_URI, null);
+ }
+
+ private void assertProgramUri(Uri uri) {
+ assertTrue("Uri(" + uri + ") isn't channel uri",
+ uri.toString().startsWith(TvContract.Programs.CONTENT_URI.toString()));
+ }
+
+ public ProgramInfoWrapper get(long channelId, int position) {
+ List<ProgramInfoWrapper> programList = mProgramInfoList.get((int) channelId);
+ if (programList == null || position >= programList.size()) {
+ return null;
+ }
+ return programList.get(position);
+ }
+ }
+
+ private class FakeCursor extends MockCursor {
+ private String[] ALL_COLUMNS = {
+ TvContract.Programs.COLUMN_CHANNEL_ID,
+ TvContract.Programs.COLUMN_TITLE,
+ TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
+ TvContract.Programs.COLUMN_EPISODE_TITLE,
+ TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS};
+ private final String[] mColumns;
+ private final boolean mIsQueryForSingleChannel;
+ private final long mStartTimeMs;
+ private final long mEndTimeMs;
+ private final int mCount;
+ private long mChannelId;
+ private int mProgramPosition;
+ private ProgramInfoWrapper mCurrentProgram;
+
+ /**
+ * Constructor
+ * @param columns the same as projection passed from {@link FakeContentProvider#query}.
+ * Can be null for query all.
+ * @param channelId channel ID to query programs belongs to the specified channel.
+ * Can be negative to indicate all channels.
+ * @param startTimeMs start of the time range to query programs.
+ * @param endTimeMs end of the time range to query programs.
+ */
+ public FakeCursor(String[] columns, long channelId, long startTimeMs, long endTimeMs) {
+ mColumns = (columns == null) ? ALL_COLUMNS : columns;
+ mIsQueryForSingleChannel = (channelId > 0);
+ mChannelId = channelId;
+ mProgramPosition = -1;
+ mStartTimeMs = startTimeMs;
+ mEndTimeMs = endTimeMs;
+ int count = 0;
+ while (moveToNext()) {
+ count++;
+ }
+ mCount = count;
+ // Rewind channel Id and program index.
+ mChannelId = channelId;
+ mProgramPosition = -1;
+ if (DEBUG) {
+ Log.d(TAG, "FakeCursor(columns=" + columns + ", channelId=" + channelId
+ + ", startTimeMs=" + startTimeMs + ", endTimeMs=" + endTimeMs
+ + ") has mCount=" + mCount);
+ }
+ }
+
+ @Override
+ public String getColumnName(int columnIndex) {
+ return mColumns[columnIndex];
+ }
+
+ @Override
+ public int getColumnIndex(String columnName) {
+ for (int i = 0; i < mColumns.length; i++) {
+ if (mColumns[i].equalsIgnoreCase(columnName)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int getInt(int columnIndex) {
+ if (DEBUG) {
+ Log.d(TAG, "Column (" + getColumnName(columnIndex) + ") is ignored in getInt()");
+ }
+ return 0;
+ }
+
+ @Override
+ public long getLong(int columnIndex) {
+ String columnName = getColumnName(columnIndex);
+ switch (columnName) {
+ case TvContract.Programs.COLUMN_CHANNEL_ID:
+ return mChannelId;
+ case TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS:
+ return mCurrentProgram.startTimeMs;
+ case TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS:
+ return mCurrentProgram.startTimeMs + mCurrentProgram.programInfo.durationMs;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Column (" + columnName + ") is ignored in getLong()");
+ }
+ return 0;
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ String columnName = getColumnName(columnIndex);
+ switch (columnName) {
+ case TvContract.Programs.COLUMN_TITLE:
+ return mCurrentProgram.programInfo.title;
+ case TvContract.Programs.COLUMN_SHORT_DESCRIPTION:
+ return mCurrentProgram.programInfo.description;
+ case TvContract.Programs.COLUMN_EPISODE_TITLE:
+ return mCurrentProgram.programInfo.episode;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Column (" + columnName + ") is ignored in getString()");
+ }
+ return null;
+ }
+
+ @Override
+ public int getCount() {
+ return mCount;
+ }
+
+ @Override
+ public boolean moveToNext() {
+ while (true) {
+ ProgramInfoWrapper program = mContentProvider.get(mChannelId, ++mProgramPosition);
+ if (program == null || program.startTimeMs >= mEndTimeMs) {
+ if (mIsQueryForSingleChannel) {
+ return false;
+ } else {
+ if (++mChannelId > Constants.UNIT_TEST_CHANNEL_COUNT) {
+ return false;
+ }
+ mProgramPosition = -1;
+ }
+ } else if (program.startTimeMs + program.programInfo.durationMs >= mStartTimeMs) {
+ mCurrentProgram = program;
+ break;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void close() {
+ // No-op.
+ }
+ }
+
+ private class TestProgramDataManagerListener implements ProgramDataManager.Listener {
+ public CountDownLatch programUpdatedLatch = new CountDownLatch(1);
+
+ @Override
+ public void onProgramUpdated() {
+ programUpdatedLatch.countDown();
+ }
+
+ public void reset() {
+ programUpdatedLatch = new CountDownLatch(1);
+ }
+ }
+
+ private class TestProgramDataManagerOnCurrentProgramUpdatedListener implements
+ OnCurrentProgramUpdatedListener {
+ public CountDownLatch currentProgramUpdatedLatch = new CountDownLatch(1);
+ public long updatedChannelId = -1;
+ public Program updatedProgram = null;
+
+ @Override
+ public void onCurrentProgramUpdated(long channelId, Program program) {
+ updatedChannelId = channelId;
+ updatedProgram = program;
+ currentProgramUpdatedLatch.countDown();
+ }
+ }
+}
diff --git a/tests/unit/src/com/android/tv/data/ProgramTest.java b/tests/unit/src/com/android/tv/data/ProgramTest.java
new file mode 100644
index 00000000..b4d78733
--- /dev/null
+++ b/tests/unit/src/com/android/tv/data/ProgramTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.data;
+
+import static android.media.tv.TvContract.Programs.Genres.COMEDY;
+import static android.media.tv.TvContract.Programs.Genres.FAMILY_KIDS;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link Program}.
+ */
+public class ProgramTest extends TestCase {
+
+ private static final int NOT_FOUND_GENRE = 987;
+
+ private static final int FAMILY_GENRE_ID = GenreItems.getId(FAMILY_KIDS);
+
+ private static final int COMEDY_GENRE_ID = GenreItems.getId(COMEDY);
+
+ public void testBuild() {
+ Program program = new Program.Builder().build();
+ assertEquals("isValid", false, program.isValid());
+ }
+
+ public void testNoGenres() {
+ Program program = new Program.Builder()
+ .setCanonicalGenres("")
+ .build();
+ assertNullCanonicalGenres(program);
+ assertHasGenre(program, NOT_FOUND_GENRE, false);
+ assertHasGenre(program, FAMILY_GENRE_ID, false);
+ assertHasGenre(program, COMEDY_GENRE_ID, false);
+ assertHasGenre(program, GenreItems.ID_ALL_CHANNELS, true);
+ }
+
+ public void testFamilyGenre() {
+ Program program = new Program.Builder()
+ .setCanonicalGenres(FAMILY_KIDS)
+ .build();
+ assertCanonicalGenres(program, FAMILY_KIDS);
+ assertHasGenre(program, NOT_FOUND_GENRE, false);
+ assertHasGenre(program, FAMILY_GENRE_ID, true);
+ assertHasGenre(program, COMEDY_GENRE_ID, false);
+ assertHasGenre(program, GenreItems.ID_ALL_CHANNELS, true);
+ }
+
+ public void testFamilyComedyGenre() {
+ Program program = new Program.Builder()
+ .setCanonicalGenres(FAMILY_KIDS + ", " + COMEDY)
+ .build();
+ assertCanonicalGenres(program, FAMILY_KIDS, COMEDY);
+ assertHasGenre(program, NOT_FOUND_GENRE, false);
+ assertHasGenre(program, FAMILY_GENRE_ID, true);
+ assertHasGenre(program, COMEDY_GENRE_ID, true);
+ assertHasGenre(program, GenreItems.ID_ALL_CHANNELS, true);
+ }
+
+ public void testOtherGenre() {
+ Program program = new Program.Builder()
+ .setCanonicalGenres("other")
+ .build();
+ assertCanonicalGenres(program);
+ assertHasGenre(program, NOT_FOUND_GENRE, false);
+ assertHasGenre(program, FAMILY_GENRE_ID, false);
+ assertHasGenre(program, COMEDY_GENRE_ID, false);
+ assertHasGenre(program, GenreItems.ID_ALL_CHANNELS, true);
+ }
+
+ private static void assertNullCanonicalGenres(Program program) {
+ String[] actual = program.getCanonicalGenres();
+ assertNull("Expected null canonical genres but was " + Arrays.toString(actual), actual);
+ }
+
+ private static void assertCanonicalGenres(Program program, String... expected) {
+ assertEquals("canonical genres", Arrays.asList(expected),
+ Arrays.asList(program.getCanonicalGenres()));
+ }
+
+ private static void assertHasGenre(Program program, int genreId, boolean expected) {
+ assertEquals("hasGenre(" + genreId + ")", expected, program.hasGenre(genreId));
+ }
+}
diff --git a/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java
new file mode 100644
index 00000000..4ffd9fa9
--- /dev/null
+++ b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.menu;
+
+import android.media.tv.TvTrackInfo;
+import android.os.SystemClock;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.tv.BaseMainActivityTestCase;
+import com.android.tv.MainActivity;
+import com.android.tv.customization.CustomAction;
+import com.android.tv.testing.Constants;
+import com.android.tv.testing.testinput.ChannelStateData;
+import com.android.tv.testing.testinput.TvTestInputConstants;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Tests for {@link TvOptionsRowAdapter}.
+ */
+@SmallTest
+public class TvOptionsRowAdapterTest extends BaseMainActivityTestCase {
+ private static final int WAIT_TRACK_SIZE_TIMEOUT_MS = 300;
+ public static final int TRACK_SIZE_CHECK_INTERVAL_MS = 10;
+
+
+ // TODO: Refactor TvOptionsRowAdapter so it does not rely on MainActivity
+ private TvOptionsRowAdapter mTvOptionsRowAdapter;
+
+ public TvOptionsRowAdapterTest() {
+ super(MainActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mTvOptionsRowAdapter = new TvOptionsRowAdapter(mActivity,
+ Collections.<CustomAction>emptyList());
+ tuneToChannel(TvTestInputConstants.CH_1);
+ }
+
+ public void testUpdateAudioAction_2tracks() {
+ mTvOptionsRowAdapter.update();
+ ChannelStateData data = new ChannelStateData();
+ data.mTvTrackInfos.add(Constants.GENERIC_AUDIO_TRACK);
+ updateThenTune(data, TvTestInputConstants.CH_2);
+ waitUntilTracksHaveSize(2);
+
+ boolean result = mTvOptionsRowAdapter.updateActions();
+ assertEquals("update Action had change", true, result);
+ assertEquals("Multi Audio enabled", true,
+ MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled());
+ }
+
+ public void testUpdateAudioAction_1track() {
+ mTvOptionsRowAdapter.update();
+ ChannelStateData data = new ChannelStateData();
+ data.mTvTrackInfos.clear();
+ data.mTvTrackInfos.add(Constants.GENERIC_AUDIO_TRACK);
+ updateThenTune(data, TvTestInputConstants.CH_2);
+ waitUntilTracksHaveSize(1);
+
+ boolean result = mTvOptionsRowAdapter.updateActions();
+ assertEquals("update Action had change", true, result);
+ assertEquals("Multi Audio enabled", false,
+ MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled());
+ }
+
+ public void testUpdateAudioAction_noTracks() {
+ mTvOptionsRowAdapter.update();
+ ChannelStateData data = new ChannelStateData();
+ data.mTvTrackInfos.clear();
+ updateThenTune(data, TvTestInputConstants.CH_2);
+ waitUntilTracksHaveSize(0);
+
+ boolean result = mTvOptionsRowAdapter.updateActions();
+ assertEquals("update Action had change", false, result);
+ assertEquals("Multi Audio enabled", false,
+ MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled());
+ }
+
+ private void waitUntilTracksHaveSize(int expected) {
+ long start = SystemClock.elapsedRealtime();
+ while (SystemClock.elapsedRealtime() < start + WAIT_TRACK_SIZE_TIMEOUT_MS) {
+ getInstrumentation().waitForIdleSync();
+ List<TvTrackInfo> tracks = mActivity.getTracks(TvTrackInfo.TYPE_AUDIO);
+ if (tracks != null && tracks.size() == expected) {
+ return;
+ }
+ SystemClock.sleep(TRACK_SIZE_CHECK_INTERVAL_MS);
+ }
+ fail("Waited for " + WAIT_TRACK_SIZE_TIMEOUT_MS + " milliseconds for track size to be "
+ + expected);
+ }
+}
diff --git a/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java b/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java
new file mode 100644
index 00000000..9b0e2805
--- /dev/null
+++ b/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.recommendation;
+
+import android.test.AndroidTestCase;
+
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for {@link ChannelRecord}.
+ */
+public class ChannelRecordTest extends AndroidTestCase {
+ private static final int CHANNEL_RECORD_MAX_HISTORY_SIZE = ChannelRecord.MAX_HISTORY_SIZE;
+
+ private Random mRandom;
+ private ChannelRecord mChannelRecord;
+ private long mLatestWatchEndTimeMs;
+
+ public void setUp() throws Exception {
+ super.setUp();
+ mLatestWatchEndTimeMs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
+ mChannelRecord = new ChannelRecord(getContext(), null, false);
+ mRandom = RecommendationUtils.createTestRandom();
+ }
+
+ public void testGetLastWatchEndTime_noHistory() {
+ assertEquals(0, mChannelRecord.getLastWatchEndTimeMs());
+ }
+
+ public void testGetLastWatchEndTime_oneHistory() {
+ addWatchLog();
+
+ assertEquals(mLatestWatchEndTimeMs, mChannelRecord.getLastWatchEndTimeMs());
+ }
+
+ public void testGetLastWatchEndTime_maxHistories() {
+ for (int i = 0; i < CHANNEL_RECORD_MAX_HISTORY_SIZE; ++i) {
+ addWatchLog();
+ }
+
+ assertEquals(mLatestWatchEndTimeMs, mChannelRecord.getLastWatchEndTimeMs());
+ }
+
+ public void testGetLastWatchEndTime_moreThanMaxHistories() {
+ for (int i = 0; i < CHANNEL_RECORD_MAX_HISTORY_SIZE + 1; ++i) {
+ addWatchLog();
+ }
+
+ assertEquals(mLatestWatchEndTimeMs, mChannelRecord.getLastWatchEndTimeMs());
+ }
+
+ public void testGetTotalWatchDuration_noHistory() {
+ assertEquals(0, mChannelRecord.getTotalWatchDurationMs());
+ }
+
+ public void testGetTotalWatchDuration_oneHistory() {
+ long durationMs = addWatchLog();
+
+ assertEquals(durationMs, mChannelRecord.getTotalWatchDurationMs());
+ }
+
+ public void testGetTotalWatchDuration_maxHistories() {
+ long totalWatchTimeMs = 0;
+ for (int i = 0; i < CHANNEL_RECORD_MAX_HISTORY_SIZE; ++i) {
+ long durationMs = addWatchLog();
+ totalWatchTimeMs += durationMs;
+ }
+
+ assertEquals(totalWatchTimeMs, mChannelRecord.getTotalWatchDurationMs());
+ }
+
+ public void testGetTotalWatchDuration_moreThanMaxHistories() {
+ long totalWatchTimeMs = 0;
+ long firstDurationMs = 0;
+ for (int i = 0; i < CHANNEL_RECORD_MAX_HISTORY_SIZE + 1; ++i) {
+ long durationMs = addWatchLog();
+ totalWatchTimeMs += durationMs;
+ if (i == 0) {
+ firstDurationMs = durationMs;
+ }
+ }
+
+ // Only latest CHANNEL_RECORD_MAX_HISTORY_SIZE logs are remained.
+ assertEquals(totalWatchTimeMs - firstDurationMs, mChannelRecord.getTotalWatchDurationMs());
+ }
+
+ /**
+ * Add new log history to channelRecord which its duration is lower than 1 minute.
+ *
+ * @return New watch log's duration time in milliseconds.
+ */
+ private long addWatchLog() {
+ // Time hopping with random seconds.
+ mLatestWatchEndTimeMs += TimeUnit.SECONDS.toMillis(mRandom.nextInt(60) + 1);
+
+ long durationMs = TimeUnit.SECONDS.toMillis(mRandom.nextInt(60) + 1);
+ mChannelRecord.logWatchHistory(new WatchedProgram(null,
+ mLatestWatchEndTimeMs, mLatestWatchEndTimeMs + durationMs));
+ mLatestWatchEndTimeMs += durationMs;
+
+ return durationMs;
+ }
+}
diff --git a/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java b/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java
new file mode 100644
index 00000000..ee9fa95f
--- /dev/null
+++ b/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.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.recommendation;
+
+import android.test.AndroidTestCase;
+
+import com.android.tv.data.Channel;
+import com.android.tv.recommendation.RecommendationUtils.ChannelRecordSortedMapHelper;
+import com.android.tv.recommendation.Recommender.Evaluator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base test case for Recommendation Evaluator Unit tests.
+ */
+public abstract class EvaluatorTestCase<T extends Evaluator> extends AndroidTestCase {
+ private static final long INVALID_CHANNEL_ID = -1;
+
+ private ChannelRecordSortedMapHelper mChannelRecordSortedMap;
+ private RecommendationDataManager mDataManager;
+
+ public T mEvaluator;
+
+ public void setUp() throws Exception {
+ super.setUp();
+ mChannelRecordSortedMap = new ChannelRecordSortedMapHelper(getContext());
+ mDataManager = RecommendationUtils
+ .createMockRecommendationDataManager(mChannelRecordSortedMap);
+ Recommender mRecommender = new FakeRecommender();
+ mEvaluator = createEvaluator();
+ mEvaluator.setRecommender(mRecommender);
+ mChannelRecordSortedMap.setRecommender(mRecommender);
+ mChannelRecordSortedMap.resetRandom(RecommendationUtils.createTestRandom());
+ }
+
+ /**
+ * Each evaluator test has to create Evaluator in {@code mEvaluator}.
+ */
+ public abstract T createEvaluator();
+
+ public void addChannels(int numberOfChannels) {
+ mChannelRecordSortedMap.addChannels(numberOfChannels);
+ }
+
+ public Channel addChannel() {
+ return mChannelRecordSortedMap.addChannel();
+ }
+
+ public void addRandomWatchLogs(long watchStartTimeMs, long watchEndTimeMs,
+ long maxWatchDurationMs) {
+ assertTrue(mChannelRecordSortedMap.addRandomWatchLogs(watchStartTimeMs, watchEndTimeMs,
+ maxWatchDurationMs));
+ }
+
+ public void addWatchLog(long channelId, long watchStartTimeMs, long durationTimeMs) {
+ assertTrue(mChannelRecordSortedMap.addWatchLog(channelId, watchStartTimeMs,
+ durationTimeMs));
+ }
+
+ public List<Long> getChannelIdListSorted() {
+ return new ArrayList<>(mChannelRecordSortedMap.keySet());
+ }
+
+ public long getLatestWatchEndTimeMs() {
+ long latestWatchEndTimeMs = 0;
+ for (ChannelRecord channelRecord : mChannelRecordSortedMap.values()) {
+ latestWatchEndTimeMs = Math.max(latestWatchEndTimeMs,
+ channelRecord.getLastWatchEndTimeMs());
+ }
+ return latestWatchEndTimeMs;
+ }
+
+ /**
+ * Check whether scores of each channels are valid.
+ */
+ protected void assertChannelScoresValid() {
+ assertEquals(Evaluator.NOT_RECOMMENDED, mEvaluator.evaluateChannel(INVALID_CHANNEL_ID));
+ assertEquals(Evaluator.NOT_RECOMMENDED,
+ mEvaluator.evaluateChannel(mChannelRecordSortedMap.size()));
+
+ for (long channelId : mChannelRecordSortedMap.keySet()) {
+ double score = mEvaluator.evaluateChannel(channelId);
+ assertTrue("Channel " + channelId + " score of " + score + "is not valid",
+ score == Evaluator.NOT_RECOMMENDED || (0.0 <= score && score <= 1.0));
+ }
+ }
+
+ /**
+ * Notify that loading channels and watch logs are finished.
+ */
+ protected void notifyChannelAndWatchLogLoaded() {
+ mEvaluator.onChannelRecordListChanged(new ArrayList<>(mChannelRecordSortedMap.values()));
+ }
+
+ private class FakeRecommender extends Recommender {
+ public FakeRecommender() {
+ super(new Recommender.Listener() {
+ @Override
+ public void onRecommenderReady() {
+ }
+
+ @Override
+ public void onRecommendationChanged() {
+ }
+ }, true, mDataManager);
+ }
+
+ @Override
+ public ChannelRecord getChannelRecord(long channelId) {
+ return mChannelRecordSortedMap.get(channelId);
+ }
+ }
+}
diff --git a/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java
new file mode 100644
index 00000000..c33271bc
--- /dev/null
+++ b/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.recommendation;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for {@link FavoriteChannelEvaluator}.
+ */
+public class FavoriteChannelEvaluatorTest extends EvaluatorTestCase<FavoriteChannelEvaluator> {
+ private static final int DEFAULT_NUMBER_OF_CHANNELS = 4;
+ private static final long DEFAULT_WATCH_START_TIME_MS =
+ System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2);
+ private static final long DEFAULT_WATCH_END_TIME_MS =
+ System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
+ private static final long DEFAULT_MAX_WATCH_DURATION_MS = TimeUnit.HOURS.toMillis(1);
+
+ public FavoriteChannelEvaluator createEvaluator() {
+ return new FavoriteChannelEvaluator();
+ }
+
+ public void testOneChannelWithNoWatchLog() {
+ long channelId = addChannel().getId();
+ notifyChannelAndWatchLogLoaded();
+
+ assertEquals(Recommender.Evaluator.NOT_RECOMMENDED,
+ mEvaluator.evaluateChannel(channelId));
+ }
+
+ public void testOneChannelWithRandomWatchLogs() {
+ addChannel();
+ addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS,
+ DEFAULT_MAX_WATCH_DURATION_MS);
+ notifyChannelAndWatchLogLoaded();
+
+ assertChannelScoresValid();
+ }
+
+ public void testMultiChannelsWithNoWatchLog() {
+ addChannels(DEFAULT_NUMBER_OF_CHANNELS);
+ notifyChannelAndWatchLogLoaded();
+
+ List<Long> channelIdList = getChannelIdListSorted();
+ for (long channelId : channelIdList) {
+ assertEquals(Recommender.Evaluator.NOT_RECOMMENDED,
+ mEvaluator.evaluateChannel(channelId));
+ }
+ }
+
+ public void testMultiChannelsWithRandomWatchLogs() {
+ addChannels(DEFAULT_NUMBER_OF_CHANNELS);
+ addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS,
+ DEFAULT_MAX_WATCH_DURATION_MS);
+ notifyChannelAndWatchLogLoaded();
+
+ assertChannelScoresValid();
+ }
+
+ public void testMultiChannelsWithSimpleWatchLogs() {
+ addChannels(DEFAULT_NUMBER_OF_CHANNELS);
+ // For two channels which has ID x and y (x < y), the channel y is more watched
+ // than the channel x. (Duration is longer than channel x)
+ long latestWatchEndTimeMs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2);
+ long durationMs = 0;
+ List<Long> channelIdList = getChannelIdListSorted();
+ for (long channelId : channelIdList) {
+ durationMs += TimeUnit.MINUTES.toMillis(30);
+ addWatchLog(channelId, latestWatchEndTimeMs, durationMs);
+ latestWatchEndTimeMs += durationMs;
+ }
+ notifyChannelAndWatchLogLoaded();
+
+ assertChannelScoresValid();
+ // Channel score must be increased as channel ID increased.
+ double previousScore = Recommender.Evaluator.NOT_RECOMMENDED;
+ for (long channelId : channelIdList) {
+ double score = mEvaluator.evaluateChannel(channelId);
+ assertTrue(previousScore <= score);
+ previousScore = score;
+ }
+ }
+
+ public void testTwoChannelsWithSameWatchDuration() {
+ long channelOne = addChannel().getId();
+ long channelTwo = addChannel().getId();
+ addWatchLog(channelOne, System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1),
+ TimeUnit.MINUTES.toMillis(30));
+ addWatchLog(channelTwo, System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(30),
+ TimeUnit.MINUTES.toMillis(30));
+ notifyChannelAndWatchLogLoaded();
+
+ assertTrue(mEvaluator.evaluateChannel(channelOne) ==
+ mEvaluator.evaluateChannel(channelTwo));
+ }
+
+ public void testTwoChannelsWithDifferentWatchDuration() {
+ long channelOne = addChannel().getId();
+ long channelTwo = addChannel().getId();
+ addWatchLog(channelOne, System.currentTimeMillis() - TimeUnit.HOURS.toMillis(3),
+ TimeUnit.MINUTES.toMillis(30));
+ addWatchLog(channelTwo, System.currentTimeMillis() - TimeUnit.HOURS.toMillis(2),
+ TimeUnit.HOURS.toMillis(1));
+ notifyChannelAndWatchLogLoaded();
+
+ // Channel two was watched longer than channel one, so it's score is bigger.
+ assertTrue(mEvaluator.evaluateChannel(channelOne) < mEvaluator.evaluateChannel(channelTwo));
+
+ addWatchLog(channelOne, System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1),
+ TimeUnit.HOURS.toMillis(1));
+
+ // Now, channel one was watched longer than channel two, so it's score is bigger.
+ assertTrue(mEvaluator.evaluateChannel(channelOne) > mEvaluator.evaluateChannel(channelTwo));
+ }
+
+ public void testScoreIncreasesWithNewWatchLog() {
+ long channelId = addChannel().getId();
+ addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS,
+ DEFAULT_MAX_WATCH_DURATION_MS);
+ notifyChannelAndWatchLogLoaded();
+
+ long latestWatchEndTimeMs = getLatestWatchEndTimeMs();
+ double previousScore = mEvaluator.evaluateChannel(channelId);
+
+ addWatchLog(channelId, latestWatchEndTimeMs, TimeUnit.MINUTES.toMillis(10));
+
+ // Score must be increased because total watch duration of the channel increases.
+ assertTrue(previousScore <= mEvaluator.evaluateChannel(channelId));
+ }
+}
diff --git a/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java
new file mode 100644
index 00000000..a888ceea
--- /dev/null
+++ b/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.recommendation;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for {@link RecentChannelEvaluator}.
+ */
+public class RecentChannelEvaluatorTest extends EvaluatorTestCase<RecentChannelEvaluator> {
+ private static final int DEFAULT_NUMBER_OF_CHANNELS = 4;
+ private static final long DEFAULT_WATCH_START_TIME_MS =
+ System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2);
+ private static final long DEFAULT_WATCH_END_TIME_MS =
+ System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
+ private static final long DEFAULT_MAX_WATCH_DURATION_MS = TimeUnit.HOURS.toMillis(1);
+
+ public RecentChannelEvaluator createEvaluator() {
+ return new RecentChannelEvaluator();
+ }
+
+ public void testOneChannelWithNoWatchLog() {
+ long channelId = addChannel().getId();
+ notifyChannelAndWatchLogLoaded();
+
+ assertEquals(Recommender.Evaluator.NOT_RECOMMENDED,
+ mEvaluator.evaluateChannel(channelId));
+ }
+
+ public void testOneChannelWithRandomWatchLogs() {
+ addChannel();
+ addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS,
+ DEFAULT_MAX_WATCH_DURATION_MS);
+ notifyChannelAndWatchLogLoaded();
+
+ assertChannelScoresValid();
+ }
+
+ public void testMultiChannelsWithNoWatchLog() {
+ addChannels(DEFAULT_NUMBER_OF_CHANNELS);
+ notifyChannelAndWatchLogLoaded();
+
+ List<Long> channelIdList = getChannelIdListSorted();
+ for (long channelId : channelIdList) {
+ assertEquals(Recommender.Evaluator.NOT_RECOMMENDED,
+ mEvaluator.evaluateChannel(channelId));
+ }
+ }
+
+ public void testMultiChannelsWithRandomWatchLogs() {
+ addChannels(DEFAULT_NUMBER_OF_CHANNELS);
+ addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS,
+ DEFAULT_MAX_WATCH_DURATION_MS);
+ notifyChannelAndWatchLogLoaded();
+
+ assertChannelScoresValid();
+ }
+
+ public void testMultiChannelsWithSimpleWatchLogs() {
+ addChannels(DEFAULT_NUMBER_OF_CHANNELS);
+ // Every channel has one watch log with 1 hour. Also, for two channels
+ // which has ID x and y (x < y), the channel y is watched later than the channel x.
+ long latestWatchEndTimeMs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2);
+ List<Long> channelIdList = getChannelIdListSorted();
+ for (long channelId : channelIdList) {
+ addWatchLog(channelId, latestWatchEndTimeMs, TimeUnit.HOURS.toMillis(1));
+ latestWatchEndTimeMs += TimeUnit.HOURS.toMillis(1);
+ }
+ notifyChannelAndWatchLogLoaded();
+
+ assertChannelScoresValid();
+ // Channel score must be increased as channel ID increased.
+ double previousScore = Recommender.Evaluator.NOT_RECOMMENDED;
+ for (long channelId : channelIdList) {
+ double score = mEvaluator.evaluateChannel(channelId);
+ assertTrue(previousScore <= score);
+ previousScore = score;
+ }
+ }
+
+ public void testScoreIncreasesWithNewWatchLog() {
+ addChannels(DEFAULT_NUMBER_OF_CHANNELS);
+ addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS,
+ DEFAULT_MAX_WATCH_DURATION_MS);
+ notifyChannelAndWatchLogLoaded();
+
+ List<Long> channelIdList = getChannelIdListSorted();
+ long latestWatchEndTimeMs = getLatestWatchEndTimeMs();
+ for (long channelId : channelIdList) {
+ double previousScore = mEvaluator.evaluateChannel(channelId);
+
+ long durationMs = TimeUnit.MINUTES.toMillis(10);
+ addWatchLog(channelId, latestWatchEndTimeMs, durationMs);
+ latestWatchEndTimeMs += durationMs;
+
+ // Score must be increased because recentness of the log increases.
+ assertTrue(previousScore <= mEvaluator.evaluateChannel(channelId));
+ }
+ }
+
+ public void testScoreDecreasesWithIncrementOfWatchedLogUpdatedTime() {
+ addChannels(DEFAULT_NUMBER_OF_CHANNELS);
+ addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS,
+ DEFAULT_MAX_WATCH_DURATION_MS);
+ notifyChannelAndWatchLogLoaded();
+
+ Map<Long, Double> scores = new HashMap<>();
+ List<Long> channelIdList = getChannelIdListSorted();
+ long latestWatchedEndTimeMs = getLatestWatchEndTimeMs();
+
+ for (long channelId : channelIdList) {
+ scores.put(channelId, mEvaluator.evaluateChannel(channelId));
+ }
+
+ long newChannelId = addChannel().getId();
+ addWatchLog(newChannelId, latestWatchedEndTimeMs, TimeUnit.MINUTES.toMillis(10));
+
+ for (long channelId : channelIdList) {
+ // Score must be decreased because LastWatchLogUpdateTime increases by new log.
+ assertTrue(mEvaluator.evaluateChannel(channelId) <= scores.get(channelId));
+ }
+ }
+}
diff --git a/tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java b/tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java
new file mode 100644
index 00000000..d275bfbb
--- /dev/null
+++ b/tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java
@@ -0,0 +1,180 @@
+/*
+ * 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.recommendation;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.tv.data.Channel;
+
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Random;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+public class RecommendationUtils {
+ private static final String TAG = "RecommendationUtils";
+ private static final long INVALID_CHANNEL_ID = -1;
+ private static final long DEFAULT_RANDOM_SEED = getSeed();
+
+ private static long getSeed() {
+ // Set random seed as the date to track failed test data easily.
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.getDefault());
+ String today = dateFormat.format(new Date());
+ Log.d(TAG, "Today's random seed is " + today);
+ return Long.valueOf(today);
+ }
+
+ /**
+ * Return the Random class which is needed to make random data for testing.
+ * Default seed of the random is today's date.
+ */
+ public static Random createTestRandom() {
+ return new Random(DEFAULT_RANDOM_SEED);
+ }
+
+ /**
+ * Create a mock RecommendationDataManager backed by a {@link ChannelRecordSortedMapHelper}.
+ */
+ public static RecommendationDataManager createMockRecommendationDataManager(
+ final ChannelRecordSortedMapHelper channelRecordSortedMap) {
+ RecommendationDataManager dataManager = Mockito.mock(RecommendationDataManager.class);
+ Mockito.doAnswer(new Answer<Integer>() {
+ @Override
+ public Integer answer(InvocationOnMock invocation) throws Throwable {
+ return channelRecordSortedMap.size();
+ }
+ }).when(dataManager).getChannelRecordCount();
+ Mockito.doAnswer(new Answer<Collection<ChannelRecord>>() {
+ @Override
+ public Collection<ChannelRecord> answer(InvocationOnMock invocation) throws Throwable {
+ return channelRecordSortedMap.values();
+ }
+ }).when(dataManager).getChannelRecords();
+ Mockito.doAnswer(new Answer<ChannelRecord>() {
+ @Override
+ public ChannelRecord answer(InvocationOnMock invocation) throws Throwable {
+ long channelId = (long) invocation.getArguments()[0];
+ return channelRecordSortedMap.get(channelId);
+ }
+ }).when(dataManager).getChannelRecord(Matchers.anyLong());
+ return dataManager;
+ }
+
+ public static class ChannelRecordSortedMapHelper extends TreeMap<Long, ChannelRecord> {
+ private Context mContext;
+ private Recommender mRecommender;
+ private Random mRandom = createTestRandom();
+
+ public ChannelRecordSortedMapHelper(Context context) {
+ mContext = context;
+ }
+
+ public void setRecommender(Recommender recommender) {
+ mRecommender = recommender;
+ }
+
+ public void resetRandom(Random random) {
+ mRandom = random;
+ }
+
+ /**
+ * Add new {@code numberOfChannels} channels by adding channel record to
+ * {@code channelRecordMap} with no history.
+ * This action corresponds to loading channels in the RecommendationDataManger.
+ */
+ public void addChannels(int numberOfChannels) {
+ for (int i = 0; i < numberOfChannels; ++i) {
+ addChannel();
+ }
+ }
+
+ /**
+ * Add new one channel by adding channel record to {@code channelRecordMap} with no history.
+ * This action corresponds to loading one channel in the RecommendationDataManger.
+ *
+ * @return The new channel was made by this method.
+ */
+ public Channel addChannel() {
+ long channelId = size();
+ Channel channel = new Channel.Builder().setId(channelId).build();
+ ChannelRecord channelRecord = new ChannelRecord(mContext, channel, false);
+ put(channelId, channelRecord);
+ return channel;
+ }
+
+ /**
+ * Add the watch logs which its durationTime is under {@code maxWatchDurationMs}.
+ * Add until latest watch end time becomes bigger than {@code watchEndTimeMs},
+ * starting from {@code watchStartTimeMs}.
+ *
+ * @return true if adding watch log success, otherwise false.
+ */
+ public boolean addRandomWatchLogs(long watchStartTimeMs, long watchEndTimeMs,
+ long maxWatchDurationMs) {
+ long latestWatchEndTimeMs = watchStartTimeMs;
+ long previousChannelId = INVALID_CHANNEL_ID;
+ List<Long> channelIdList = new ArrayList<>(keySet());
+ while (latestWatchEndTimeMs < watchEndTimeMs) {
+ long channelId = channelIdList.get(mRandom.nextInt(channelIdList.size()));
+ if (previousChannelId == channelId) {
+ // Time hopping with random minutes.
+ latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(mRandom.nextInt(30) + 1);
+ }
+ long watchedDurationMs = mRandom.nextInt((int) maxWatchDurationMs) + 1;
+ if (!addWatchLog(channelId, latestWatchEndTimeMs, watchedDurationMs)) {
+ return false;
+ }
+ latestWatchEndTimeMs += watchedDurationMs;
+ previousChannelId = channelId;
+ }
+ return true;
+ }
+
+ /**
+ * Add new watch log to channel that id is {@code ChannelId}. Add watch log starts from
+ * {@code watchStartTimeMs} with duration {@code durationTimeMs}. If adding is finished,
+ * notify the recommender that there's a new watch log.
+ *
+ * @return true if adding watch log success, otherwise false.
+ */
+ public boolean addWatchLog(long channelId, long watchStartTimeMs, long durationTimeMs) {
+ ChannelRecord channelRecord = get(channelId);
+ if (channelRecord == null ||
+ watchStartTimeMs + durationTimeMs > System.currentTimeMillis()) {
+ return false;
+ }
+
+ channelRecord.logWatchHistory(new WatchedProgram(null, watchStartTimeMs,
+ watchStartTimeMs + durationTimeMs));
+ if (mRecommender != null) {
+ mRecommender.onNewWatchLog(channelRecord);
+ }
+ return true;
+ }
+ }
+}
diff --git a/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java
new file mode 100644
index 00000000..4f16d168
--- /dev/null
+++ b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java
@@ -0,0 +1,324 @@
+/*
+ * 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.recommendation;
+
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+
+import com.android.tv.data.Channel;
+import com.android.tv.recommendation.RecommendationUtils.ChannelRecordSortedMapHelper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+public class RecommenderTest extends AndroidTestCase {
+ private static final int DEFAULT_NUMBER_OF_CHANNELS = 5;
+ private static final long DEFAULT_WATCH_START_TIME_MS =
+ System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2);
+ private static final long DEFAULT_WATCH_END_TIME_MS =
+ System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
+ private static final long DEFAULT_MAX_WATCH_DURATION_MS = TimeUnit.HOURS.toMillis(1);
+
+ private final Comparator<Channel> CHANNEL_SORT_KEY_COMPARATOR = new Comparator<Channel>() {
+ @Override
+ public int compare(Channel lhs, Channel rhs) {
+ return mRecommender.getChannelSortKey(lhs.getId())
+ .compareTo(mRecommender.getChannelSortKey(rhs.getId()));
+ }
+ };
+ private final Runnable START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS = new Runnable() {
+ @Override
+ public void run() {
+ // Add 4 channels in ChannelRecordMap for testing. Store the added channels to
+ // mChannels_1 ~ mChannels_4. They are sorted by channel id in increasing order.
+ mChannel_1 = mChannelRecordSortedMap.addChannel();
+ mChannel_2 = mChannelRecordSortedMap.addChannel();
+ mChannel_3 = mChannelRecordSortedMap.addChannel();
+ mChannel_4 = mChannelRecordSortedMap.addChannel();
+ }
+ };
+
+ private RecommendationDataManager mDataManager;
+ private Recommender mRecommender;
+ private FakeEvaluator mEvaluator;
+ private ChannelRecordSortedMapHelper mChannelRecordSortedMap;
+ private boolean mOnRecommenderReady;
+ private boolean mOnRecommendationChanged;
+ private Channel mChannel_1;
+ private Channel mChannel_2;
+ private Channel mChannel_3;
+ private Channel mChannel_4;
+
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mChannelRecordSortedMap = new ChannelRecordSortedMapHelper(getContext());
+ mDataManager = RecommendationUtils
+ .createMockRecommendationDataManager(mChannelRecordSortedMap);
+ mChannelRecordSortedMap.resetRandom(RecommendationUtils.createTestRandom());
+ }
+
+ public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveNoScore() {
+ createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
+
+ // Recommender doesn't recommend any channels because all channels are not recommended.
+ assertEquals(0, mRecommender.recommendChannels().size());
+ assertEquals(0, mRecommender.recommendChannels(-5).size());
+ assertEquals(0, mRecommender.recommendChannels(0).size());
+ assertEquals(0, mRecommender.recommendChannels(3).size());
+ assertEquals(0, mRecommender.recommendChannels(4).size());
+ assertEquals(0, mRecommender.recommendChannels(5).size());
+ }
+
+ public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveNoScore() {
+ createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
+
+ // Recommender recommends every channel because it recommends not-recommended channels too.
+ assertEquals(4, mRecommender.recommendChannels().size());
+ assertEquals(0, mRecommender.recommendChannels(-5).size());
+ assertEquals(0, mRecommender.recommendChannels(0).size());
+ assertEquals(3, mRecommender.recommendChannels(3).size());
+ assertEquals(4, mRecommender.recommendChannels(4).size());
+ assertEquals(4, mRecommender.recommendChannels(5).size());
+ }
+
+ public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveScore() {
+ createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
+
+ setChannelScores_scoreIncreasesAsChannelIdIncreases();
+
+ // recommendChannels must be sorted by score in decreasing order.
+ // (i.e. sorted by channel ID in decreasing order in this case)
+ MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(),
+ mChannel_4, mChannel_3, mChannel_2, mChannel_1);
+ assertEquals(0, mRecommender.recommendChannels(-5).size());
+ assertEquals(0, mRecommender.recommendChannels(0).size());
+ MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(3),
+ mChannel_4, mChannel_3, mChannel_2);
+ MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(4),
+ mChannel_4, mChannel_3, mChannel_2, mChannel_1);
+ MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(5),
+ mChannel_4, mChannel_3, mChannel_2, mChannel_1);
+ }
+
+ public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveScore() {
+ createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
+
+ setChannelScores_scoreIncreasesAsChannelIdIncreases();
+
+ // recommendChannels must be sorted by score in decreasing order.
+ // (i.e. sorted by channel ID in decreasing order in this case)
+ MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(),
+ mChannel_4, mChannel_3, mChannel_2, mChannel_1);
+ assertEquals(0, mRecommender.recommendChannels(-5).size());
+ assertEquals(0, mRecommender.recommendChannels(0).size());
+ MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(3),
+ mChannel_4, mChannel_3, mChannel_2);
+ MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(4),
+ mChannel_4, mChannel_3, mChannel_2, mChannel_1);
+ MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(5),
+ mChannel_4, mChannel_3, mChannel_2, mChannel_1);
+ }
+
+ public void testRecommendChannels_includeRecommendedOnly_fewChannelsHaveScore() {
+ createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
+
+ mEvaluator.setChannelScore(mChannel_1.getId(), 1.0);
+ mEvaluator.setChannelScore(mChannel_2.getId(), 1.0);
+
+ // Only two channels are recommended because recommender doesn't recommend other channels.
+ MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(),
+ mChannel_1, mChannel_2);
+ assertEquals(0, mRecommender.recommendChannels(-5).size());
+ assertEquals(0, mRecommender.recommendChannels(0).size());
+ MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(3),
+ mChannel_1, mChannel_2);
+ MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(4),
+ mChannel_1, mChannel_2);
+ MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(5),
+ mChannel_1, mChannel_2);
+ }
+
+ public void testRecommendChannels_notIncludeRecommendedOnly_fewChannelsHaveScore() {
+ createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
+
+ mEvaluator.setChannelScore(mChannel_1.getId(), 1.0);
+ mEvaluator.setChannelScore(mChannel_2.getId(), 1.0);
+
+ assertEquals(4, mRecommender.recommendChannels().size());
+ MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels().subList(0, 2),
+ mChannel_1, mChannel_2);
+
+ assertEquals(0, mRecommender.recommendChannels(-5).size());
+ assertEquals(0, mRecommender.recommendChannels(0).size());
+
+ assertEquals(3, mRecommender.recommendChannels(3).size());
+ MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(3).subList(0, 2),
+ mChannel_1, mChannel_2);
+
+ assertEquals(4, mRecommender.recommendChannels(4).size());
+ MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(4).subList(0, 2),
+ mChannel_1, mChannel_2);
+
+ assertEquals(4, mRecommender.recommendChannels(5).size());
+ MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(5).subList(0, 2),
+ mChannel_1, mChannel_2);
+ }
+
+ public void testGetChannelSortKey_recommendAllChannels() {
+ createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
+
+ setChannelScores_scoreIncreasesAsChannelIdIncreases();
+
+ List<Channel> expectedChannelList = mRecommender.recommendChannels();
+ List<Channel> channelList = Arrays.asList(mChannel_1, mChannel_2, mChannel_3, mChannel_4);
+ Collections.sort(channelList, CHANNEL_SORT_KEY_COMPARATOR);
+
+ // Recommended channel list and channel list sorted by sort key must be the same.
+ MoreAsserts.assertContentsInOrder(channelList, expectedChannelList.toArray());
+ assertSortKeyNotInvalid(channelList);
+ }
+
+ public void testGetChannelSortKey_recommendFewChannels() {
+ // Test with recommending 3 channels.
+ createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
+
+ setChannelScores_scoreIncreasesAsChannelIdIncreases();
+
+ List<Channel> expectedChannelList = mRecommender.recommendChannels(3);
+ // A channel which is not recommended by the recommender has to get an invalid sort key.
+ assertEquals(Recommender.INVALID_CHANNEL_SORT_KEY,
+ mRecommender.getChannelSortKey(mChannel_1.getId()));
+
+ List<Channel> channelList = Arrays.asList(mChannel_2, mChannel_3, mChannel_4);
+ Collections.sort(channelList, CHANNEL_SORT_KEY_COMPARATOR);
+
+ MoreAsserts.assertContentsInOrder(channelList, expectedChannelList.toArray());
+ assertSortKeyNotInvalid(channelList);
+ }
+
+ public void testListener_onRecommendationChanged() {
+ createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS);
+ // FakeEvaluator doesn't recommend a channel with empty watch log. As every channel
+ // doesn't have a watch log, nothing is recommended and recommendation isn't changed.
+ assertFalse(mOnRecommendationChanged);
+
+ // Set lastRecommendationUpdatedTimeUtcMs to check recommendation changed because,
+ // recommender has a minimum recommendation update period.
+ mRecommender.setLastRecommendationUpdatedTimeUtcMs(
+ System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10));
+ long latestWatchEndTimeMs = DEFAULT_WATCH_START_TIME_MS;
+ for (long channelId : mChannelRecordSortedMap.keySet()) {
+ mEvaluator.setChannelScore(channelId, 1.0);
+ // Add a log to recalculate the recommendation score.
+ assertTrue(mChannelRecordSortedMap.addWatchLog(channelId, latestWatchEndTimeMs,
+ TimeUnit.MINUTES.toMillis(10)));
+ latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(10);
+ }
+
+ // onRecommendationChanged must be called, because recommend channels are not empty,
+ // by setting score to each channel.
+ assertTrue(mOnRecommendationChanged);
+ }
+
+ public void testListener_onRecommenderReady() {
+ createRecommender(true, new Runnable() {
+ @Override
+ public void run() {
+ mChannelRecordSortedMap.addChannels(DEFAULT_NUMBER_OF_CHANNELS);
+ mChannelRecordSortedMap.addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS,
+ DEFAULT_WATCH_END_TIME_MS, DEFAULT_MAX_WATCH_DURATION_MS);
+ }
+ });
+
+ // After loading channels and watch logs are finished, recommender must be available to use.
+ assertTrue(mOnRecommenderReady);
+ }
+
+ private void assertSortKeyNotInvalid(List<Channel> channelList) {
+ for (Channel channel : channelList) {
+ MoreAsserts.assertNotEqual(Recommender.INVALID_CHANNEL_SORT_KEY,
+ mRecommender.getChannelSortKey(channel.getId()));
+ }
+ }
+
+ private void createRecommender(boolean includeRecommendedOnly,
+ Runnable startDataManagerRunnable) {
+ mRecommender = new Recommender(new Recommender.Listener() {
+ @Override
+ public void onRecommenderReady() {
+ mOnRecommenderReady = true;
+ }
+ @Override
+ public void onRecommendationChanged() {
+ mOnRecommendationChanged = true;
+ }
+ }, includeRecommendedOnly, mDataManager);
+
+ mEvaluator = new FakeEvaluator();
+ mRecommender.registerEvaluator(mEvaluator);
+ mChannelRecordSortedMap.setRecommender(mRecommender);
+
+ // When mRecommender is instantiated, its dataManager will be started, and load channels
+ // and watch history data if it is not started.
+ if (startDataManagerRunnable != null) {
+ startDataManagerRunnable.run();
+ mRecommender.onChannelRecordChanged();
+ }
+ // After loading channels and watch history data are finished,
+ // RecommendationDataManager calls listener.onChannelRecordLoaded()
+ // which will be mRecommender.onChannelRecordLoaded().
+ mRecommender.onChannelRecordLoaded();
+ }
+
+ private List<Long> getChannelIdListSorted() {
+ return new ArrayList<>(mChannelRecordSortedMap.keySet());
+ }
+
+ private void setChannelScores_scoreIncreasesAsChannelIdIncreases() {
+ List<Long> channelIdList = getChannelIdListSorted();
+ double score = Math.pow(0.5, channelIdList.size());
+ for (long channelId : channelIdList) {
+ // Channel with smaller id has smaller score than channel with higher id.
+ mEvaluator.setChannelScore(channelId, score);
+ score *= 2.0;
+ }
+ }
+
+ private class FakeEvaluator extends Recommender.Evaluator {
+ private Map<Long, Double> mChannelScore = new HashMap<>();
+
+ @Override
+ public double evaluateChannel(long channelId) {
+ if (getRecommender().getChannelRecord(channelId) == null) {
+ return NOT_RECOMMENDED;
+ }
+ Double score = mChannelScore.get(channelId);
+ return score == null ? NOT_RECOMMENDED : score;
+ }
+
+ public void setChannelScore(long channelId, double score) {
+ mChannelScore.put(channelId, score);
+ }
+ }
+}
diff --git a/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java
new file mode 100644
index 00000000..e3c92859
--- /dev/null
+++ b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.recommendation;
+
+import android.test.MoreAsserts;
+
+import com.android.tv.data.Program;
+import com.android.tv.recommendation.RoutineWatchEvaluator.ProgramTime;
+
+import java.util.Calendar;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class RoutineWatchEvaluatorTest extends EvaluatorTestCase<RoutineWatchEvaluator> {
+
+ public RoutineWatchEvaluator createEvaluator() {
+ return new RoutineWatchEvaluator();
+ }
+
+ public void testSplitTextToWords() {
+ assertSplitTextToWords("");
+ assertSplitTextToWords("Google", "Google");
+ assertSplitTextToWords("The Big Bang Theory", "The", "Big", "Bang", "Theory");
+ assertSplitTextToWords("Hello, world!", "Hello", "world");
+ assertSplitTextToWords("Adam's Rib", "Adam's", "Rib");
+ assertSplitTextToWords("G.I. Joe", "G.I", "Joe");
+ assertSplitTextToWords("A.I.", "A.I");
+ }
+
+ public void testCalculateMaximumMatchedWordSequenceLength() {
+ assertMaximumMatchedWordSequenceLength(0, "", "Google");
+ assertMaximumMatchedWordSequenceLength(2, "The Big Bang Theory", "Big Bang");
+ assertMaximumMatchedWordSequenceLength(2, "The Big Bang Theory", "Theory Of Big Bang");
+ assertMaximumMatchedWordSequenceLength(4, "The Big Bang Theory", "The Big Bang Theory");
+ assertMaximumMatchedWordSequenceLength(1, "Modern Family", "Family Guy");
+ assertMaximumMatchedWordSequenceLength(1, "The Simpsons", "The Walking Dead");
+ assertMaximumMatchedWordSequenceLength(3, "Game Of Thrones 1", "Game Of Thrones 6");
+ assertMaximumMatchedWordSequenceLength(0, "Dexter", "Friends");
+ }
+
+ public void testProgramTime_createFromProgram() {
+ Calendar time = Calendar.getInstance();
+ int todayDayOfWeek = time.get(Calendar.DAY_OF_WEEK);
+ // Value of DayOfWeek is between 1 and 7 (inclusive).
+ int tomorrowDayOfWeek = (todayDayOfWeek % 7) + 1;
+
+ // Today 00:00 - 01:00.
+ ProgramTime programTimeToday0000_0100 = ProgramTime.createFromProgram(
+ createDummyProgram(todayAtHourMin(0, 0), TimeUnit.HOURS.toMillis(1)));
+ assertProgramTime(todayDayOfWeek, hourMinuteToSec(0, 0), hourMinuteToSec(1, 0),
+ programTimeToday0000_0100);
+
+ // Today 23:30 - 24:30.
+ ProgramTime programTimeToday2330_2430 = ProgramTime.createFromProgram(
+ createDummyProgram(todayAtHourMin(23, 30), TimeUnit.HOURS.toMillis(1)));
+ assertProgramTime(todayDayOfWeek, hourMinuteToSec(23, 30), hourMinuteToSec(24, 30),
+ programTimeToday2330_2430);
+
+ // Tomorrow 00:00 - 01:00.
+ ProgramTime programTimeTomorrow0000_0100 = ProgramTime.createFromProgram(
+ createDummyProgram(tomorrowAtHourMin(0, 0), TimeUnit.HOURS.toMillis(1)));
+ assertProgramTime(tomorrowDayOfWeek, hourMinuteToSec(0, 0), hourMinuteToSec(1, 0),
+ programTimeTomorrow0000_0100);
+
+ // Tomorrow 23:30 - 24:30.
+ ProgramTime programTimeTomorrow2330_2430 = ProgramTime.createFromProgram(
+ createDummyProgram(tomorrowAtHourMin(23, 30), TimeUnit.HOURS.toMillis(1)));
+ assertProgramTime(tomorrowDayOfWeek, hourMinuteToSec(23, 30), hourMinuteToSec(24, 30),
+ programTimeTomorrow2330_2430);
+
+ // Today 18:00 - Tomorrow 12:00.
+ ProgramTime programTimeToday1800_3600 = ProgramTime.createFromProgram(
+ createDummyProgram(todayAtHourMin(18, 0), TimeUnit.HOURS.toMillis(18)));
+ // Maximum duration of ProgramTime is 12 hours.
+ // So, this program looks like it ends at Tomorrow 06:00 (30:00).
+ assertProgramTime(todayDayOfWeek, hourMinuteToSec(18, 0), hourMinuteToSec(30, 0),
+ programTimeToday1800_3600);
+ }
+
+ public void testCalculateOverlappedIntervalScore() {
+ // Today 21:00 - 24:00.
+ ProgramTime programTimeToday2100_2400 = ProgramTime.createFromProgram(
+ createDummyProgram(todayAtHourMin(21, 0), TimeUnit.HOURS.toMillis(3)));
+ // Today 22:00 - 01:00.
+ ProgramTime programTimeToday2200_0100 = ProgramTime.createFromProgram(
+ createDummyProgram(todayAtHourMin(22, 0), TimeUnit.HOURS.toMillis(3)));
+ // Tomorrow 00:00 - 03:00.
+ ProgramTime programTimeTomorrow0000_0300 = ProgramTime.createFromProgram(
+ createDummyProgram(tomorrowAtHourMin(0, 0), TimeUnit.HOURS.toMillis(3)));
+ // Tomorrow 20:00 - Tomorrow 23:00.
+ ProgramTime programTimeTomorrow2000_2300 = ProgramTime.createFromProgram(
+ createDummyProgram(tomorrowAtHourMin(20, 0), TimeUnit.HOURS.toMillis(3)));
+
+ // Check intersection time and commutative law in all cases.
+ int oneHourInSec = hourMinuteToSec(1, 0);
+ assertOverlappedIntervalScore(2 * oneHourInSec, true,
+ programTimeToday2100_2400, programTimeToday2200_0100);
+ assertOverlappedIntervalScore(0, false,
+ programTimeToday2100_2400, programTimeTomorrow0000_0300);
+ assertOverlappedIntervalScore(2 * oneHourInSec, false,
+ programTimeToday2100_2400, programTimeTomorrow2000_2300);
+ assertOverlappedIntervalScore(oneHourInSec, true,
+ programTimeToday2200_0100, programTimeTomorrow0000_0300);
+ assertOverlappedIntervalScore(oneHourInSec, false,
+ programTimeToday2200_0100, programTimeTomorrow2000_2300);
+ assertOverlappedIntervalScore(0, false,
+ programTimeTomorrow0000_0300, programTimeTomorrow2000_2300);
+ }
+
+ public void testGetTimeOfDayInSec() {
+ // Time was set as 00:00:00. So, getTimeOfDay must returns 0 (= 0 * 60 * 60 + 0 * 60 + 0).
+ assertEquals("TimeOfDayInSec", hourMinuteToSec(0, 0),
+ RoutineWatchEvaluator.getTimeOfDayInSec(todayAtHourMin(0, 0)));
+
+ // Time was set as 23:59:59. So, getTimeOfDay must returns 23 * 60 + 60 + 59 * 60 + 59.
+ assertEquals("TimeOfDayInSec", hourMinuteSecondToSec(23, 59, 59),
+ RoutineWatchEvaluator.getTimeOfDayInSec(todayAtHourMinSec(23, 59, 59)));
+ }
+
+ private void assertSplitTextToWords(String text, String... words) {
+ List<String> wordList = RoutineWatchEvaluator.splitTextToWords(text);
+ MoreAsserts.assertContentsInOrder(wordList, words);
+ }
+
+ private void assertMaximumMatchedWordSequenceLength(int expectedLength,
+ String text1, String text2) {
+ List<String> wordList1 = RoutineWatchEvaluator.splitTextToWords(text1);
+ List<String> wordList2 = RoutineWatchEvaluator.splitTextToWords(text2);
+ assertEquals("MaximumMatchedWordSequenceLength", expectedLength,
+ mEvaluator.calculateMaximumMatchedWordSequenceLength(wordList1, wordList2));
+ assertEquals("MaximumMatchedWordSequenceLength", expectedLength,
+ mEvaluator.calculateMaximumMatchedWordSequenceLength(wordList2, wordList1));
+ }
+
+ private void assertProgramTime(int expectedWeekDay, int expectedStartTimeOfDayInSec,
+ int expectedEndTimeOfDayInSec, ProgramTime actualProgramTime) {
+ assertEquals("Weekday", expectedWeekDay, actualProgramTime.weekDay);
+ assertEquals("StartTimeOfDayInSec",
+ expectedStartTimeOfDayInSec, actualProgramTime.startTimeOfDayInSec);
+ assertEquals("EndTimeOfDayInSec",
+ expectedEndTimeOfDayInSec, actualProgramTime.endTimeOfDayInSec);
+ }
+
+ private void assertOverlappedIntervalScore(int expectedSeconds, boolean overlappedOnSameDay,
+ ProgramTime t1, ProgramTime t2) {
+ double score = (double) expectedSeconds;
+ if (!overlappedOnSameDay) {
+ score *= RoutineWatchEvaluator.MULTIPLIER_FOR_UNMATCHED_DAY_OF_WEEK;
+ }
+ // Two tests for testing commutative law.
+ assertEquals("OverlappedIntervalScore",
+ score, mEvaluator.calculateOverlappedIntervalScore(t1, t2));
+ assertEquals("OverlappedIntervalScore",
+ score, mEvaluator.calculateOverlappedIntervalScore(t2, t1));
+ }
+
+ private int hourMinuteToSec(int hour, int minute) {
+ return hourMinuteSecondToSec(hour, minute, 0);
+ }
+
+ private int hourMinuteSecondToSec(int hour, int minute, int second) {
+ return hour * 60 * 60 + minute * 60 + second;
+ }
+
+ private Calendar todayAtHourMin(int hour, int minute) {
+ return todayAtHourMinSec(hour, minute, 0);
+ }
+
+ private Calendar todayAtHourMinSec(int hour, int minute, int second) {
+ Calendar time = Calendar.getInstance();
+ time.set(Calendar.HOUR_OF_DAY, hour);
+ time.set(Calendar.MINUTE, minute);
+ time.set(Calendar.SECOND, second);
+ return time;
+ }
+
+ private Calendar tomorrowAtHourMin(int hour, int minute) {
+ Calendar time = todayAtHourMin(hour, minute);
+ time.add(Calendar.DATE, 1);
+ return time;
+ }
+
+ private Program createDummyProgram(Calendar startTime, long programDurationMs) {
+ long startTimeMs = startTime.getTimeInMillis();
+
+ return new Program.Builder()
+ .setStartTimeUtcMillis(startTimeMs)
+ .setEndTimeUtcMillis(startTimeMs + programDurationMs)
+ .build();
+ }
+}
diff --git a/tests/src/com/android/tv/tests/TvActivityTest.java b/tests/unit/src/com/android/tv/tests/TvActivityTest.java
index 4b918826..92998ab3 100644
--- a/tests/src/com/android/tv/tests/TvActivityTest.java
+++ b/tests/unit/src/com/android/tv/tests/TvActivityTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014 The Android Open Source Project
+ * 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.
@@ -17,9 +17,11 @@
package com.android.tv.tests;
import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.MediumTest;
import com.android.tv.TvActivity;
+@MediumTest
public class TvActivityTest extends ActivityInstrumentationTestCase2<TvActivity> {
public TvActivityTest() {
diff --git a/tests/unit/src/com/android/tv/ui/SetupViewTest.java b/tests/unit/src/com/android/tv/ui/SetupViewTest.java
new file mode 100644
index 00000000..ce3b79fb
--- /dev/null
+++ b/tests/unit/src/com/android/tv/ui/SetupViewTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.ui;
+
+import android.content.pm.ResolveInfo;
+import android.media.tv.TvInputInfo;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.tv.testing.ComparatorTester;
+import com.android.tv.util.SetupUtils;
+import com.android.tv.util.TestUtils;
+import com.android.tv.util.TvInputManagerHelper;
+
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Test for {@link SetupView}
+ */
+@SmallTest
+public class SetupViewTest extends AndroidTestCase {
+ public void testComparator() throws Exception {
+ final LinkedHashMap<String, Boolean> INPUT_ID_TO_NEW_INPUT = new LinkedHashMap<>();
+ INPUT_ID_TO_NEW_INPUT.put("2_new_input", true);
+ INPUT_ID_TO_NEW_INPUT.put("4_new_input", true);
+ INPUT_ID_TO_NEW_INPUT.put("0_old_input", false);
+ INPUT_ID_TO_NEW_INPUT.put("1_old_input", false);
+ INPUT_ID_TO_NEW_INPUT.put("3_old_input", false);
+
+ SetupUtils setupUtils = Mockito.mock(SetupUtils.class);
+ Mockito.when(setupUtils.isNewInput(Matchers.anyString())).thenAnswer(
+ new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ String inputId = (String) invocation.getArguments()[0];
+ return INPUT_ID_TO_NEW_INPUT.get(inputId);
+ }
+ }
+ );
+ TvInputManagerHelper inputManager = Mockito.mock(TvInputManagerHelper.class);
+ Mockito.when(inputManager.getDefaultTvInputInfoComparator()).thenReturn(
+ new Comparator<TvInputInfo>() {
+ @Override
+ public int compare(TvInputInfo lhs, TvInputInfo rhs) {
+ return lhs.getId().compareTo(rhs.getId());
+ }
+ }
+ );
+ SetupView.TvInputInfoComparator comparator =
+ new SetupView.TvInputInfoComparator(setupUtils, inputManager);
+ ComparatorTester<TvInputInfo> comparatorTester =
+ ComparatorTester.withoutEqualsTest(comparator);
+ ResolveInfo resolveInfo = TestUtils.createResolveInfo("test", "test");
+ for (String id : INPUT_ID_TO_NEW_INPUT.keySet()) {
+ // Put mock resolveInfo to prevent NPE in {@link TvInputInfo#toString}
+ TvInputInfo info1 = TestUtils.createTvInputInfo(
+ resolveInfo, id, "test1", TvInputInfo.TYPE_TUNER, false);
+ TvInputInfo info2 = TestUtils.createTvInputInfo(
+ resolveInfo, id, "test2", TvInputInfo.TYPE_DISPLAY_PORT, true);
+ TvInputInfo info3 = TestUtils.createTvInputInfo(
+ resolveInfo, id, "test", TvInputInfo.TYPE_HDMI, true);
+ comparatorTester.addComparableGroup(info1, info2, info3);
+ }
+ comparatorTester.test();
+ }
+}
diff --git a/tests/unit/src/com/android/tv/util/FakeClock.java b/tests/unit/src/com/android/tv/util/FakeClock.java
new file mode 100644
index 00000000..a4ab2e4f
--- /dev/null
+++ b/tests/unit/src/com/android/tv/util/FakeClock.java
@@ -0,0 +1,34 @@
+/*
+ * 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.util;
+
+public class FakeClock implements Clock {
+ private long mCurrentTimeMillis;
+
+ public FakeClock() {
+ mCurrentTimeMillis = System.currentTimeMillis();
+ }
+
+ public void setCurrentTimeMillis(long time) {
+ mCurrentTimeMillis = time;
+ }
+
+ @Override
+ public long currentTimeMillis() {
+ return mCurrentTimeMillis;
+ }
+}
diff --git a/tests/unit/src/com/android/tv/util/ImageCacheTest.java b/tests/unit/src/com/android/tv/util/ImageCacheTest.java
new file mode 100644
index 00000000..a73b79fe
--- /dev/null
+++ b/tests/unit/src/com/android/tv/util/ImageCacheTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.util;
+
+import static com.android.tv.util.BitmapUtils.createScaledBitmapInfo;
+
+import android.graphics.Bitmap;
+
+import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link ImageCache}.
+ */
+public class ImageCacheTest extends TestCase {
+
+ private static final Bitmap ORIG = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565);
+
+ private static final String KEY = "same";
+ private static final ScaledBitmapInfo INFO_200 = createScaledBitmapInfo(KEY, ORIG, 200, 200);
+ private static final ScaledBitmapInfo INFO_100 = createScaledBitmapInfo(KEY, ORIG, 100, 100);
+ private static final ScaledBitmapInfo INFO_50 = createScaledBitmapInfo(KEY, ORIG, 50, 50);
+ private static final ScaledBitmapInfo INFO_25 = createScaledBitmapInfo(KEY, ORIG, 25, 25);
+
+ private ImageCache mImageCache;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mImageCache = ImageCache.newInstance(0.1f);
+ }
+
+ //TODO: Empty the cache in the setup. Try using @VisibleForTesting
+
+ public void testPutIfLarger_smaller() throws Exception {
+
+ mImageCache.putIfNeeded( INFO_50);
+ assertSame("before", INFO_50, mImageCache.get(KEY));
+
+ mImageCache.putIfNeeded( INFO_25);
+ assertSame("after", INFO_50, mImageCache.get(KEY));
+ }
+
+ public void testPutIfLarger_larger() throws Exception {
+ mImageCache.putIfNeeded( INFO_50);
+ assertSame("before", INFO_50, mImageCache.get(KEY));
+
+ mImageCache.putIfNeeded(INFO_100);
+ assertSame("after", INFO_100, mImageCache.get(KEY));
+ }
+
+ public void testPutIfLarger_alreadyMax() throws Exception {
+
+ mImageCache.putIfNeeded( INFO_100);
+ assertSame("before", INFO_100, mImageCache.get(KEY));
+
+ mImageCache.putIfNeeded( INFO_200);
+ assertSame("after", INFO_100, mImageCache.get(KEY));
+ }
+}
diff --git a/tests/unit/src/com/android/tv/util/ScaledBitmapInfoTest.java b/tests/unit/src/com/android/tv/util/ScaledBitmapInfoTest.java
new file mode 100644
index 00000000..ef707470
--- /dev/null
+++ b/tests/unit/src/com/android/tv/util/ScaledBitmapInfoTest.java
@@ -0,0 +1,52 @@
+package com.android.tv.util;
+
+import android.graphics.Bitmap;
+import android.test.AndroidTestCase;
+
+import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
+
+/**
+ * Tests for {@link ScaledBitmapInfo}.
+ */
+public class ScaledBitmapInfoTest extends AndroidTestCase {
+
+ private static final Bitmap B80x100 = Bitmap.createBitmap(80, 100, Bitmap.Config.RGB_565);
+ private static final Bitmap B960x1440 = Bitmap.createBitmap(960, 1440, Bitmap.Config.RGB_565);
+
+ public void testSize_B100x100to50x50() {
+ ScaledBitmapInfo actual = BitmapUtils.createScaledBitmapInfo("B80x100", B80x100, 50, 50);
+ assertScaledBitmapSize(2, 40, 50, actual);
+ }
+
+ public void testNeedsToReload_B100x100to50x50() {
+ ScaledBitmapInfo actual = BitmapUtils.createScaledBitmapInfo("B80x100", B80x100, 50, 50);
+ assertNeedsToReload(false, actual, 25, 25);
+ assertNeedsToReload(false, actual, 50, 50);
+ assertNeedsToReload(false, actual, 99, 99);
+ assertNeedsToReload(true, actual, 100, 100);
+ assertNeedsToReload(true, actual, 101, 101);
+ }
+
+ /**
+ * Reproduces <a href="http://b/20488453">b/20488453</a>.
+ */
+ public void testBug20488453() {
+ ScaledBitmapInfo actual = BitmapUtils
+ .createScaledBitmapInfo("B960x1440", B960x1440, 284, 160);
+ assertScaledBitmapSize(8, 107, 160, actual);
+ assertNeedsToReload(false, actual, 284, 160);
+ }
+
+ private static void assertNeedsToReload(boolean expected, ScaledBitmapInfo scaledBitmap,
+ int reqWidth, int reqHeight) {
+ assertEquals(scaledBitmap.id + " needToReload(" + reqWidth + "," + reqHeight + ")",
+ expected, scaledBitmap.needToReload(reqWidth, reqHeight));
+ }
+
+ private static void assertScaledBitmapSize(int expectedInSampleSize, int expectedWidth,
+ int expectedHeight, ScaledBitmapInfo actual) {
+ assertEquals(actual.id + " inSampleSize", expectedInSampleSize, actual.inSampleSize);
+ assertEquals(actual.id + " width", expectedWidth, actual.bitmap.getWidth());
+ assertEquals(actual.id + " height", expectedHeight, actual.bitmap.getHeight());
+ }
+}
diff --git a/tests/unit/src/com/android/tv/util/TestUtils.java b/tests/unit/src/com/android/tv/util/TestUtils.java
new file mode 100644
index 00000000..872e8c51
--- /dev/null
+++ b/tests/unit/src/com/android/tv/util/TestUtils.java
@@ -0,0 +1,65 @@
+/*
+ * 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.util;
+
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.media.tv.TvInputInfo;
+
+import com.android.tv.common.TvCommonConstants;
+
+import java.lang.reflect.Constructor;
+
+/**
+ * A class that includes convenience methods for testing.
+ */
+public class TestUtils {
+ public static TvInputInfo createTvInputInfo(ResolveInfo service, String id, String parentId,
+ int type, boolean isHardwareInput) throws Exception {
+ // Create a mock TvInputInfo by using private constructor
+ // TODO: Find better way to mock TvInputInfo.
+ // Note that mockito doesn't support mock/spy on final object.
+ if (!TvCommonConstants.IS_MNC_PREVIEW && !TvCommonConstants.IS_MNC_OR_HIGHER) {
+ return createTvInputInfoForLmp(service, id, parentId, type);
+ }
+ return createTvInputInfoForMnc(service, id, parentId, type, isHardwareInput);
+ }
+
+ private static TvInputInfo createTvInputInfoForLmp(ResolveInfo service, String id,
+ String parentId, int type) throws Exception {
+ Constructor<TvInputInfo> constructor = TvInputInfo.class.getDeclaredConstructor(new Class[]{
+ ResolveInfo.class, String.class, String.class, int.class});
+ constructor.setAccessible(true);
+ return constructor.newInstance(service, id, parentId, type);
+ }
+
+ private static TvInputInfo createTvInputInfoForMnc(ResolveInfo service, String id,
+ String parentId, int type, boolean isHardwareInput) throws Exception {
+ Constructor<TvInputInfo> constructor = TvInputInfo.class.getDeclaredConstructor(new Class[]{
+ ResolveInfo.class, String.class, String.class, int.class, boolean.class});
+ constructor.setAccessible(true);
+ return constructor.newInstance(service, id, parentId, type, isHardwareInput);
+ }
+
+ public static ResolveInfo createResolveInfo(String packageName, String name) {
+ ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.serviceInfo = new ServiceInfo();
+ resolveInfo.serviceInfo.packageName = packageName;
+ resolveInfo.serviceInfo.name = name;
+ return resolveInfo;
+ }
+}
diff --git a/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java b/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java
new file mode 100644
index 00000000..6251d1a3
--- /dev/null
+++ b/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.util;
+
+import android.content.pm.ResolveInfo;
+import android.media.tv.TvInputInfo;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.tv.testing.ComparatorTester;
+
+import java.util.LinkedHashMap;
+
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Test for {@link TvInputManagerHelper}
+ */
+@SmallTest
+public class TvInputManagerHelperTest extends AndroidTestCase {
+ public void testComparator() throws Exception {
+ final LinkedHashMap<String, Boolean> INPUT_ID_TO_PARTNER_INPUT = new LinkedHashMap<>();
+ INPUT_ID_TO_PARTNER_INPUT.put("2_partner_input", true);
+ INPUT_ID_TO_PARTNER_INPUT.put("3_partner_input", true);
+ INPUT_ID_TO_PARTNER_INPUT.put("1_3rd_party_input", false);
+ INPUT_ID_TO_PARTNER_INPUT.put("4_3rd_party_input", false);
+
+ TvInputManagerHelper manager = Mockito.mock(TvInputManagerHelper.class);
+ Mockito.doAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ TvInputInfo info = (TvInputInfo) invocation.getArguments()[0];
+ return INPUT_ID_TO_PARTNER_INPUT.get(info.getId());
+ }
+ }).when(manager).isPartnerInput(Mockito.<TvInputInfo>any());
+ Mockito.doAnswer(new Answer<String>() {
+ @Override
+ public String answer(InvocationOnMock invocation) throws Throwable {
+ TvInputInfo info = (TvInputInfo) invocation.getArguments()[0];
+ return info.getId();
+ }
+ }).when(manager).loadLabel(Mockito.<TvInputInfo>any());
+
+ ComparatorTester<TvInputInfo> comparatorTester =
+ ComparatorTester.withoutEqualsTest(
+ new TvInputManagerHelper.TvInputInfoComparator(manager));
+ ResolveInfo resolveInfo1 = TestUtils.createResolveInfo("1_test", "1_test");
+ ResolveInfo resolveInfo2 = TestUtils.createResolveInfo("2_test", "2_test");
+ for (String inputId : INPUT_ID_TO_PARTNER_INPUT.keySet()) {
+ TvInputInfo info1 = TestUtils.createTvInputInfo(resolveInfo1, inputId, null, 0, false);
+ TvInputInfo info2 = TestUtils.createTvInputInfo(resolveInfo2, inputId, null, 0, false);
+ comparatorTester.addComparableGroup(info1, info2);
+ }
+ comparatorTester.test();
+ }
+}
diff --git a/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java b/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java
new file mode 100644
index 00000000..b657f49c
--- /dev/null
+++ b/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.util;
+
+import static com.android.tv.util.TvTrackInfoUtils.getBestTrackInfo;
+
+import android.media.tv.TvTrackInfo;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.tv.testing.ComparatorTester;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Tests for {@link com.android.tv.util.TvTrackInfoUtils}.
+ */
+@SmallTest
+public class TvTrackInfoUtilsTest extends TestCase {
+
+ private static final String UN_MATCHED_ID = "no matching ID";
+
+ private static final TvTrackInfo INFO_1_EN_1 = create("1", "en", 1);
+
+ private static final TvTrackInfo INFO_2_EN_5 = create("2", "en", 5);
+
+ private static final TvTrackInfo INFO_3_FR_5 = create("3", "fr", 5);
+
+ private static TvTrackInfo create(String id, String fr, int audioChannelCount) {
+ return new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, id)
+ .setLanguage(fr)
+ .setAudioChannelCount(audioChannelCount)
+ .build();
+ }
+
+ private static final List<TvTrackInfo> ALL = Arrays.asList(INFO_1_EN_1, INFO_2_EN_5, INFO_3_FR_5);
+
+ public void testGetBestTrackInfo_empty() {
+ TvTrackInfo result = getBestTrackInfo(Collections.<TvTrackInfo>emptyList(),
+ UN_MATCHED_ID, "en", 1);
+ assertEquals("best track ", null, result);
+ }
+
+ public void testGetBestTrackInfo_exactMatch() {
+ TvTrackInfo result = getBestTrackInfo(ALL, "1", "en", 1);
+ assertEquals("best track ", INFO_1_EN_1, result);
+ }
+
+ public void testGetBestTrackInfo_langAndChannelCountMatch() {
+ TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "en", 5);
+ assertEquals("best track ", INFO_2_EN_5, result);
+ }
+
+ public void testGetBestTrackInfo_languageOnlyMatch() {
+ TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "fr", 1);
+ assertEquals("best track ", INFO_3_FR_5, result);
+ }
+
+ public void testGetBestTrackInfo_noMatches() {
+ TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "kr", 1);
+ assertEquals("best track ", INFO_1_EN_1, result);
+ }
+
+
+ public void testComparator() {
+ Comparator<TvTrackInfo> comparator = TvTrackInfoUtils.createComparator("1", "en", 1);
+ ComparatorTester.withoutEqualsTest(comparator)
+ // lang not match
+ .addComparableGroup(create("1", "kr", 1), create("2", "kr", 2),
+ create("1", "ja", 1),
+ create("1", "ch", 1))
+ // lang match not count match
+ .addComparableGroup(create("2", "en", 2), create("3", "en", 3),
+ create("1", "en", 2))
+ // lang and count match
+ .addComparableGroup(create("2", "en", 1), create("3", "en", 1))
+ // all match
+ .addComparableGroup(create("1", "en", 1), create("1", "en", 1))
+ .test();
+ }
+}
diff --git a/tests/unit/src/com/android/tv/util/UtilsTest_GetDurationString.java b/tests/unit/src/com/android/tv/util/UtilsTest_GetDurationString.java
new file mode 100644
index 00000000..1cdda744
--- /dev/null
+++ b/tests/unit/src/com/android/tv/util/UtilsTest_GetDurationString.java
@@ -0,0 +1,250 @@
+/*
+ * 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.util;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import android.text.format.DateUtils;
+
+import java.lang.Exception;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+
+/**
+ * Tests for {@link com.android.tv.util.Utils#getDurationString}.
+ * <p/>
+ * This test uses deprecated flags {@link DateUtils#FORMAT_12HOUR} and
+ * {@link DateUtils#FORMAT_24HOUR} to run this test independent to system's 12/24h format.
+ * Note that changing system setting requires permission android.permission.WRITE_SETTINGS
+ * and it should be defined in TV app, not this test.
+ */
+@SmallTest
+public class UtilsTest_GetDurationString extends AndroidTestCase {
+ // TODO: Mock Context so we can specify current time and locale for test.
+ private Locale mLocale;
+ private static final long DATE_2015_2_1_MS = getFeb2015InMillis(1, 0, 0);
+
+ // All possible list for a paramter to test parameter independent result.
+ private static final boolean[] PARAM_USE_SHORT_FORMAT = {false, true};
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ // Set locale to US
+ mLocale = Locale.getDefault();
+ Locale.setDefault(Locale.US);
+ }
+
+ /**
+ * Return time in millis assuming that whose year is 2015 and month is Jan.
+ */
+ private static long getJan2015InMillis(int date, int hour, int minutes) {
+ return new GregorianCalendar(
+ 2015, Calendar.JANUARY, date, hour, minutes).getTimeInMillis();
+ }
+
+ private static long getJan2015InMillis(int date, int hour) {
+ return getJan2015InMillis(date, hour, 0);
+ }
+
+ /**
+ * Return time in millis assuming that whose year is 2015 and month is Feb.
+ */
+ private static long getFeb2015InMillis(int date, int hour, int minutes) {
+ return new GregorianCalendar(
+ 2015, Calendar.FEBRUARY, date, hour, minutes).getTimeInMillis();
+ }
+
+ private static long getFeb2015InMillis(int date, int hour) {
+ return getFeb2015InMillis(date, hour, 0);
+ }
+
+ public void testSameDateAndTime() {
+ assertEquals("3:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(1, 3), getFeb2015InMillis(1, 3), false,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("03:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(1, 3), getFeb2015InMillis(1, 3), false,
+ DateUtils.FORMAT_24HOUR));
+ }
+
+ public void testDurationWithinToday() {
+ assertEquals("12:00 – 3:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ DATE_2015_2_1_MS, getFeb2015InMillis(1, 3), false,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("00:00 – 03:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ DATE_2015_2_1_MS, getFeb2015InMillis(1, 3), false,
+ DateUtils.FORMAT_24HOUR));
+ }
+
+ public void testDurationFromYesterdayToToday() {
+ assertEquals("Jan 31, 3:00 AM – Feb 1, 4:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getJan2015InMillis(31, 3), getFeb2015InMillis(1, 4), false,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("Jan 31, 03:00 – Feb 1, 04:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getJan2015InMillis(31, 3), getFeb2015InMillis(1, 4), false,
+ DateUtils.FORMAT_24HOUR));
+ assertEquals("1/31, 11:30 PM – 12:30 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getJan2015InMillis(31, 23, 30), getFeb2015InMillis(1, 0, 30), true,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("1/31, 23:30 – 00:30",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getJan2015InMillis(31, 23, 30), getFeb2015InMillis(1, 0, 30), true,
+ DateUtils.FORMAT_24HOUR));
+ }
+
+ public void testDurationFromTodayToTomorrow() {
+ assertEquals("Feb 1, 3:00 AM – Feb 2, 4:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(1, 3), getFeb2015InMillis(2, 4), false,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("Feb 1, 03:00 – Feb 2, 04:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(1, 3), getFeb2015InMillis(2, 4), false,
+ DateUtils.FORMAT_24HOUR));
+ assertEquals("2/1, 3:00 AM – 2/2, 4:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(1, 3), getFeb2015InMillis(2, 4), true,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("2/1, 03:00 – 2/2, 04:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(1, 3), getFeb2015InMillis(2, 4), true,
+ DateUtils.FORMAT_24HOUR));
+
+ assertEquals("Feb 1, 11:30 PM – Feb 2, 12:30 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(1, 23, 30), getFeb2015InMillis(2, 0, 30), false,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("Feb 1, 23:30 – Feb 2, 00:30",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(1, 23, 30), getFeb2015InMillis(2, 0, 30), false,
+ DateUtils.FORMAT_24HOUR));
+ assertEquals("11:30 PM – 12:30 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(1, 23, 30), getFeb2015InMillis(2, 0, 30), true,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("23:30 – 00:30",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(1, 23, 30), getFeb2015InMillis(2, 0, 30), true,
+ DateUtils.FORMAT_24HOUR));
+ }
+
+ public void testDurationWithinTomorrow() {
+ assertEquals("Feb 2, 2:00 – 4:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(2, 2), getFeb2015InMillis(2, 4), false,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("Feb 2, 02:00 – 04:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(2, 2), getFeb2015InMillis(2, 4), false,
+ DateUtils.FORMAT_24HOUR));
+ assertEquals("2/2, 2:00 – 4:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(2, 2), getFeb2015InMillis(2, 4), true,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("2/2, 02:00 – 04:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(2, 2), getFeb2015InMillis(2, 4), true,
+ DateUtils.FORMAT_24HOUR));
+ }
+
+ public void testStartOfDay() {
+ assertEquals("12:00 – 1:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ DATE_2015_2_1_MS, getFeb2015InMillis(1, 1), false,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("00:00 – 01:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ DATE_2015_2_1_MS, getFeb2015InMillis(1, 1), false,
+ DateUtils.FORMAT_24HOUR));
+
+ assertEquals("Feb 2, 12:00 – 1:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(2, 0), getFeb2015InMillis(2, 1), false,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("Feb 2, 00:00 – 01:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(2, 0), getFeb2015InMillis(2, 1), false,
+ DateUtils.FORMAT_24HOUR));
+ assertEquals("2/2, 12:00 – 1:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(2, 0), getFeb2015InMillis(2, 1), true,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("2/2, 00:00 – 01:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(2, 0), getFeb2015InMillis(2, 1), true,
+ DateUtils.FORMAT_24HOUR));
+ }
+
+ public void testEndOfDay() {
+ for (boolean useShortFormat : PARAM_USE_SHORT_FORMAT) {
+ assertEquals("11:00 PM – 12:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(1, 23), getFeb2015InMillis(2, 0), useShortFormat,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("23:00 – 00:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(1, 23), getFeb2015InMillis(2, 0), useShortFormat,
+ DateUtils.FORMAT_24HOUR));
+ }
+
+ assertEquals("Feb 2, 11:00 PM – 12:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(2, 23), getFeb2015InMillis(3, 0), false,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("Feb 2, 23:00 – 00:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(2, 23), getFeb2015InMillis(3, 0), false,
+ DateUtils.FORMAT_24HOUR));
+ assertEquals("2/2, 11:00 PM – 12:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(2, 23), getFeb2015InMillis(3, 0), true,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("2/2, 23:00 – 00:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ getFeb2015InMillis(2, 23), getFeb2015InMillis(3, 0), true,
+ DateUtils.FORMAT_24HOUR));
+ }
+
+ public void testMidnight() {
+ for (boolean useShortFormat : PARAM_USE_SHORT_FORMAT) {
+ assertEquals("12:00 AM",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ DATE_2015_2_1_MS, DATE_2015_2_1_MS, useShortFormat,
+ DateUtils.FORMAT_12HOUR));
+ assertEquals("00:00",
+ Utils.getDurationString(getContext(), DATE_2015_2_1_MS,
+ DATE_2015_2_1_MS, DATE_2015_2_1_MS, useShortFormat,
+ DateUtils.FORMAT_24HOUR));
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ // Revive system locale.
+ Locale.setDefault(mLocale);
+ }
+}
diff --git a/tests/unit/src/com/android/tv/util/UtilsTest_IsInGivenDay.java b/tests/unit/src/com/android/tv/util/UtilsTest_IsInGivenDay.java
new file mode 100644
index 00000000..160a2231
--- /dev/null
+++ b/tests/unit/src/com/android/tv/util/UtilsTest_IsInGivenDay.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.util;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+/**
+ * Tests for {@link com.android.tv.util.Utils#isInGivenDay}.
+ */
+@SmallTest
+public class UtilsTest_IsInGivenDay extends AndroidTestCase {
+ public void testIsInGivenDay() {
+ assertTrue(Utils.isInGivenDay(
+ new GregorianCalendar(2015, Calendar.JANUARY, 1).getTimeInMillis(),
+ new GregorianCalendar(2015, Calendar.JANUARY, 1, 0, 30).getTimeInMillis()));
+ }
+
+ public void testIsNotInGivenDay() {
+ assertFalse(Utils.isInGivenDay(
+ new GregorianCalendar(2015, Calendar.JANUARY, 1).getTimeInMillis(),
+ new GregorianCalendar(2015, Calendar.JANUARY, 2).getTimeInMillis()));
+ }
+
+ public void testIfTimeZoneApplied() {
+ TimeZone timeZone = TimeZone.getDefault();
+
+ TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
+
+ // 2015.01.01 00:00 in KST = 2014.12.31 15:00 in UTC
+ long date2015StartMs =
+ new GregorianCalendar(2015, Calendar.JANUARY, 1).getTimeInMillis();
+
+ // 2015.01.01 10:00 in KST = 2015.01.01 01:00 in UTC
+ long date2015Start10AMMs =
+ new GregorianCalendar(2015, Calendar.JANUARY, 1, 10, 0).getTimeInMillis();
+
+ // Those two times aren't in the same day in UTC, but they are in KST.
+ assertTrue(Utils.isInGivenDay(date2015StartMs, date2015Start10AMMs));
+
+ TimeZone.setDefault(timeZone);
+ }
+}