diff options
author | Nick Chalko <nchalko@google.com> | 2015-08-03 15:39:56 -0700 |
---|---|---|
committer | Nick Chalko <nchalko@google.com> | 2015-08-03 15:53:37 -0700 |
commit | 816a4be1a0f34f6a48877c8afd3dbbca19eac435 (patch) | |
tree | 4f18dda269764494942f5313acc93db4a35d47db /tests | |
parent | 6edd2b09e5d16a29c703a5fcbd2e88c5cf5e55b7 (diff) | |
download | TV-816a4be1a0f34f6a48877c8afd3dbbca19eac435.tar.gz |
Migrate Live Channels App Src to AOSP branch
Bug: 21625152
Change-Id: I07e2830b27440556dc757e6340b4f77d1c0cbc66
Diffstat (limited to 'tests')
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 Binary files differnew file mode 100644 index 00000000..dd2044ca --- /dev/null +++ b/tests/common/res/drawable-xhdpi/blue.png diff --git a/tests/common/res/drawable-xhdpi/blue_small.png b/tests/common/res/drawable-xhdpi/blue_small.png Binary files differnew file mode 100644 index 00000000..22394ebb --- /dev/null +++ b/tests/common/res/drawable-xhdpi/blue_small.png diff --git a/tests/common/res/drawable-xhdpi/crash_test_android_logo.png b/tests/common/res/drawable-xhdpi/crash_test_android_logo.png Binary files differnew file mode 100644 index 00000000..2442cf04 --- /dev/null +++ b/tests/common/res/drawable-xhdpi/crash_test_android_logo.png diff --git a/tests/common/res/drawable-xhdpi/green.png b/tests/common/res/drawable-xhdpi/green.png Binary files differnew file mode 100644 index 00000000..8306b9c3 --- /dev/null +++ b/tests/common/res/drawable-xhdpi/green.png diff --git a/tests/common/res/drawable-xhdpi/green_large.png b/tests/common/res/drawable-xhdpi/green_large.png Binary files differnew file mode 100644 index 00000000..77bbb231 --- /dev/null +++ b/tests/common/res/drawable-xhdpi/green_large.png diff --git a/tests/common/res/drawable-xhdpi/red.png b/tests/common/res/drawable-xhdpi/red.png Binary files differnew file mode 100644 index 00000000..89f889b9 --- /dev/null +++ b/tests/common/res/drawable-xhdpi/red.png diff --git a/tests/common/res/drawable-xhdpi/red_large.png b/tests/common/res/drawable-xhdpi/red_large.png Binary files differnew file mode 100644 index 00000000..c52a1242 --- /dev/null +++ b/tests/common/res/drawable-xhdpi/red_large.png 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 Binary files differnew file mode 100644 index 00000000..9ea1cd14 --- /dev/null +++ b/tests/input/res/drawable-xhdpi/android_48dp.png diff --git a/tests/input/res/drawable-xhdpi/icon.png b/tests/input/res/drawable-xhdpi/icon.png Binary files differnew file mode 100644 index 00000000..8497c28c --- /dev/null +++ b/tests/input/res/drawable-xhdpi/icon.png 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); + } +} |