programForGivenTime = new ArrayList<>();
+ if (!channel.repeatPrograms) {
+ for (XmlTvParser.XmlTvProgram program : channelPrograms) {
+ if (program.startTimeUtcMillis <= endTimeMs
+ && program.endTimeUtcMillis >= startTimeMs) {
+ programForGivenTime.add(new Program.Builder()
+ .setChannelId(ContentUris.parseId(channelUri))
+ .setTitle(program.title)
+ .setDescription(program.description)
+ .setContentRatings(XmlTvParser.xmlTvRatingToTvContentRating(
+ program.rating))
+ .setCanonicalGenres(program.category)
+ .setPosterArtUri(program.icon != null ? program.icon.src : null)
+ .setInternalProviderData(TvContractUtil.
+ convertVideoInfoToInternalProviderData(
+ program.videoType,
+ program.videoSrc != null ? program.videoSrc : channel.url))
+ .setStartTimeUtcMillis(program.startTimeUtcMillis)
+ .setEndTimeUtcMillis(program.endTimeUtcMillis)
+ .build()
+ );
+ }
+ }
+ return programForGivenTime;
+ }
+
+ // If repeat-programs is on, schedule the programs sequentially in a loop. To make every
+ // device play the same program in a given channel and time, we assumes the loop started
+ // from the epoch time.
+ long totalDurationMs = 0;
+ for (XmlTvParser.XmlTvProgram program : channelPrograms) {
+ totalDurationMs += program.getDurationMillis();
+ }
+
+ long programStartTimeMs = startTimeMs - startTimeMs % totalDurationMs;
+ int i = 0;
+ final int programCount = channelPrograms.size();
+ while (programStartTimeMs < endTimeMs) {
+ XmlTvParser.XmlTvProgram programInfo = channelPrograms.get(i++ % programCount);
+ long programEndTimeMs = programStartTimeMs + programInfo.getDurationMillis();
+ if (programEndTimeMs < startTimeMs) {
+ programStartTimeMs = programEndTimeMs;
+ continue;
+ }
+ programForGivenTime.add(new Program.Builder()
+ .setChannelId(ContentUris.parseId(channelUri))
+ .setTitle(programInfo.title)
+ .setDescription(programInfo.description)
+ .setContentRatings(XmlTvParser.xmlTvRatingToTvContentRating(
+ programInfo.rating))
+ .setCanonicalGenres(programInfo.category)
+ .setPosterArtUri(programInfo.icon.src)
+ // NOTE: {@code COLUMN_INTERNAL_PROVIDER_DATA} is a private field where
+ // TvInputService can store anything it wants. Here, we store video type and
+ // video URL so that TvInputService can play the video later with this field.
+ .setInternalProviderData(TvContractUtil.convertVideoInfoToInternalProviderData(
+ programInfo.videoType,
+ programInfo.videoSrc != null ? programInfo.videoSrc : channel.url))
+ .setStartTimeUtcMillis(programStartTimeMs)
+ .setEndTimeUtcMillis(programEndTimeMs)
+ .build()
+ );
+ programStartTimeMs = programEndTimeMs;
+ }
+ return programForGivenTime;
+ }
+
+ /**
+ * Updates the system database, TvProvider, with the given programs.
+ *
+ * If there is any overlap between the given and existing programs, the existing ones
+ * will be updated with the given ones if they have the same title or replaced.
+ *
+ * @param channelUri The channel where the program info will be added.
+ * @param newPrograms A list of {@link Program} instances which includes program
+ * information.
+ */
+ private void updatePrograms(Uri channelUri, List newPrograms) {
+ final int fetchedProgramsCount = newPrograms.size();
+ if (fetchedProgramsCount == 0) {
+ return;
+ }
+ List oldPrograms = TvContractUtil.getPrograms(context.getContentResolver(),
+ channelUri);
+ Program firstNewProgram = newPrograms.get(0);
+ int oldProgramsIndex = 0;
+ int newProgramsIndex = 0;
+ // Skip the past programs. They will be automatically removed by the system.
+ for (Program program : oldPrograms) {
+ oldProgramsIndex++;
+ if(program.getEndTimeUtcMillis() > firstNewProgram.getStartTimeUtcMillis()) {
+ break;
+ }
+ }
+ // Compare the new programs with old programs one by one and update/delete the old one or
+ // insert new program if there is no matching program in the database.
+ ArrayList ops = new ArrayList<>();
+ while (newProgramsIndex < fetchedProgramsCount) {
+ Program oldProgram = oldProgramsIndex < oldPrograms.size()
+ ? oldPrograms.get(oldProgramsIndex) : null;
+ Program newProgram = newPrograms.get(newProgramsIndex);
+ boolean addNewProgram = false;
+ if (oldProgram != null) {
+ if (oldProgram.equals(newProgram)) {
+ // Exact match. No need to update. Move on to the next programs.
+ oldProgramsIndex++;
+ newProgramsIndex++;
+ } else if (needsUpdate(oldProgram, newProgram)) {
+ // Partial match. Update the old program with the new one.
+ // NOTE: Use 'update' in this case instead of 'insert' and 'delete'. There could
+ // be application specific settings which belong to the old program.
+ ops.add(ContentProviderOperation.newUpdate(
+ TvContract.buildProgramUri(oldProgram.getProgramId()))
+ .withValues(newProgram.toContentValues())
+ .build());
+ oldProgramsIndex++;
+ newProgramsIndex++;
+ } else if (oldProgram.getEndTimeUtcMillis() < newProgram.getEndTimeUtcMillis()) {
+ // No match. Remove the old program first to see if the next program in
+ // {@code oldPrograms} partially matches the new program.
+ ops.add(ContentProviderOperation.newDelete(
+ TvContract.buildProgramUri(oldProgram.getProgramId()))
+ .build());
+ oldProgramsIndex++;
+ } else {
+ // No match. The new program does not match any of the old programs. Insert it
+ // as a new program.
+ addNewProgram = true;
+ newProgramsIndex++;
+ }
+ } else {
+ // No old programs. Just insert new programs.
+ addNewProgram = true;
+ newProgramsIndex++;
+ }
+ if (addNewProgram) {
+ ops.add(ContentProviderOperation
+ .newInsert(TvContract.Programs.CONTENT_URI)
+ .withValues(newProgram.toContentValues())
+ .build());
+ }
+ // Throttle the batch operation not to cause TransactionTooLargeException.
+ if (ops.size() > BATCH_OPERATION_COUNT
+ || newProgramsIndex >= fetchedProgramsCount) {
+ try {
+ context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Failed to insert programs.", e);
+ return;
+ }
+ ops.clear();
+ }
+ }
+ }
+
+ /**
+ * Returns {@code true} if the {@code oldProgram} program needs to be updated with the
+ * {@code newProgram} program.
+ */
+ private boolean needsUpdate(Program oldProgram, Program newProgram) {
+ // NOTE: Here, we update the old program if it has the same title and overlaps with the new
+ // program. The test logic is just an example and you can modify this. E.g. check whether
+ // the both programs have the same program ID if your EPG supports any ID for the programs.
+ return oldProgram.getTitle().equals(newProgram.getTitle())
+ && oldProgram.getStartTimeUtcMillis() <= newProgram.getEndTimeUtcMillis()
+ && newProgram.getStartTimeUtcMillis() <= oldProgram.getEndTimeUtcMillis();
+ }
+}
diff --git a/app/src/main/java/at/pansy/iptv/util/IptvUtil.java b/app/src/main/java/at/pansy/iptv/util/IptvUtil.java
new file mode 100644
index 0000000..ddacc12
--- /dev/null
+++ b/app/src/main/java/at/pansy/iptv/util/IptvUtil.java
@@ -0,0 +1,140 @@
+package at.pansy.iptv.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.GZIPInputStream;
+
+import at.pansy.iptv.xmltv.XmlTvParser;
+
+/**
+ * Static helper methods for fetching the channel feed.
+ */
+public class IptvUtil {
+
+ public static final int FORMAT_XMLTV = 0;
+ public static final int FORMAT_M3U = 1;
+
+ private static final String TAG = "IptvUtil";
+ private static HashMap sampleTvListings = new HashMap<>();
+
+ private static final int URLCONNECTION_CONNECTION_TIMEOUT_MS = 3000; // 3 sec
+ private static final int URLCONNECTION_READ_TIMEOUT_MS = 10000; // 10 sec
+
+ private IptvUtil() {
+ }
+
+ public static XmlTvParser.TvListing getTvListings(Context context, String url, int format) {
+
+ if (sampleTvListings.containsKey(url)) {
+ return sampleTvListings.get(url);
+ }
+
+ Uri catalogUri =
+ Uri.parse(url).normalizeScheme();
+
+ XmlTvParser.TvListing sampleTvListing = null;
+ try {
+ InputStream inputStream = getInputStream(context, catalogUri);
+ if (url.endsWith(".gz")) {
+ inputStream = new GZIPInputStream(inputStream);
+ }
+ if (format == FORMAT_M3U) {
+ sampleTvListing = parse(inputStream);
+ } else {
+ sampleTvListing = XmlTvParser.parse(inputStream);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Error in fetching " + catalogUri, e);
+ }
+ if (sampleTvListing != null) {
+ sampleTvListings.put(url, sampleTvListing);
+ }
+ return sampleTvListing;
+ }
+
+ public static InputStream getInputStream(Context context, Uri uri) throws IOException {
+ InputStream inputStream;
+ if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())
+ || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())
+ || ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+ inputStream = context.getContentResolver().openInputStream(uri);
+ } else {
+ URLConnection urlConnection = new URL(uri.toString()).openConnection();
+ urlConnection.setConnectTimeout(URLCONNECTION_CONNECTION_TIMEOUT_MS);
+ urlConnection.setReadTimeout(URLCONNECTION_READ_TIMEOUT_MS);
+ inputStream = urlConnection.getInputStream();
+ }
+ return new BufferedInputStream(inputStream);
+ }
+
+ private static XmlTvParser.TvListing parse(InputStream inputStream) throws IOException {
+ BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
+ String line;
+ List channels = new ArrayList<>();
+ List programs = new ArrayList<>();
+ Map channelMap = new HashMap<>();
+
+ while ((line = in.readLine()) != null) {
+ if (line.startsWith("#EXTINF:")) {
+ // #EXTINF:0051 tvg-id="blizz.de" group-title="DE Spartensender" tvg-logo="897815.png", [COLOR orangered]blizz TV HD[/COLOR]
+
+ String id = null;
+ String displayName = null;
+ String displayNumber = null;
+ int originalNetworkId = 0;
+ XmlTvParser.XmlTvIcon icon = null;
+
+ String[] parts = line.split(", ", 2);
+ if (parts.length == 2) {
+ for (String part : parts[0].split(" ")) {
+ if (part.startsWith("#EXTINF:")) {
+ displayNumber = part.substring(8).replaceAll("^0+", "");
+ originalNetworkId = Integer.parseInt(displayNumber);
+ } else if (part.startsWith("tvg-id=")) {
+ int end = part.indexOf("\"", 8);
+ if (end > 8) {
+ id = part.substring(8, end);
+ }
+ } else if (part.startsWith("tvg-logo=")) {
+ int end = part.indexOf("\"", 10);
+ if (end > 10) {
+ icon = new XmlTvParser.XmlTvIcon("http://logo.iptv.ink/"
+ + part.substring(10, end));
+ }
+ }
+ }
+ displayName = parts[1].replaceAll("\\[\\/?COLOR[^\\]]*\\]", "");
+ }
+
+ if (originalNetworkId != 0 && displayName != null) {
+ XmlTvParser.XmlTvChannel channel =
+ new XmlTvParser.XmlTvChannel(id, displayName, displayNumber, icon,
+ originalNetworkId, 0, 0, false);
+ if (channelMap.containsKey(originalNetworkId)) {
+ channels.set(channelMap.get(originalNetworkId), channel);
+ } else {
+ channelMap.put(originalNetworkId, channels.size());
+ channels.add(channel);
+ }
+ }
+ } else if (line.startsWith("http") && channels.size() > 0) {
+ channels.get(channels.size()-1).url = line;
+ }
+ }
+ return new XmlTvParser.TvListing(channels, programs);
+ }
+}
diff --git a/app/src/main/java/at/pansy/iptv/util/RendererUtil.java b/app/src/main/java/at/pansy/iptv/util/RendererUtil.java
new file mode 100644
index 0000000..a647126
--- /dev/null
+++ b/app/src/main/java/at/pansy/iptv/util/RendererUtil.java
@@ -0,0 +1,41 @@
+package at.pansy.iptv.util;
+
+import android.content.Context;
+;
+import com.google.android.exoplayer.upstream.DefaultHttpDataSource;
+import com.google.android.exoplayer.upstream.DefaultUriDataSource;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by notz.
+ */
+public class RendererUtil {
+
+ public static String processUrlParameters(String url, HashMap httpHeaders) {
+ String[] parameters = url.split("\\|");
+ for (int i = 1; i < parameters.length; i++) {
+ String[] pair = parameters[i].split("=", 2);
+ if (pair.length == 2) {
+ httpHeaders.put(pair[0], pair[1]);
+ }
+ }
+
+ return parameters[0];
+ }
+
+ public static DefaultUriDataSource createDefaultUriDataSource(Context context, String userAgent,
+ HashMap httpHeaders) {
+
+ DefaultHttpDataSource httpDataSource = new DefaultHttpDataSource(userAgent, null, null,
+ DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, false);
+
+ for (Map.Entry header : httpHeaders.entrySet()) {
+ httpDataSource.setRequestProperty(header.getKey(), header.getValue());
+ }
+
+ return new DefaultUriDataSource(context, null, httpDataSource);
+ }
+}
diff --git a/app/src/main/java/at/pansy/iptv/util/SyncUtil.java b/app/src/main/java/at/pansy/iptv/util/SyncUtil.java
new file mode 100644
index 0000000..54eaa58
--- /dev/null
+++ b/app/src/main/java/at/pansy/iptv/util/SyncUtil.java
@@ -0,0 +1,64 @@
+/*
+ * 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 at.pansy.iptv.util;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.media.tv.TvContract;
+import android.os.Bundle;
+import android.util.Log;
+
+import at.pansy.iptv.service.AccountService;
+import at.pansy.iptv.sync.SyncAdapter;
+
+/**
+ * Static helper methods for working with the SyncAdapter framework.
+ */
+public class SyncUtil {
+
+ public static final String ACCOUNT_TYPE = "at.pansy.iptv.account";
+
+ private static final String TAG = "SyncUtil";
+ private static final String CONTENT_AUTHORITY = TvContract.AUTHORITY;
+
+ public static void setUpPeriodicSync(Context context, String inputId) {
+ Account account = AccountService.getAccount(ACCOUNT_TYPE);
+ AccountManager accountManager =
+ (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
+ if (!accountManager.addAccountExplicitly(account, null, null)) {
+ Log.e(TAG, "Account already exists.");
+ }
+ ContentResolver.setIsSyncable(account, CONTENT_AUTHORITY, 1);
+ ContentResolver.setSyncAutomatically(account, CONTENT_AUTHORITY, true);
+ Bundle bundle = new Bundle();
+ bundle.putString(SyncAdapter.BUNDLE_KEY_INPUT_ID, inputId);
+ ContentResolver.addPeriodicSync(account, CONTENT_AUTHORITY, bundle,
+ SyncAdapter.FULL_SYNC_FREQUENCY_SEC);
+ }
+
+ public static void requestSync(String inputId, boolean currentProgramOnly) {
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
+ bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
+ bundle.putString(SyncAdapter.BUNDLE_KEY_INPUT_ID, inputId);
+ bundle.putBoolean(SyncAdapter.BUNDLE_KEY_CURRENT_PROGRAM_ONLY, currentProgramOnly);
+ ContentResolver.requestSync(AccountService.getAccount(ACCOUNT_TYPE), CONTENT_AUTHORITY,
+ bundle);
+ }
+}
diff --git a/app/src/main/java/at/pansy/iptv/util/TvContractUtil.java b/app/src/main/java/at/pansy/iptv/util/TvContractUtil.java
new file mode 100644
index 0000000..18f0fec
--- /dev/null
+++ b/app/src/main/java/at/pansy/iptv/util/TvContractUtil.java
@@ -0,0 +1,349 @@
+/*
+ * 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 at.pansy.iptv.util;
+
+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.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 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;
+
+import at.pansy.iptv.domain.Channel;
+import at.pansy.iptv.domain.PlaybackInfo;
+import at.pansy.iptv.domain.Program;
+import at.pansy.iptv.xmltv.XmlTvParser;
+
+/**
+ * Static helper methods for working with {@link TvContract}.
+ */
+public class TvContractUtil {
+ private static final String TAG = "TvContractUtils";
+ private static final boolean DEBUG = true;
+
+ private static final SparseArray VIDEO_HEIGHT_TO_FORMAT_MAP = new SparseArray<>();
+
+ static {
+ VIDEO_HEIGHT_TO_FORMAT_MAP.put(480, Channels.VIDEO_FORMAT_480P);
+ VIDEO_HEIGHT_TO_FORMAT_MAP.put(576, Channels.VIDEO_FORMAT_576P);
+ VIDEO_HEIGHT_TO_FORMAT_MAP.put(720, Channels.VIDEO_FORMAT_720P);
+ VIDEO_HEIGHT_TO_FORMAT_MAP.put(1080, Channels.VIDEO_FORMAT_1080P);
+ VIDEO_HEIGHT_TO_FORMAT_MAP.put(2160, Channels.VIDEO_FORMAT_2160P);
+ VIDEO_HEIGHT_TO_FORMAT_MAP.put(4320, Channels.VIDEO_FORMAT_4320P);
+ }
+
+ private TvContractUtil() {}
+
+ public static void updateChannels(
+ Context context, String inputId, List channels) {
+ // Create a map from original network ID to channel row ID for existing channels.
+ SparseArray mExistingChannelsMap = new SparseArray<>();
+ 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 logos = new HashMap<>();
+ for (XmlTvParser.XmlTvChannel channel : channels) {
+ values.put(Channels.COLUMN_DISPLAY_NUMBER, channel.displayNumber);
+ values.put(Channels.COLUMN_DISPLAY_NAME, channel.displayName);
+ 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);
+ values.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.url);
+ Long rowId = mExistingChannelsMap.get(channel.originalNetworkId);
+ Uri uri;
+ if (rowId == null) {
+ uri = resolver.insert(Channels.CONTENT_URI, values);
+ } else {
+ uri = TvContract.buildChannelUri(rowId);
+ resolver.update(uri, values, null, null);
+ mExistingChannelsMap.remove(channel.originalNetworkId);
+ }
+ if (!TextUtils.isEmpty(channel.icon.src)) {
+ logos.put(TvContract.buildChannelLogoUri(uri), channel.icon.src);
+ }
+ }
+ 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);
+ }
+ }
+
+ private static String getVideoFormat(int videoHeight) {
+ return VIDEO_HEIGHT_TO_FORMAT_MAP.get(videoHeight);
+ }
+
+ public static LongSparseArray buildChannelMap(
+ ContentResolver resolver, String inputId, List channels) {
+ Uri uri = TvContract.buildChannelsUriForInput(inputId);
+ String[] projection = {
+ Channels._ID,
+ Channels.COLUMN_DISPLAY_NUMBER
+ };
+
+ LongSparseArray 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 Channel getChannel(ContentResolver resolver, Uri channelUri) {
+ Cursor cursor = null;
+ try {
+ // TvProvider returns programs chronological order by default.
+ cursor = resolver.query(channelUri, null, null, null, null);
+ if (cursor == null || cursor.getCount() == 0) {
+ return null;
+ }
+ if (cursor.moveToNext()) {
+ return Channel.fromCursor(cursor);
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to get channel for " + channelUri, e);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return null;
+ }
+
+ public static List getPrograms(ContentResolver resolver, Uri channelUri) {
+ Uri uri = TvContract.buildProgramsUriForChannel(channelUri);
+ Cursor cursor = null;
+ List programs = new ArrayList<>();
+ try {
+ // TvProvider returns programs chronological order by default.
+ cursor = resolver.query(uri, null, null, null, null);
+ if (cursor == null || cursor.getCount() == 0) {
+ return programs;
+ }
+ while (cursor.moveToNext()) {
+ programs.add(Program.fromCursor(cursor));
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to get programs for " + channelUri, e);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return programs;
+ }
+
+ public static List 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,
+ Programs.COLUMN_CANONICAL_GENRE };
+ Cursor cursor = null;
+ List list = new ArrayList<>();
+ try {
+ cursor = resolver.query(uri, projection, null, null, null);
+ while (cursor != null && cursor.moveToNext()) {
+ long startMs = cursor.getLong(0);
+ long endMs = cursor.getLong(1);
+ TvContentRating[] ratings = stringToContentRatings(cursor.getString(2));
+ Pair 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 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 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 XmlTvParser.XmlTvChannel getChannelByNumber(String channelNumber,
+ List channels) {
+ for (XmlTvParser.XmlTvChannel channel : channels) {
+ if (channelNumber.equals(channel.displayNumber)) {
+ return channel;
+ }
+ }
+ throw new IllegalArgumentException("Unknown channel: " + channelNumber);
+ }
+
+ public static class InsertLogosTask extends AsyncTask