diff options
Diffstat (limited to 'tests/common/src/com/android/tv/testing/data')
4 files changed, 1024 insertions, 0 deletions
diff --git a/tests/common/src/com/android/tv/testing/data/ChannelInfo.java b/tests/common/src/com/android/tv/testing/data/ChannelInfo.java new file mode 100644 index 00000000..e39c057d --- /dev/null +++ b/tests/common/src/com/android/tv/testing/data/ChannelInfo.java @@ -0,0 +1,352 @@ +/* + * 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.data; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.net.Uri; +import android.support.annotation.Nullable; +import android.util.SparseArray; +import java.util.Objects; + +/** 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); + } + + 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 float videoPixelAspectRatio; + 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) { + // tests/input/tools/get_test_logos.sh only stores 1000 logos. + builder.setLogoUrl(getUriStringForChannelLogo(context, channelNumber)); + } + return builder.build(); + } + + public static String getUriStringForChannelLogo(Context context, int logoIndex) { + int index = (logoIndex % 1000) + 1; + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(context.getPackageName()) + .path("drawable") + .appendPath("ch_" + index + "_logo") + .build() + .toString(); + } + + 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, + float videoPixelAspectRatio, + 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.videoPixelAspectRatio = videoPixelAspectRatio; + 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 + + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChannelInfo that = (ChannelInfo) o; + return Objects.equals(originalNetworkId, that.originalNetworkId) + && Objects.equals(videoWidth, that.videoWidth) + && Objects.equals(videoHeight, that.videoHeight) + && Objects.equals(audioChannel, that.audioChannel) + && Objects.equals(audioLanguageCount, that.audioLanguageCount) + && Objects.equals(hasClosedCaption, that.hasClosedCaption) + && Objects.equals(appLinkColor, that.appLinkColor) + && Objects.equals(number, that.number) + && Objects.equals(name, that.name) + && Objects.equals(logoUrl, that.logoUrl) + && Objects.equals(program, that.program) + && Objects.equals(appLinkText, that.appLinkText) + && Objects.equals(appLinkIconUri, that.appLinkIconUri) + && Objects.equals(appLinkPosterArtUri, that.appLinkPosterArtUri) + && Objects.equals(appLinkIntentUri, that.appLinkIntentUri); + } + + @Override + public int hashCode() { + return Objects.hash(number, name, originalNetworkId); + } + + /** 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 float mVideoPixelAspectRatio = 1.0f; // default value + 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; + mVideoPixelAspectRatio = other.videoPixelAspectRatio; + 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 setVideoPixelAspectRatio(float videoPixelAspectRatio) { + mVideoPixelAspectRatio = videoPixelAspectRatio; + 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, + mVideoPixelAspectRatio, + mAudioChannel, + mAudioLanguageCount, + mHasClosedCaption, + mProgram, + mAppLinkText, + mAppLinkColor, + mAppLinkIconUri, + mAppLinkPosterArtUri, + mAppLinkIntentUri); + } + } +} diff --git a/tests/common/src/com/android/tv/testing/data/ChannelUtils.java b/tests/common/src/com/android/tv/testing/data/ChannelUtils.java new file mode 100644 index 00000000..920c7087 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/data/ChannelUtils.java @@ -0,0 +1,189 @@ +/* + * 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.data; + +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.support.annotation.WorkerThread; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +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"; + private static final boolean DEBUG = false; + + /** + * Query and return the map of (channel_id, ChannelInfo). See: {@link + * com.android.tv.testing.data.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; + System.arraycopy(ChannelInfo.PROJECTION, 0, projections, 1, ChannelInfo.PROJECTION.length); + 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 (!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) { + if (DEBUG) { + Log.d(TAG, "Inserting " + channel); + } + uri = resolver.insert(TvContract.Channels.CONTENT_URI, values); + } else { + if (DEBUG) { + Log.d(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 List<ChannelInfo> createChannelInfos(Context context, int channelCount) { + List<ChannelInfo> channels = new ArrayList<>(); + for (int i = 1; i <= channelCount; i++) { + channels.add(ChannelInfo.create(context, i)); + } + return channels; + } + + 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/data/ProgramInfo.java b/tests/common/src/com/android/tv/testing/data/ProgramInfo.java new file mode 100644 index 00000000..6d801425 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/data/ProgramInfo.java @@ -0,0 +1,336 @@ +/* + * 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.data; + +import android.content.Context; +import android.database.Cursor; +import android.media.tv.TvContentRating; +import android.media.tv.TvContract; +import com.android.tv.testing.R; +import com.android.tv.testing.utils.Utils; +import java.util.Arrays; +import java.util.Objects; +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 durationsSumMs; + + static { + durationsSumMs = 0; + for (long duration : DURATIONS_MS) { + durationsSumMs += duration; + } + } + + /** 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) / durationsSumMs) * DURATIONS_MS.length; + startTimeMs += (index / DURATIONS_MS.length) * durationsSumMs; + 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) * durationsSumMs; + 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) + && episode == null + && !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, + episode != null ? (index % SEASON_MAX + 1) : seasonNumber, + episode != null ? (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 + + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProgramInfo that = (ProgramInfo) o; + return Objects.equals(seasonNumber, that.seasonNumber) + && Objects.equals(episodeNumber, that.episodeNumber) + && Objects.equals(durationMs, that.durationMs) + && Objects.equals(title, that.title) + && Objects.equals(episode, that.episode) + && Objects.equals(posterArtUri, that.posterArtUri) + && Objects.equals(description, that.description) + && Objects.equals(genre, that.genre) + && Arrays.equals(contentRatings, that.contentRatings) + && Objects.equals(resourceUri, that.resourceUri); + } + + @Override + public int hashCode() { + return Objects.hash(title, episode, seasonNumber, episodeNumber); + } + + 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/data/ProgramUtils.java b/tests/common/src/com/android/tv/testing/data/ProgramUtils.java new file mode 100644 index 00000000..21647719 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/data/ProgramUtils.java @@ -0,0 +1,147 @@ +/* + * 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.data; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.media.tv.TvContract; +import android.media.tv.TvContract.Programs; +import android.net.Uri; +import android.util.Log; +import com.android.tv.common.TvContentRatingCache; +import com.android.tv.common.util.Clock; +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** Static utilities for using Programs in tests */ +public final class ProgramUtils { + private static final String TAG = "ProgramUtils"; + private static final boolean DEBUG = false; + + /** Populate program data for a week */ + public 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, Clock clock) { + populatePrograms(context, channelUri, program, clock, PROGRAM_INSERT_DURATION_MS); + } + + public static void populatePrograms( + Context context, + Uri channelUri, + ProgramInfo program, + Clock clock, + long programInsertDurationMs) { + long currentTimeMs = clock.currentTimeMillis(); + long targetEndTimeMs = currentTimeMs + programInsertDurationMs; + populatePrograms(context, channelUri, program, currentTimeMs, targetEndTimeMs); + } + + public static void populatePrograms( + Context context, + Uri channelUri, + ProgramInfo program, + long currentTimeMs, + long targetEndTimeMs) { + 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, + TvContentRatingCache.contentRatingsToString(program.contentRatings)); + + 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); + if (programAt.seasonNumber != 0) { + values.put(Programs.COLUMN_SEASON_NUMBER, programAt.seasonNumber); + } + if (programAt.episodeNumber != 0) { + 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) { + try { + context.getContentResolver() + .bulkInsert( + Programs.CONTENT_URI, + list.toArray(new ContentValues[list.size()])); + } catch (SQLiteException e) { + Log.e(TAG, "Can't insert EPG.", e); + return; + } + if (DEBUG) Log.d(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() {} + + public static void updateProgramForAllChannelsOf( + Context context, String inputId, Clock clock, long durationMs) { + // Reload channels so we have the ids. + Map<Long, ChannelInfo> channelIdToInfoMap = + ChannelUtils.queryChannelInfoMapForTvInput(context, inputId); + for (Long channelId : channelIdToInfoMap.keySet()) { + ProgramInfo programInfo = ProgramInfo.create(); + populatePrograms( + context, TvContract.buildChannelUri(channelId), programInfo, clock, durationMs); + } + } +} |