diff options
Diffstat (limited to 'tests/common/src/com')
27 files changed, 2459 insertions, 0 deletions
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() { + } +} |