blob: 14a9184caf023c05c7fd466b441b402ce2421819 [file] [log] [blame]
/*
* Copyright 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.example.android.sampletvinput;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
import android.media.tv.TvContract.Programs;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.Pair;
import android.util.SparseArray;
import com.example.android.sampletvinput.rich.RichTvInputService.ChannelInfo;
import com.example.android.sampletvinput.rich.RichTvInputService.PlaybackInfo;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
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 TvContractUtils {
private static final String TAG = "TvContractUtils";
private static final boolean DEBUG = true;
private static final SparseArray<String> VIDEO_HEIGHT_TO_FORMAT_MAP =
new SparseArray<String>();
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 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> mExistingChannelsMap = new SparseArray<Long>();
Uri channelsUri = TvContract.buildChannelsUriForInput(inputId);
String[] projection = {Channels._ID, Channels.COLUMN_ORIGINAL_NETWORK_ID};
Cursor cursor = null;
ContentResolver resolver = context.getContentResolver();
try {
cursor = resolver.query(channelsUri, projection, null, null, null);
while (cursor != null && cursor.moveToNext()) {
long rowId = cursor.getLong(0);
int originalNetworkId = cursor.getInt(1);
mExistingChannelsMap.put(originalNetworkId, rowId);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
// If a channel exists, update it. If not, insert a new one.
ContentValues values = new ContentValues();
values.put(Channels.COLUMN_INPUT_ID, inputId);
Map<Uri, String> logos = new HashMap<Uri, String>();
for (ChannelInfo channel : channels) {
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);
values.put(Channels.COLUMN_TRANSPORT_STREAM_ID, channel.transportStreamId);
values.put(Channels.COLUMN_SERVICE_ID, channel.serviceId);
String videoFormat = getVideoFormat(channel.videoHeight);
if (videoFormat != null) {
values.put(Channels.COLUMN_VIDEO_FORMAT, videoFormat);
} else {
values.putNull(Channels.COLUMN_VIDEO_FORMAT);
}
Long rowId = mExistingChannelsMap.get(channel.originalNetworkId);
Uri uri;
if (rowId == null) {
uri = resolver.insert(TvContract.Channels.CONTENT_URI, values);
} else {
uri = TvContract.buildChannelUri(rowId);
resolver.update(uri, values, null, null);
mExistingChannelsMap.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 = mExistingChannelsMap.size();
for(int i = 0; i < size; ++i) {
Long rowId = mExistingChannelsMap.valueAt(i);
resolver.delete(TvContract.buildChannelUri(rowId), null, null);
}
}
public static int getChannelCount(ContentResolver resolver, String inputId) {
Uri uri = TvContract.buildChannelsUriForInput(inputId);
String[] projection = { TvContract.Channels._ID };
Cursor cursor = null;
try {
cursor = resolver.query(uri, projection, null, null, null);
if (cursor != null) {
return cursor.getCount();
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return 0;
}
private static String getVideoFormat(int videoHeight) {
return VIDEO_HEIGHT_TO_FORMAT_MAP.get(videoHeight);
}
public static LongSparseArray<ChannelInfo> buildChannelMap(ContentResolver resolver,
String inputId, List<ChannelInfo> channels) {
Uri uri = TvContract.buildChannelsUriForInput(inputId);
String[] projection = {
TvContract.Channels._ID,
TvContract.Channels.COLUMN_DISPLAY_NUMBER
};
LongSparseArray<ChannelInfo> channelMap = new LongSparseArray<>();
Cursor cursor = null;
try {
cursor = resolver.query(uri, projection, null, null, null);
if (cursor == null || cursor.getCount() == 0) {
return null;
}
while (cursor.moveToNext()) {
long channelId = cursor.getLong(0);
String channelNumber = cursor.getString(1);
channelMap.put(channelId, getChannelByNumber(channelNumber, channels));
}
} catch (Exception e) {
Log.d(TAG, "Content provider query: " + e.getStackTrace());
return null;
} finally {
if (cursor != null) {
cursor.close();
}
}
return channelMap;
}
public static long getLastProgramEndTimeMillis(ContentResolver resolver, Uri channelUri) {
Uri uri = TvContract.buildProgramsUriForChannel(channelUri);
String[] projection = {Programs.COLUMN_END_TIME_UTC_MILLIS};
Cursor cursor = null;
try {
// TvProvider returns programs chronological order by default.
cursor = resolver.query(uri, projection, null, null, null);
if (cursor == null || cursor.getCount() == 0) {
return 0;
}
cursor.moveToLast();
return cursor.getLong(0);
} catch (Exception e) {
Log.w(TAG, "Unable to get last program end time for " + channelUri, e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return 0;
}
public static List<PlaybackInfo> getProgramPlaybackInfo(
ContentResolver resolver, Uri channelUri, long startTimeMs, long endTimeMs,
int maxProgramInReturn) {
Uri uri = TvContract.buildProgramsUriForChannel(channelUri, startTimeMs,
endTimeMs);
String[] projection = { Programs.COLUMN_START_TIME_UTC_MILLIS,
Programs.COLUMN_END_TIME_UTC_MILLIS,
Programs.COLUMN_CONTENT_RATING,
Programs.COLUMN_INTERNAL_PROVIDER_DATA };
Cursor cursor = null;
List<PlaybackInfo> list = new ArrayList<>();
try {
cursor = resolver.query(uri, projection, null, null, null);
while (cursor.moveToNext()) {
long startMs = cursor.getLong(0);
long endMs = cursor.getLong(1);
TvContentRating[] ratings = stringToContentRatings(cursor.getString(2));
Pair<Integer, String> values = parseInternalProviderData(cursor.getString(3));
int videoType = values.first;
String videoUrl = values.second;
list.add(new PlaybackInfo(startMs, endMs, videoUrl, videoType,
ratings));
if (list.size() > maxProgramInReturn) {
break;
}
}
} catch (Exception e) {
Log.e(TAG, "Failed to get program playback info from TvProvider.", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return list;
}
public static String convertVideoInfoToInternalProviderData(int videotype, String videoUrl) {
return videotype + "," + videoUrl;
}
public static Pair<Integer, String> parseInternalProviderData(String internalData) {
String[] values = internalData.split(",", 2);
if (values.length != 2) {
throw new IllegalArgumentException(internalData);
}
return new Pair<>(Integer.parseInt(values[0]), values[1]);
}
public static void insertUrl(Context context, Uri contentUri, URL sourceUrl) {
if (DEBUG) {
Log.d(TAG, "Inserting " + sourceUrl + " to " + contentUri);
}
InputStream is = null;
OutputStream os = null;
try {
is = sourceUrl.openStream();
os = context.getContentResolver().openOutputStream(contentUri);
copy(is, os);
} catch (IOException ioe) {
Log.e(TAG, "Failed to write " + sourceUrl + " to " + contentUri, ioe);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Ignore exception.
}
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
// Ignore exception.
}
}
}
}
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 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 static ChannelInfo getChannelByNumber(String channelNumber,
List<ChannelInfo> channels) {
for (ChannelInfo info : channels) {
if (info.number.equals(channelNumber)) {
return info;
}
}
throw new IllegalArgumentException("Unknown channel: " + channelNumber);
}
private TvContractUtils() {}
public static class InsertLogosTask extends AsyncTask<Map<Uri, String>, Void, Void> {
private final Context mContext;
InsertLogosTask(Context context) {
mContext = context;
}
@Override
public Void doInBackground(Map<Uri, String>... logosList) {
for (Map<Uri, String> logos : logosList) {
for (Uri uri : logos.keySet()) {
try {
insertUrl(mContext, uri, new URL(logos.get(uri)));
} catch (MalformedURLException e) {
Log.e(TAG, "Can't load " + logos.get(uri), e);
}
}
}
return null;
}
}
}