aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/data
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/data')
-rw-r--r--src/com/android/tv/data/Channel.java97
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java42
-rw-r--r--src/com/android/tv/data/ChannelLogoFetcher.java307
-rw-r--r--src/com/android/tv/data/ChannelNumber.java71
-rw-r--r--src/com/android/tv/data/InternalDataUtils.java2
-rw-r--r--src/com/android/tv/data/StreamInfo.java4
-rw-r--r--src/com/android/tv/data/epg/EpgFetcher.java327
-rw-r--r--src/com/android/tv/data/epg/EpgReader.java29
-rw-r--r--src/com/android/tv/data/epg/StubEpgReader.java17
9 files changed, 513 insertions, 383 deletions
diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java
index 30f84236..4da56311 100644
--- a/src/com/android/tv/data/Channel.java
+++ b/src/com/android/tv/data/Channel.java
@@ -52,6 +52,16 @@ public final class Channel {
public static final int LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART = 3;
/**
+ * Compares the channel numbers of channels which belong to the same input.
+ */
+ public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR = new Comparator<Channel>() {
+ @Override
+ public int compare(Channel lhs, Channel rhs) {
+ return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
+ }
+ };
+
+ /**
* When a TIS doesn't provide any information about app link, and it doesn't have a leanback
* launch intent, there will be no app link card for the TIS.
*/
@@ -87,9 +97,15 @@ public final class Channel {
TvContract.Channels.COLUMN_APP_LINK_ICON_URI,
TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI,
TvContract.Channels.COLUMN_APP_LINK_INTENT_URI,
+ TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input
};
/**
+ * Channel number delimiter between major and minor parts.
+ */
+ public static final char CHANNEL_NUMBER_DELIMITER = '-';
+
+ /**
* Creates {@code Channel} object from cursor.
*
* <p>The query that created the cursor MUST use {@link #PROJECTION}
@@ -103,7 +119,7 @@ public final class Channel {
channel.mPackageName = Utils.intern(cursor.getString(index++));
channel.mInputId = Utils.intern(cursor.getString(index++));
channel.mType = Utils.intern(cursor.getString(index++));
- channel.mDisplayNumber = cursor.getString(index++);
+ channel.mDisplayNumber = normalizeDisplayNumber(cursor.getString(index++));
channel.mDisplayName = cursor.getString(index++);
channel.mDescription = cursor.getString(index++);
channel.mVideoFormat = Utils.intern(cursor.getString(index++));
@@ -114,17 +130,29 @@ public final class Channel {
channel.mAppLinkIconUri = cursor.getString(index++);
channel.mAppLinkPosterArtUri = cursor.getString(index++);
channel.mAppLinkIntentUri = cursor.getString(index++);
+ if (Utils.isBundledInput(channel.mInputId)) {
+ channel.mRecordingProhibited = cursor.getInt(index++) != 0;
+ }
return channel;
}
/**
- * Creates a {@link Channel} object from the DVR database.
+ * Replaces the channel number separator with dash('-').
*/
- public static Channel fromDvrCursor(Cursor c) {
- Channel channel = new Channel();
- int index = -1;
- channel.mDvrId = c.getLong(++index);
- return channel;
+ public static String normalizeDisplayNumber(String string) {
+ if (!TextUtils.isEmpty(string)) {
+ int length = string.length();
+ for (int i = 0; i < length; i++) {
+ char c = string.charAt(i);
+ if (c == '.' || Character.isWhitespace(c)
+ || Character.getType(c) == Character.DASH_PUNCTUATION) {
+ StringBuilder sb = new StringBuilder(string);
+ sb.setCharAt(i, CHANNEL_NUMBER_DELIMITER);
+ return sb.toString();
+ }
+ }
+ }
+ return string;
}
/** ID of this channel. Matches to BaseColumns._ID. */
@@ -147,8 +175,10 @@ public final class Channel {
private String mAppLinkIntentUri;
private Intent mAppLinkIntent;
private int mAppLinkType;
+ private String mLogoUri;
+ private boolean mRecordingProhibited;
- private long mDvrId;
+ private boolean mChannelLogoExist;
private Channel() {
// Do nothing.
@@ -230,10 +260,14 @@ public final class Channel {
}
/**
- * Returns an ID in DVR database.
+ * Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher.
*/
- public long getDvrId() {
- return mDvrId;
+ public String getLogoUri() {
+ return mLogoUri;
+ }
+
+ public boolean isRecordingProhibited() {
+ return mRecordingProhibited;
}
/**
@@ -279,6 +313,13 @@ public final class Channel {
}
/**
+ * Sets channel logo uri which is got from cloud.
+ */
+ public void setLogoUri(String logoUri) {
+ mLogoUri = logoUri;
+ }
+
+ /**
* Check whether {@code other} has same read-only channel info as this. But, it cannot check two
* channels have same logos. It also excludes browsable and locked, because two fields are
* changed by TV app.
@@ -298,7 +339,8 @@ public final class Channel {
&& mAppLinkColor == other.mAppLinkColor
&& Objects.equals(mAppLinkIconUri, other.mAppLinkIconUri)
&& Objects.equals(mAppLinkPosterArtUri, other.mAppLinkPosterArtUri)
- && Objects.equals(mAppLinkIntentUri, other.mAppLinkIntentUri);
+ && Objects.equals(mAppLinkIntentUri, other.mAppLinkIntentUri)
+ && Objects.equals(mRecordingProhibited, other.mRecordingProhibited);
}
@Override
@@ -315,7 +357,8 @@ public final class Channel {
+ ", isPassthrough=" + mIsPassthrough
+ ", browsable=" + mBrowsable
+ ", locked=" + mLocked
- + ", appLinkText=" + mAppLinkText + "}";
+ + ", appLinkText=" + mAppLinkText
+ + ", recordingProhibited=" + mRecordingProhibited + "}";
}
void copyFrom(Channel other) {
@@ -340,6 +383,8 @@ public final class Channel {
mAppLinkIntentUri = other.mAppLinkIntentUri;
mAppLinkIntent = other.mAppLinkIntent;
mAppLinkType = other.mAppLinkType;
+ mRecordingProhibited = other.mRecordingProhibited;
+ mChannelLogoExist = other.mChannelLogoExist;
}
/**
@@ -389,8 +434,6 @@ public final class Channel {
mChannel.mDisplayName = "name";
mChannel.mDescription = "description";
mChannel.mBrowsable = true;
- mChannel.mLocked = false;
- mChannel.mIsPassthrough = false;
}
public Builder(Channel other) {
@@ -422,7 +465,7 @@ public final class Channel {
@VisibleForTesting
public Builder setDisplayNumber(String displayNumber) {
- mChannel.mDisplayNumber = displayNumber;
+ mChannel.mDisplayNumber = normalizeDisplayNumber(displayNumber);
return this;
}
@@ -485,6 +528,11 @@ public final class Channel {
return this;
}
+ public Builder setRecordingProhibited(boolean recordingProhibited) {
+ mChannel.mRecordingProhibited = recordingProhibited;
+ return this;
+ }
+
public Channel build() {
Channel channel = new Channel();
channel.copyFrom(mChannel);
@@ -524,6 +572,21 @@ public final class Channel {
}
/**
+ * Sets if the channel logo exists. This method should be only called from
+ * {@link ChannelDataManager}.
+ */
+ void setChannelLogoExist(boolean exist) {
+ mChannelLogoExist = exist;
+ }
+
+ /**
+ * Returns if channel logo exists.
+ */
+ public boolean channelLogoExists() {
+ return mChannelLogoExist;
+ }
+
+ /**
* Returns the type of app link for this channel.
* It returns {@link #APP_LINK_TYPE_CHANNEL} if the channel has a non null app link text and
* a valid app link intent, it returns {@link #APP_LINK_TYPE_APP} if the input service which
@@ -655,4 +718,4 @@ public final class Channel {
return label;
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java
index 6f9ea6d7..eb3871fc 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -21,10 +21,14 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
+import android.content.res.AssetFileDescriptor;
import android.database.ContentObserver;
+import android.database.sqlite.SQLiteException;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
import android.media.tv.TvInputManager.TvInputCallback;
+import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -43,6 +47,8 @@ import com.android.tv.util.PermissionUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
+import java.io.FileNotFoundException;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -192,7 +198,6 @@ public class ChannelDataManager {
mStarted = false;
mDbLoadFinished = false;
- ChannelLogoFetcher.stopFetchingChannelLogos();
mInputManager.removeCallback(mTvInputCallback);
mContentResolver.unregisterContentObserver(mChannelObserver);
mHandler.removeCallbacksAndMessages(null);
@@ -590,6 +595,36 @@ public class ChannelDataManager {
}
}
+ private class checkChannelLogoExistTask extends AsyncTask<Void, Void, Boolean> {
+ private final Channel mChannel;
+
+ public checkChannelLogoExistTask(Channel channel) {
+ mChannel = channel;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ boolean result = false;
+ try {
+ AssetFileDescriptor f = mContext.getContentResolver().openAssetFileDescriptor(
+ TvContract.buildChannelLogoUri(mChannel.getId()), "r");
+ result = true;
+ f.close();
+ } catch (SQLiteException | IOException | NullPointerException e) {
+ // File not found or asset file not found.
+ }
+ return result;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ ChannelWrapper wrapper = mChannelWrapperMap.get(mChannel.getId());
+ if (wrapper != null) {
+ wrapper.mChannel.setChannelLogoExist(result);
+ }
+ }
+ }
+
private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask {
public QueryAllChannelsTask(ContentResolver contentResolver) {
@@ -625,6 +660,8 @@ public class ChannelDataManager {
boolean newlyAdded = !removedChannelIds.remove(channelId);
ChannelWrapper channelWrapper;
if (newlyAdded) {
+ new checkChannelLogoExistTask(channel)
+ .executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
channelWrapper = new ChannelWrapper(channel);
mChannelWrapperMap.put(channel.getId(), channelWrapper);
if (!channelWrapper.mInputRemoved) {
@@ -640,9 +677,9 @@ public class ChannelDataManager {
// {@link #applyUpdatedValuesToDb} is called. Therefore, the value
// between DB and ChannelDataManager could be different for a while.
// Therefore, we'll keep the values in ChannelDataManager.
- channelWrapper.mChannel.copyFrom(channel);
channel.setBrowsable(oldChannel.isBrowsable());
channel.setLocked(oldChannel.isLocked());
+ channelWrapper.mChannel.copyFrom(channel);
if (!channelWrapper.mInputRemoved) {
channelUpdated = true;
updatedChannelWrappers.add(channelWrapper);
@@ -693,7 +730,6 @@ public class ChannelDataManager {
r.run();
}
mPostRunnablesAfterChannelUpdate.clear();
- ChannelLogoFetcher.startFetchingChannelLogos(mContext);
}
}
diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java
index 5a549f83..256ecdb2 100644
--- a/src/com/android/tv/data/ChannelLogoFetcher.java
+++ b/src/com/android/tv/data/ChannelLogoFetcher.java
@@ -16,155 +16,68 @@
package com.android.tv.data;
+import android.content.ContentProviderOperation;
import android.content.Context;
-import android.database.Cursor;
+import android.content.OperationApplicationException;
+import android.content.SharedPreferences;
import android.graphics.Bitmap.CompressFormat;
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.os.RemoteException;
+import android.support.annotation.AnyThread;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.util.AsyncDbTask;
+import com.android.tv.common.SharedPreferencesUtils;
import com.android.tv.util.BitmapUtils;
import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
import com.android.tv.util.PermissionUtils;
-import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
import java.util.Map;
-import java.util.Set;
+import java.util.List;
/**
- * Utility class for TMS data.
- * This class is thread safe.
+ * Fetches channel logos from the cloud into the database. It's for the channels which have no logos
+ * or need update logos. This class is thread safe.
*/
public class ChannelLogoFetcher {
private static final String TAG = "ChannelLogoFetcher";
private static final boolean DEBUG = false;
- /**
- * The name of the file which contains the TMS data.
- * The file has multiple records and each of them is a string separated by '|' like
- * STATION_NAME|SHORT_NAME|CALL_SIGN|LOGO_URI.
- */
- private static final String TMS_US_TABLE_FILE = "tms_us.table";
- private static final String TMS_KR_TABLE_FILE = "tms_kr.table";
- private static final String FIELD_SEPARATOR = "\\|";
- private static final String NAME_SEPARATOR_FOR_TMS = "\\(|\\)|\\{|\\}|\\[|\\]";
- private static final String NAME_SEPARATOR_FOR_DB = "\\W";
- private static final int INDEX_NAME = 0;
- private static final int INDEX_SHORT_NAME = 1;
- private static final int INDEX_CALL_SIGN = 2;
- private static final int INDEX_LOGO_URI = 3;
-
- private static final String COLUMN_CHANNEL_LOGO = "logo";
+ private static final String PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO =
+ "is_first_time_fetch_channel_logo";
- private static final Object sLock = new Object();
- private static final Set<Long> sChannelIdBlackListSet = new HashSet<>();
- private static LoadChannelTask sQueryTask;
private static FetchLogoTask sFetchTask;
/**
- * Fetch the channel logos from TMS data and insert them into TvProvider.
+ * Fetches the channel logos from the cloud data and insert them into TvProvider.
* The previous task is canceled and a new task starts.
*/
- public static void startFetchingChannelLogos(Context context) {
+ @AnyThread
+ public static synchronized void startFetchingChannelLogos(
+ Context context, List<Channel> channels) {
if (!PermissionUtils.hasAccessAllEpg(context)) {
// TODO: support this feature for non-system LC app. b/23939816
return;
}
- synchronized (sLock) {
- stopFetchingChannelLogos();
- if (DEBUG) Log.d(TAG, "Request to start fetching logos.");
- sQueryTask = new LoadChannelTask(context);
- sQueryTask.executeOnDbThread();
+ if (sFetchTask != null) {
+ sFetchTask.cancel(true);
}
- }
-
- /**
- * Stops the current fetching tasks. This can be called when the Activity pauses.
- */
- public static void stopFetchingChannelLogos() {
- synchronized (sLock) {
- if (DEBUG) Log.d(TAG, "Request to stop fetching logos.");
- if (sQueryTask != null) {
- sQueryTask.cancel(true);
- sQueryTask = null;
- }
- if (sFetchTask != null) {
- sFetchTask.cancel(true);
- sFetchTask = null;
- }
+ if (DEBUG) Log.d(TAG, "Request to start fetching logos.");
+ if (channels == null || channels.isEmpty()) {
+ return;
}
+ sFetchTask = new FetchLogoTask(context, channels);
+ sFetchTask.execute();
}
private ChannelLogoFetcher() {
}
- private static final class LoadChannelTask extends AsyncDbTask<Void, Void, List<Channel>> {
- private final Context mContext;
-
- public LoadChannelTask(Context context) {
- mContext = context;
- }
-
- @Override
- protected List<Channel> doInBackground(Void... arg) {
- // Load channels which doesn't have channel logos.
- if (DEBUG) Log.d(TAG, "Starts loading the channels from DB");
- String[] projection =
- new String[] { Channels._ID, Channels.COLUMN_DISPLAY_NAME };
- String selection = COLUMN_CHANNEL_LOGO + " IS NULL AND "
- + Channels.COLUMN_PACKAGE_NAME + "=?";
- String[] selectionArgs = new String[] { mContext.getPackageName() };
- try (Cursor c = mContext.getContentResolver().query(Channels.CONTENT_URI,
- projection, selection, selectionArgs, null)) {
- if (c == null) {
- Log.e(TAG, "Query returns null cursor", new RuntimeException());
- return null;
- }
- List<Channel> channels = new ArrayList<>();
- while (!isCancelled() && c.moveToNext()) {
- long channelId = c.getLong(0);
- if (sChannelIdBlackListSet.contains(channelId)) {
- continue;
- }
- channels.add(new Channel.Builder().setId(c.getLong(0))
- .setDisplayName(c.getString(1).toUpperCase(Locale.getDefault()))
- .build());
- }
- return channels;
- }
- }
-
- @Override
- protected void onPostExecute(List<Channel> channels) {
- synchronized (sLock) {
- if (DEBUG) {
- int count = channels == null ? 0 : channels.size();
- Log.d(TAG, count + " channels are loaded");
- }
- if (sQueryTask == this) {
- sQueryTask = null;
- if (channels != null && !channels.isEmpty()) {
- sFetchTask = new FetchLogoTask(mContext, channels);
- sFetchTask.execute();
- }
- }
- }
- }
- }
-
private static final class FetchLogoTask extends AsyncTask<Void, Void, Void> {
private final Context mContext;
private final List<Channel> mChannels;
@@ -180,83 +93,53 @@ public class ChannelLogoFetcher {
if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
return null;
}
- // Load the TMS table data.
- if (DEBUG) Log.d(TAG, "Loads TMS data");
- Map<String, String> channelNameLogoUriMap = new HashMap<>();
- try {
- channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_US_TABLE_FILE));
- if (isCancelled()) {
- if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
- return null;
+ List<Channel> channelsToUpdate = new ArrayList<>();
+ List<Channel> channelsToRemove = new ArrayList<>();
+ // Updates or removes the logo by comparing the logo uri which is got from the cloud
+ // and the stored one. And we assume that the data got form the cloud is 100%
+ // correct and completed.
+ SharedPreferences sharedPreferences =
+ mContext.getSharedPreferences(
+ SharedPreferencesUtils.SHARED_PREF_CHANNEL_LOGO_URIS,
+ Context.MODE_PRIVATE);
+ SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit();
+ Map<String, ?> uncheckedChannels = sharedPreferences.getAll();
+ boolean isFirstTimeFetchChannelLogo = sharedPreferences.getBoolean(
+ PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, true);
+ // Iterating channels.
+ for (Channel channel : mChannels) {
+ String channelIdString = Long.toString(channel.getId());
+ String storedChannelLogoUri = (String) uncheckedChannels.remove(channelIdString);
+ if (!TextUtils.isEmpty(channel.getLogoUri())
+ && !TextUtils.equals(storedChannelLogoUri, channel.getLogoUri())) {
+ channelsToUpdate.add(channel);
+ sharedPreferencesEditor.putString(channelIdString, channel.getLogoUri());
+ } else if (TextUtils.isEmpty(channel.getLogoUri())
+ && (!TextUtils.isEmpty(storedChannelLogoUri)
+ || isFirstTimeFetchChannelLogo)) {
+ channelsToRemove.add(channel);
+ sharedPreferencesEditor.remove(channelIdString);
}
- channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_KR_TABLE_FILE));
- } catch (IOException e) {
- Log.e(TAG, "Loading TMS data failed.", e);
- return null;
}
- if (isCancelled()) {
- if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
- return null;
+
+ // Removes non existing channels from SharedPreferences.
+ for (String channelId : uncheckedChannels.keySet()) {
+ sharedPreferencesEditor.remove(channelId);
}
- // Iterating channels.
- for (Channel channel : mChannels) {
+ // Updates channel logos.
+ for (Channel channel : channelsToUpdate) {
if (isCancelled()) {
if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
return null;
}
- // Download the channel logo.
- if (TextUtils.isEmpty(channel.getDisplayName())) {
- if (DEBUG) {
- Log.d(TAG, "The channel with ID (" + channel.getId()
- + ") doesn't have the display name.");
- }
- sChannelIdBlackListSet.add(channel.getId());
- continue;
- }
- String channelName = channel.getDisplayName().trim();
- String logoUri = channelNameLogoUriMap.get(channelName);
- if (TextUtils.isEmpty(logoUri)) {
- if (DEBUG) {
- Log.d(TAG, "Can't find a logo URI for channel '" + channelName + "'");
- }
- // Find the candidate names. If the channel name is CNN-HD, then find CNNHD
- // and CNN. Or if the channel name is KQED+, then find KQED.
- String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_DB);
- if (splitNames.length > 1) {
- StringBuilder sb = new StringBuilder();
- for (String splitName : splitNames) {
- sb.append(splitName);
- }
- logoUri = channelNameLogoUriMap.get(sb.toString());
- if (DEBUG) {
- if (TextUtils.isEmpty(logoUri)) {
- Log.d(TAG, "Can't find a logo URI for channel '" + sb.toString()
- + "'");
- }
- }
- }
- if (TextUtils.isEmpty(logoUri)
- && splitNames[0].length() != channelName.length()) {
- logoUri = channelNameLogoUriMap.get(splitNames[0]);
- if (DEBUG) {
- if (TextUtils.isEmpty(logoUri)) {
- Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0]
- + "'");
- }
- }
- }
- }
- if (TextUtils.isEmpty(logoUri)) {
- sChannelIdBlackListSet.add(channel.getId());
- continue;
- }
+ // Downloads the channel logo.
+ String logoUri = channel.getLogoUri();
ScaledBitmapInfo bitmapInfo = BitmapUtils.decodeSampledBitmapFromUriString(
mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE);
if (bitmapInfo == null) {
Log.e(TAG, "Failed to load bitmap. {channelName=" + channel.getDisplayName()
+ ", " + "logoUri=" + logoUri + "}");
- sChannelIdBlackListSet.add(channel.getId());
continue;
}
if (isCancelled()) {
@@ -264,12 +147,15 @@ public class ChannelLogoFetcher {
return null;
}
- // Insert the logo to DB.
+ // Inserts the logo to DB.
Uri dstLogoUri = TvContract.buildChannelLogoUri(channel.getId());
try (OutputStream os = mContext.getContentResolver().openOutputStream(dstLogoUri)) {
bitmapInfo.bitmap.compress(CompressFormat.PNG, 100, os);
} catch (IOException e) {
Log.e(TAG, "Failed to write " + logoUri + " to " + dstLogoUri, e);
+ // Removes it from the shared preference for the failed channels to make it
+ // retry next time.
+ sharedPreferencesEditor.remove(Long.toString(channel.getId()));
continue;
}
if (DEBUG) {
@@ -277,63 +163,30 @@ public class ChannelLogoFetcher {
+ dstLogoUri + "}");
}
}
- if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully.");
- return null;
- }
- @WorkerThread
- private Map<String, String> readTmsFile(Context context, String fileName)
- throws IOException {
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(
- context.getAssets().open(fileName)))) {
- Map<String, String> channelNameLogoUriMap = new HashMap<>();
- String line;
- while ((line = reader.readLine()) != null && !isCancelled()) {
- String[] data = line.split(FIELD_SEPARATOR);
- if (data.length != INDEX_LOGO_URI + 1) {
- if (DEBUG) Log.d(TAG, "Invalid or comment row: " + line);
- continue;
- }
- addChannelNames(channelNameLogoUriMap,
- data[INDEX_NAME].toUpperCase(Locale.getDefault()),
- data[INDEX_LOGO_URI]);
- addChannelNames(channelNameLogoUriMap,
- data[INDEX_SHORT_NAME].toUpperCase(Locale.getDefault()),
- data[INDEX_LOGO_URI]);
- addChannelNames(channelNameLogoUriMap,
- data[INDEX_CALL_SIGN].toUpperCase(Locale.getDefault()),
- data[INDEX_LOGO_URI]);
+ // Removes the logos for the channels that have logos before but now
+ // their logo uris are null.
+ boolean deleteChannelLogoFailed = false;
+ if (!channelsToRemove.isEmpty()) {
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (Channel channel : channelsToRemove) {
+ ops.add(ContentProviderOperation.newDelete(
+ TvContract.buildChannelLogoUri(channel.getId())).build());
+ }
+ try {
+ mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ } catch (RemoteException | OperationApplicationException e) {
+ deleteChannelLogoFailed = true;
+ Log.e(TAG, "Error deleting obsolete channels", e);
}
- return channelNameLogoUriMap;
}
- }
-
- private void addChannelNames(Map<String, String> channelNameLogoUriMap, String channelName,
- String logoUri) {
- if (!TextUtils.isEmpty(channelName)) {
- channelNameLogoUriMap.put(channelName, logoUri);
- // Find the candidate names.
- // If the name is like "W05AAD (W05AA-D)", then split the names into "W05AAD" and
- // "W05AA-D"
- String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_TMS);
- if (splitNames.length > 1) {
- for (String name : splitNames) {
- name = name.trim();
- if (channelNameLogoUriMap.get(name) == null) {
- channelNameLogoUriMap.put(name, logoUri);
- }
- }
- }
- }
- }
-
- @Override
- protected void onPostExecute(Void result) {
- synchronized (sLock) {
- if (sFetchTask == this) {
- sFetchTask = null;
- }
+ if (isFirstTimeFetchChannelLogo && !deleteChannelLogoFailed) {
+ sharedPreferencesEditor.putBoolean(
+ PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, false);
}
+ sharedPreferencesEditor.commit();
+ if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully.");
+ return null;
}
}
}
diff --git a/src/com/android/tv/data/ChannelNumber.java b/src/com/android/tv/data/ChannelNumber.java
index 59021609..29054aa5 100644
--- a/src/com/android/tv/data/ChannelNumber.java
+++ b/src/com/android/tv/data/ChannelNumber.java
@@ -17,37 +17,38 @@
package com.android.tv.data;
import android.support.annotation.NonNull;
+import android.text.TextUtils;
import android.view.KeyEvent;
+import com.android.tv.util.StringUtils;
+
+import java.util.Objects;
+
/**
* A convenience class to handle channel number.
*/
public final class ChannelNumber implements Comparable<ChannelNumber> {
- public static final String PRIMARY_CHANNEL_DELIMITER = "-";
- public static final String[] CHANNEL_DELIMITERS = {"-", ".", " "};
-
private static final int[] CHANNEL_DELIMITER_KEYCODES = {
KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_NUMPAD_SUBTRACT, KeyEvent.KEYCODE_PERIOD,
KeyEvent.KEYCODE_NUMPAD_DOT, KeyEvent.KEYCODE_SPACE
};
+ /** The major part of the channel number. */
public String majorNumber;
+ /** The flag which indicates whether it has a delimiter or not. */
public boolean hasDelimiter;
+ /** The major part of the channel number. */
public String minorNumber;
public ChannelNumber() {
reset();
}
- public ChannelNumber(String major, boolean hasDelimiter, String minor) {
- setChannelNumber(major, hasDelimiter, minor);
- }
-
public void reset() {
setChannelNumber("", false, "");
}
- public void setChannelNumber(String majorNumber, boolean hasDelimiter, String minorNumber) {
+ private void setChannelNumber(String majorNumber, boolean hasDelimiter, String minorNumber) {
this.majorNumber = majorNumber;
this.hasDelimiter = hasDelimiter;
this.minorNumber = minorNumber;
@@ -56,7 +57,7 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
@Override
public String toString() {
if (hasDelimiter) {
- return majorNumber + PRIMARY_CHANNEL_DELIMITER + minorNumber;
+ return majorNumber + Channel.CHANNEL_NUMBER_DELIMITER + minorNumber;
}
return majorNumber;
}
@@ -75,6 +76,22 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
return major - opponentMajor;
}
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ChannelNumber) {
+ ChannelNumber channelNumber = (ChannelNumber) obj;
+ return TextUtils.equals(majorNumber, channelNumber.majorNumber)
+ && TextUtils.equals(minorNumber, channelNumber.minorNumber)
+ && hasDelimiter == channelNumber.hasDelimiter;
+ }
+ return super.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(majorNumber, hasDelimiter, minorNumber);
+ }
+
public static boolean isChannelNumberDelimiterKey(int keyCode) {
for (int delimiterKeyCode : CHANNEL_DELIMITER_KEYCODES) {
if (delimiterKeyCode == keyCode) {
@@ -84,22 +101,22 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
return false;
}
+ /**
+ * Returns the ChannelNumber instance.
+ * <p>
+ * Note that all the channel number argument should be normalized by
+ * {@link Channel#normalizeDisplayNumber}. The channels retrieved from
+ * {@link ChannelDataManager} are already normalized.
+ */
public static ChannelNumber parseChannelNumber(String number) {
if (number == null) {
return null;
}
ChannelNumber ret = new ChannelNumber();
- int indexOfDelimiter = -1;
- for (String delimiter : CHANNEL_DELIMITERS) {
- indexOfDelimiter = number.indexOf(delimiter);
- if (indexOfDelimiter >= 0) {
- break;
- }
- }
+ int indexOfDelimiter = number.indexOf(Channel.CHANNEL_NUMBER_DELIMITER);
if (indexOfDelimiter == 0 || indexOfDelimiter == number.length() - 1) {
return null;
- }
- if (indexOfDelimiter < 0) {
+ } else if (indexOfDelimiter < 0) {
ret.majorNumber = number;
if (!isInteger(ret.majorNumber)) {
return null;
@@ -115,25 +132,31 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
return ret;
}
+ /**
+ * Compares the channel numbers.
+ * <p>
+ * Note that all the channel number arguments should be normalized by
+ * {@link Channel#normalizeDisplayNumber}. The channels retrieved from
+ * {@link ChannelDataManager} are already normalized.
+ */
public static int compare(String lhs, String rhs) {
ChannelNumber lhsNumber = parseChannelNumber(lhs);
ChannelNumber rhsNumber = parseChannelNumber(rhs);
+ // Null first
if (lhsNumber == null && rhsNumber == null) {
- return 0;
+ return StringUtils.compare(lhs, rhs);
} else if (lhsNumber == null /* && rhsNumber != null */) {
return -1;
- } else if (lhsNumber != null && rhsNumber == null) {
+ } else if (rhsNumber == null) {
return 1;
}
return lhsNumber.compareTo(rhsNumber);
}
- public static boolean isInteger(String string) {
+ private static boolean isInteger(String string) {
try {
Integer.parseInt(string);
- } catch(NumberFormatException e) {
- return false;
- } catch(NullPointerException e) {
+ } catch(NumberFormatException | NullPointerException e) {
return false;
}
return true;
diff --git a/src/com/android/tv/data/InternalDataUtils.java b/src/com/android/tv/data/InternalDataUtils.java
index 6054f089..e33ca18f 100644
--- a/src/com/android/tv/data/InternalDataUtils.java
+++ b/src/com/android/tv/data/InternalDataUtils.java
@@ -21,7 +21,7 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.tv.data.Program.CriticScore;
-import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.data.RecordedProgram;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java
index fe461f14..709863cf 100644
--- a/src/com/android/tv/data/StreamInfo.java
+++ b/src/com/android/tv/data/StreamInfo.java
@@ -38,5 +38,9 @@ public interface StreamInfo {
int getAudioChannelCount();
boolean hasClosedCaption();
boolean isVideoAvailable();
+ /**
+ * Returns true, if video or audio is available.
+ */
+ boolean isVideoOrAudioAvailable();
int getVideoUnavailableReason();
}
diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java
index 3b093b6a..ddd68ad7 100644
--- a/src/com/android/tv/data/epg/EpgFetcher.java
+++ b/src/com/android/tv/data/epg/EpgFetcher.java
@@ -16,13 +16,11 @@
package com.android.tv.data.epg;
-import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
-import android.content.pm.PackageManager;
import android.database.Cursor;
import android.location.Address;
import android.media.tv.TvContentRating;
@@ -46,9 +44,11 @@ import com.android.tv.TvApplication;
import com.android.tv.common.WeakHandler;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ChannelLogoFetcher;
import com.android.tv.data.InternalDataUtils;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
+import com.android.tv.tuner.util.PostalCodeUtils;
import com.android.tv.util.LocationUtils;
import com.android.tv.util.RecurringRunner;
import com.android.tv.util.Utils;
@@ -56,8 +56,10 @@ import com.android.tv.util.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@@ -69,14 +71,27 @@ public class EpgFetcher {
private static final boolean DEBUG = false;
private static final int MSG_FETCH_EPG = 1;
+ private static final int MSG_FAST_FETCH_EPG = 2;
private static final long EPG_PREFETCH_RECURRING_PERIOD_MS = TimeUnit.HOURS.toMillis(4);
private static final long EPG_READER_INIT_WAIT_MS = TimeUnit.MINUTES.toMillis(1);
private static final long LOCATION_INIT_WAIT_MS = TimeUnit.SECONDS.toMillis(10);
private static final long LOCATION_ERROR_WAIT_MS = TimeUnit.HOURS.toMillis(1);
+ private static final long NO_INFO_FETCHED_WAIT_MS = TimeUnit.SECONDS.toMillis(10);
private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30);
+ private static final long PROGRAM_FETCH_SHORT_DURATION_SEC = TimeUnit.HOURS.toSeconds(3);
+ private static final long PROGRAM_FETCH_LONG_DURATION_SEC = TimeUnit.DAYS.toSeconds(2)
+ + EPG_PREFETCH_RECURRING_PERIOD_MS / 1000;
+
+ // This equals log2(EPG_PREFETCH_RECURRING_PERIOD_MS / NO_INFO_FETCHED_WAIT_MS + 1),
+ // since we will double waiting time every other trial, therefore this limit the maximum
+ // waiting time less than half of EPG_PREFETCH_RECURRING_PERIOD_MS.
+ private static final int NO_INFO_RETRY_LIMIT = 31 - Integer.numberOfLeadingZeros(
+ (int) (EPG_PREFETCH_RECURRING_PERIOD_MS / NO_INFO_FETCHED_WAIT_MS + 1));
+
private static final int BATCH_OPERATION_COUNT = 100;
+ private static final int QUERY_CHANNEL_COUNT = 50;
private static final String SUPPORTED_COUNTRY_CODE = Locale.US.getCountry();
private static final String CONTENT_RATING_SEPARATOR = ",";
@@ -96,8 +111,11 @@ public class EpgFetcher {
private EpgFetcherHandler mHandler;
private RecurringRunner mRecurringRunner;
private boolean mStarted;
+ private boolean mScanningChannels;
+ private int mFetchRetryCount;
private long mLastEpgTimestamp = -1;
+ // @GuardedBy("this")
private String mLineupId;
public static synchronized EpgFetcher getInstance(Context context) {
@@ -122,21 +140,33 @@ public class EpgFetcher {
@Override
public void onLoadFinished() {
if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()");
- handleChannelChanged();
+ if (!mScanningChannels) {
+ handleChannelChanged();
+ }
}
@Override
public void onChannelListUpdated() {
if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()");
- handleChannelChanged();
+ if (!mScanningChannels) {
+ handleChannelChanged();
+ }
}
@Override
public void onChannelBrowsableChanged() {
if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelBrowsableChanged()");
- handleChannelChanged();
+ if (!mScanningChannels) {
+ handleChannelChanged();
+ }
}
});
+ // Warm up to get address, because the first call of getCurrentAddress is usually failed.
+ try {
+ LocationUtils.getCurrentAddress(mContext);
+ } catch (SecurityException | IOException e) {
+ // Do nothing
+ }
}
private void handleChannelChanged() {
@@ -145,7 +175,9 @@ public class EpgFetcher {
stop();
}
} else {
- start();
+ if (canStart()) {
+ start();
+ }
}
}
@@ -173,17 +205,14 @@ public class EpgFetcher {
if (!TextUtils.isEmpty(getLastLineupId())) {
return true;
}
- if (mContext.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
- != PackageManager.PERMISSION_GRANTED) {
- if (DEBUG) Log.d(TAG, "No permission to check the current location.");
- return false;
+ if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ return true;
}
-
try {
Address address = LocationUtils.getCurrentAddress(mContext);
if (address != null
&& !TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) {
- if (DEBUG) Log.d(TAG, "Country not supported: " + address.getCountryCode());
+ Log.i(TAG, "Country not supported: " + address.getCountryCode());
return false;
}
} catch (SecurityException e) {
@@ -197,9 +226,13 @@ public class EpgFetcher {
/**
* Starts fetching EPG.
+ *
+ * @param resetNextRunTime if true, next run time is reset, so EPG will be fetched
+ * {@link #EPG_PREFETCH_RECURRING_PERIOD_MS} later.
+ * otherwise, EPG is fetched when this method is called.
*/
@MainThread
- public void start() {
+ private void startInternal(boolean resetNextRunTime) {
if (DEBUG) Log.d(TAG, "start()");
if (mStarted) {
if (DEBUG) Log.d(TAG, "EpgFetcher thread already started.");
@@ -215,19 +248,35 @@ public class EpgFetcher {
mHandler = new EpgFetcherHandler(handlerThread.getLooper(), this);
mRecurringRunner = new RecurringRunner(mContext, EPG_PREFETCH_RECURRING_PERIOD_MS,
new EpgRunner(), null);
- mRecurringRunner.start();
+ mRecurringRunner.start(resetNextRunTime);
if (DEBUG) Log.d(TAG, "EpgFetcher thread started successfully.");
}
+ @MainThread
+ public void start() {
+ if (System.currentTimeMillis() - getLastUpdatedEpgTimestamp() >
+ EPG_PREFETCH_RECURRING_PERIOD_MS) {
+ startImmediately(false);
+ } else {
+ startInternal(false);
+ }
+ }
+
/**
* Starts fetching EPG immediately if possible without waiting for the timer.
+ *
+ * @param clearStoredLineupId if true, stored lineup id will be clear before fetching EPG.
*/
@MainThread
- public void startImmediately() {
- start();
+ public void startImmediately(boolean clearStoredLineupId) {
+ startInternal(true);
if (mStarted) {
+ if (clearStoredLineupId) {
+ if (DEBUG) Log.d(TAG, "Clear stored lineup id: " + mLineupId);
+ setLastLineupId(null);
+ }
if (DEBUG) Log.d(TAG, "Starting fetcher immediately");
- fetchEpg();
+ postFetchRequest(true, 0);
}
}
@@ -246,48 +295,71 @@ public class EpgFetcher {
mHandler.getLooper().quit();
}
- private void fetchEpg() {
- fetchEpg(0);
+ /**
+ * Notifies EPG fetcher that channel scanning is started.
+ */
+ @MainThread
+ public void onChannelScanStarted() {
+ stop();
+ mScanningChannels = true;
}
- private void fetchEpg(long delay) {
- mHandler.removeMessages(MSG_FETCH_EPG);
- mHandler.sendEmptyMessageDelayed(MSG_FETCH_EPG, delay);
+ /**
+ * Notifies EPG fetcher that channel scanning is finished.
+ */
+ @MainThread
+ public void onChannelScanFinished() {
+ mScanningChannels = false;
+ start();
+ }
+
+ private void postFetchRequest(boolean fastFetch, long delay) {
+ int msg = fastFetch ? MSG_FAST_FETCH_EPG : MSG_FETCH_EPG;
+ mHandler.removeMessages(msg);
+ mHandler.sendEmptyMessageDelayed(msg, delay);
}
private void onFetchEpg() {
+ onFetchEpg(false);
+ }
+
+ private void onFetchEpg(boolean fastFetch) {
if (DEBUG) Log.d(TAG, "Start fetching EPG.");
if (!mEpgReader.isAvailable()) {
- if (DEBUG) Log.d(TAG, "EPG reader is not temporarily available.");
- fetchEpg(EPG_READER_INIT_WAIT_MS);
+ Log.i(TAG, "EPG reader is not temporarily available.");
+ postFetchRequest(fastFetch, EPG_READER_INIT_WAIT_MS);
return;
}
String lineupId = getLastLineupId();
if (lineupId == null) {
- Address address;
try {
- address = LocationUtils.getCurrentAddress(mContext);
+ PostalCodeUtils.updatePostalCode(mContext);
} catch (IOException e) {
- if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e);
- fetchEpg(LOCATION_ERROR_WAIT_MS);
- return;
+ if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e);
+ postFetchRequest(fastFetch, LOCATION_ERROR_WAIT_MS);
+ return;
+ }
} catch (SecurityException e) {
- Log.w(TAG, "No permission to get the current location.");
- return;
- }
- if (address == null) {
- if (DEBUG) Log.d(TAG, "Null address returned.");
- fetchEpg(LOCATION_INIT_WAIT_MS);
+ if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ Log.w(TAG, "No permission to get the current location.");
+ return;
+ }
+ } catch (PostalCodeUtils.NoPostalCodeException e) {
+ Log.i(TAG, "Failed to get the current postal code.");
+ postFetchRequest(fastFetch, LOCATION_INIT_WAIT_MS);
return;
}
- if (DEBUG) Log.d(TAG, "Current location is " + address);
+ String postalCode = PostalCodeUtils.getLastPostalCode(mContext);
+ if (DEBUG) Log.d(TAG, "The current postal code is " + postalCode);
- lineupId = getLineupForAddress(address);
+ lineupId = pickLineupForPostalCode(postalCode);
if (lineupId != null) {
- if (DEBUG) Log.d(TAG, "Saving lineup " + lineupId + "found for " + address);
+ Log.i(TAG, "Selecting the lineup " + lineupId);
setLastLineupId(lineupId);
} else {
- if (DEBUG) Log.d(TAG, "No lineup found for " + address);
+ Log.i(TAG, "Failed to get lineup id");
+ retryFetchEpg(fastFetch);
return;
}
}
@@ -299,48 +371,109 @@ public class EpgFetcher {
return;
}
- boolean updated = false;
List<Channel> channels = mEpgReader.getChannels(lineupId);
- for (Channel channel : channels) {
- List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channel.getId()));
- Collections.sort(programs);
- if (DEBUG) {
- Log.d(TAG, "Fetched " + programs.size() + " programs for channel " + channel);
- }
- if (updateEpg(channel.getId(), programs)) {
- updated = true;
+ if (channels.isEmpty()) {
+ Log.i(TAG, "Failed to get EPG channels.");
+ retryFetchEpg(fastFetch);
+ return;
+ }
+ mFetchRetryCount = 0;
+ if (!fastFetch) {
+ for (Channel channel : channels) {
+ if (!mStarted) {
+ break;
+ }
+ List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channel.getId()));
+ Collections.sort(programs);
+ Log.i(TAG, "Fetched " + programs.size() + " programs for channel " + channel);
+ updateEpg(channel.getId(), programs);
}
+ setLastUpdatedEpgTimestamp(epgTimestamp);
+ } else {
+ handleFastFetch(channels, PROGRAM_FETCH_SHORT_DURATION_SEC);
+ if (DEBUG) Log.d(TAG, "First fast fetch Done.");
+ handleFastFetch(channels, PROGRAM_FETCH_LONG_DURATION_SEC);
+ if (DEBUG) Log.d(TAG, "Second fast fetch Done.");
}
- final boolean epgUpdated = updated;
- setLastUpdatedEpgTimestamp(epgTimestamp);
- mHandler.removeMessages(MSG_FETCH_EPG);
+ if (!fastFetch) {
+ mHandler.removeMessages(MSG_FETCH_EPG);
+ }
if (DEBUG) Log.d(TAG, "Fetching EPG is finished.");
+ // Start to fetch channel logos after epg fetching finished.
+ ChannelLogoFetcher.startFetchingChannelLogos(mContext, channels);
}
- @Nullable
- private String getLineupForAddress(Address address) {
- String lineup = null;
- if (TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) {
- String postalCode = address.getPostalCode();
- if (!TextUtils.isEmpty(postalCode)) {
- lineup = getLineupForPostalCode(postalCode);
+ private void retryFetchEpg(boolean fastFetch) {
+ if (mFetchRetryCount < NO_INFO_RETRY_LIMIT) {
+ postFetchRequest(fastFetch, NO_INFO_FETCHED_WAIT_MS * 1 << mFetchRetryCount);
+ mFetchRetryCount++;
+ } else {
+ mFetchRetryCount = 0;
+ }
+ }
+
+ private void handleFastFetch(List<Channel> channels, long duration) {
+ List<Long> channelIds = new ArrayList<>(channels.size());
+ for (Channel channel : channels) {
+ channelIds.add(channel.getId());
+ }
+ Map<Long, List<Program>> allPrograms = new HashMap<>();
+ List<Long> queryChannelIds = new ArrayList<>(QUERY_CHANNEL_COUNT);
+ for (Long channelId : channelIds) {
+ queryChannelIds.add(channelId);
+ if (queryChannelIds.size() >= QUERY_CHANNEL_COUNT) {
+ allPrograms.putAll(
+ new HashMap<>(mEpgReader.getPrograms(queryChannelIds, duration)));
+ queryChannelIds.clear();
}
}
- return lineup;
+ if (!queryChannelIds.isEmpty()) {
+ allPrograms.putAll(
+ new HashMap<>(mEpgReader.getPrograms(queryChannelIds, duration)));
+ }
+ for (Channel channel : channels) {
+ List<Program> programs = allPrograms.get(channel.getId());
+ if (programs == null) continue;
+ Collections.sort(programs);
+ Log.i(TAG, "Fast fetched " + programs.size() + " programs for channel " + channel);
+ updateEpg(channel.getId(), programs);
+ }
}
@Nullable
- private String getLineupForPostalCode(String postalCode) {
+ private String pickLineupForPostalCode(String postalCode) {
List<Lineup> lineups = mEpgReader.getLineups(postalCode);
+ int maxCount = 0;
+ String maxLineupId = null;
for (Lineup lineup : lineups) {
- // TODO(EPG): handle more than OTA digital
- if (lineup.type == Lineup.LINEUP_BROADCAST_DIGITAL) {
- if (DEBUG) Log.d(TAG, "Setting lineup to " + lineup.name + "(" + lineup.id + ")");
- return lineup.id;
+ int count = getMatchedChannelCount(lineup.id);
+ Log.i(TAG, lineup.name + " (" + lineup.id + ") - " + count + " matches");
+ if (count > maxCount) {
+ maxCount = count;
+ maxLineupId = lineup.id;
}
}
- return null;
+ return maxLineupId;
+ }
+
+ private int getMatchedChannelCount(String lineupId) {
+ // Construct a list of display numbers for existing channels.
+ List<Channel> channels = mChannelDataManager.getChannelList();
+ if (channels.isEmpty()) {
+ if (DEBUG) Log.d(TAG, "No existing channel to compare");
+ return 0;
+ }
+ List<String> numbers = new ArrayList<>(channels.size());
+ for (Channel c : channels) {
+ // We only support local channels from physical tuners.
+ if (c.isPhysicalTunerChannel()) {
+ numbers.add(c.getDisplayNumber());
+ }
+ }
+
+ numbers.retainAll(mEpgReader.getChannelNumbers(lineupId));
+ return numbers.size();
}
private long getLastUpdatedEpgTimestamp() {
@@ -357,16 +490,16 @@ public class EpgFetcher {
KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).commit();
}
- private String getLastLineupId() {
+ synchronized private String getLastLineupId() {
if (mLineupId == null) {
mLineupId = PreferenceManager.getDefaultSharedPreferences(mContext)
.getString(KEY_LAST_LINEUP_ID, null);
}
- if (DEBUG) Log.d(TAG, "Last lineup_id " + mLineupId);
+ if (DEBUG) Log.d(TAG, "Last lineup is " + mLineupId);
return mLineupId;
}
- private void setLastLineupId(String lineupId) {
+ synchronized private void setLastLineupId(String lineupId) {
mLineupId = lineupId;
PreferenceManager.getDefaultSharedPreferences(mContext).edit()
.putString(KEY_LAST_LINEUP_ID, lineupId).commit();
@@ -381,19 +514,9 @@ public class EpgFetcher {
long startTimeMs = System.currentTimeMillis();
long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION;
List<Program> oldPrograms = queryPrograms(channelId, startTimeMs, endTimeMs);
- Program currentOldProgram = oldPrograms.size() > 0 ? oldPrograms.get(0) : null;
int oldProgramsIndex = 0;
int newProgramsIndex = 0;
- // Skip the past programs. They will be automatically removed by the system.
- if (currentOldProgram != null) {
- long oldStartTimeUtcMillis = currentOldProgram.getStartTimeUtcMillis();
- for (Program program : newPrograms) {
- if (program.getEndTimeUtcMillis() > oldStartTimeUtcMillis) {
- break;
- }
- newProgramsIndex++;
- }
- }
+
// 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<ContentProviderOperation> ops = new ArrayList<>();
@@ -439,7 +562,7 @@ public class EpgFetcher {
}
if (addNewProgram) {
ops.add(ContentProviderOperation
- .newInsert(TvContract.Programs.CONTENT_URI)
+ .newInsert(Programs.CONTENT_URI)
.withValues(toContentValues(newProgram))
.build());
}
@@ -501,27 +624,25 @@ public class EpgFetcher {
@SuppressWarnings("deprecation")
private static ContentValues toContentValues(Program program) {
ContentValues values = new ContentValues();
- values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId());
- putValue(values, TvContract.Programs.COLUMN_TITLE, program.getTitle());
- putValue(values, TvContract.Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle());
+ values.put(Programs.COLUMN_CHANNEL_ID, program.getChannelId());
+ putValue(values, Programs.COLUMN_TITLE, program.getTitle());
+ putValue(values, Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle());
if (BuildCompat.isAtLeastN()) {
- putValue(values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER,
- program.getSeasonNumber());
- putValue(values, TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
- program.getEpisodeNumber());
+ putValue(values, Programs.COLUMN_SEASON_DISPLAY_NUMBER, program.getSeasonNumber());
+ putValue(values, Programs.COLUMN_EPISODE_DISPLAY_NUMBER, program.getEpisodeNumber());
} else {
- putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber());
- putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber());
+ putValue(values, Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber());
+ putValue(values, Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber());
}
- putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription());
- putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri());
- putValue(values, TvContract.Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri());
+ putValue(values, Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription());
+ putValue(values, Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription());
+ putValue(values, Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri());
+ putValue(values, Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri());
String[] canonicalGenres = program.getCanonicalGenres();
if (canonicalGenres != null && canonicalGenres.length > 0) {
- putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE,
- Genres.encode(canonicalGenres));
+ putValue(values, Programs.COLUMN_CANONICAL_GENRE, Genres.encode(canonicalGenres));
} else {
- putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, "");
+ putValue(values, Programs.COLUMN_CANONICAL_GENRE, "");
}
TvContentRating[] ratings = program.getContentRatings();
if (ratings != null && ratings.length > 0) {
@@ -530,14 +651,13 @@ public class EpgFetcher {
sb.append(CONTENT_RATING_SEPARATOR);
sb.append(ratings[i].flattenToString());
}
- putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, sb.toString());
+ putValue(values, Programs.COLUMN_CONTENT_RATING, sb.toString());
} else {
- putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, "");
+ putValue(values, Programs.COLUMN_CONTENT_RATING, "");
}
- values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
- program.getStartTimeUtcMillis());
- values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis());
- putValue(values, TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA,
+ values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, program.getStartTimeUtcMillis());
+ values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis());
+ putValue(values, Programs.COLUMN_INTERNAL_PROVIDER_DATA,
InternalDataUtils.serializeInternalProviderData(program));
return values;
}
@@ -569,6 +689,9 @@ public class EpgFetcher {
case MSG_FETCH_EPG:
epgFetcher.onFetchEpg();
break;
+ case MSG_FAST_FETCH_EPG:
+ epgFetcher.onFetchEpg(true);
+ break;
default:
super.handleMessage(msg);
break;
@@ -579,7 +702,7 @@ public class EpgFetcher {
private class EpgRunner implements Runnable {
@Override
public void run() {
- fetchEpg();
+ postFetchRequest(false, 0);
}
}
}
diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java
index 4f3b6f52..95cd933e 100644
--- a/src/com/android/tv/data/epg/EpgReader.java
+++ b/src/com/android/tv/data/epg/EpgReader.java
@@ -22,9 +22,10 @@ import android.support.annotation.WorkerThread;
import com.android.tv.data.Channel;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
-import com.android.tv.dvr.SeriesInfo;
+import com.android.tv.dvr.data.SeriesInfo;
import java.util.List;
+import java.util.Map;
/**
* An interface used to retrieve the EPG data. This class should be used in worker thread.
@@ -43,21 +44,37 @@ public interface EpgReader {
long getEpgTimestamp();
/**
- * Returns the channels list.
+ * Returns the lineups list.
+ */
+ List<Lineup> getLineups(@NonNull String postalCode);
+
+ /**
+ * Returns the list of channel numbers (unsorted) for the given lineup. The result is used to
+ * choose the most appropriate lineup among others by comparing the channel numbers of the
+ * existing channels on the device.
+ */
+ List<String> getChannelNumbers(@NonNull String lineupId);
+
+ /**
+ * Returns the list of channels for the given lineup. The returned channels should map into the
+ * existing channels on the device. This method is usually called after selecting the lineup.
*/
List<Channel> getChannels(@NonNull String lineupId);
/**
- * Returns the lineups list.
+ * Returns the programs for the given channel. Must call {@link #getChannels(String)}
+ * beforehand. Note that the {@code Program} doesn't have valid program ID because it's not
+ * retrieved from TvProvider.
*/
- List<Lineup> getLineups(@NonNull String postalCode);
+ List<Program> getPrograms(long channelId);
/**
- * Returns the programs for the given channel. The result is sorted by the start time.
+ * Returns the programs for the given channels.
* Note that the {@code Program} doesn't have valid program ID because it's not retrieved from
* TvProvider.
+ * This method is only used to get programs for a short duration typically.
*/
- List<Program> getPrograms(long channelId);
+ Map<Long, List<Program>> getPrograms(List<Long> channelIds, long duration);
/**
* Returns the series information for the given series ID.
diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java
index 64093f89..220daf22 100644
--- a/src/com/android/tv/data/epg/StubEpgReader.java
+++ b/src/com/android/tv/data/epg/StubEpgReader.java
@@ -21,10 +21,11 @@ import android.content.Context;
import com.android.tv.data.Channel;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
-import com.android.tv.dvr.SeriesInfo;
+import com.android.tv.dvr.data.SeriesInfo;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
/**
* A stub class to read EPG.
@@ -44,12 +45,17 @@ public class StubEpgReader implements EpgReader{
}
@Override
- public List<Channel> getChannels(String lineupId) {
+ public List<Lineup> getLineups(String postalCode) {
return Collections.emptyList();
}
@Override
- public List<Lineup> getLineups(String postalCode) {
+ public List<String> getChannelNumbers(String lineupId) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<Channel> getChannels(String lineupId) {
return Collections.emptyList();
}
@@ -59,6 +65,11 @@ public class StubEpgReader implements EpgReader{
}
@Override
+ public Map<Long, List<Program>> getPrograms(List<Long> channelIds, long duration) {
+ return Collections.emptyMap();
+ }
+
+ @Override
public SeriesInfo getSeriesInfo(String seriesId) {
return null;
}