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/BaseProgram.java39
-rw-r--r--src/com/android/tv/data/Channel.java114
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java200
-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/PreviewDataManager.java636
-rw-r--r--src/com/android/tv/data/PreviewProgramContent.java259
-rw-r--r--src/com/android/tv/data/Program.java140
-rw-r--r--src/com/android/tv/data/ProgramDataManager.java76
-rw-r--r--src/com/android/tv/data/StreamInfo.java4
-rw-r--r--src/com/android/tv/data/WatchedHistoryManager.java161
-rw-r--r--src/com/android/tv/data/epg/EpgFetchHelper.java233
-rw-r--r--src/com/android/tv/data/epg/EpgFetcher.java988
-rw-r--r--src/com/android/tv/data/epg/EpgReader.java45
-rw-r--r--src/com/android/tv/data/epg/StubEpgReader.java37
16 files changed, 2360 insertions, 952 deletions
diff --git a/src/com/android/tv/data/BaseProgram.java b/src/com/android/tv/data/BaseProgram.java
index f420de02..4e36c80a 100644
--- a/src/com/android/tv/data/BaseProgram.java
+++ b/src/com/android/tv/data/BaseProgram.java
@@ -17,12 +17,17 @@
package com.android.tv.data;
import android.content.Context;
+import android.media.tv.TvContentRating;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.tv.R;
import java.util.Comparator;
/**
* Base class for {@link com.android.tv.data.Program} and
- * {@link com.android.tv.dvr.RecordedProgram}.
+ * {@link com.android.tv.dvr.data.RecordedProgram}.
*/
public abstract class BaseProgram {
/**
@@ -94,14 +99,29 @@ public abstract class BaseProgram {
abstract public String getTitle();
/**
- * Returns the program's title withe its season and episode number.
+ * Returns the episode title.
*/
- abstract public String getTitleWithEpisodeNumber(Context context);
+ abstract public String getEpisodeTitle();
/**
* Returns the displayed title of the program episode.
*/
- abstract public String getEpisodeDisplayTitle(Context context);
+ public String getEpisodeDisplayTitle(Context context) {
+ if (!TextUtils.isEmpty(getEpisodeNumber())) {
+ String episodeTitle = getEpisodeTitle() == null ? "" : getEpisodeTitle();
+ if (TextUtils.equals(getSeasonNumber(), "0")) {
+ // Do not show "S0: ".
+ return String.format(context.getResources().getString(
+ R.string.display_episode_title_format_no_season_number),
+ getEpisodeNumber(), episodeTitle);
+ } else {
+ return String.format(context.getResources().getString(
+ R.string.display_episode_title_format),
+ getSeasonNumber(), getEpisodeNumber(), episodeTitle);
+ }
+ }
+ return getEpisodeTitle();
+ }
/**
* Returns the description of the program.
@@ -158,6 +178,10 @@ public abstract class BaseProgram {
*/
abstract public int[] getCanonicalGenreIds();
+ /** Returns the array of content ratings. */
+ @Nullable
+ abstract public TvContentRating[] getContentRatings();
+
/**
* Returns channel's ID of the program.
*/
@@ -169,6 +193,13 @@ public abstract class BaseProgram {
abstract public boolean isValid();
/**
+ * Checks whether the program is episodic or not.
+ */
+ public boolean isEpisodic() {
+ return getSeriesId() != null;
+ }
+
+ /**
* Generates the series ID for the other inputs than the tuner TV input.
*/
public static String generateSeriesId(String packageName, String title) {
diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java
index 30f84236..4a391ae7 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.
*/
@@ -81,15 +91,22 @@ public final class Channel {
TvContract.Channels.COLUMN_DESCRIPTION,
TvContract.Channels.COLUMN_VIDEO_FORMAT,
TvContract.Channels.COLUMN_BROWSABLE,
+ TvContract.Channels.COLUMN_SEARCHABLE,
TvContract.Channels.COLUMN_LOCKED,
TvContract.Channels.COLUMN_APP_LINK_TEXT,
TvContract.Channels.COLUMN_APP_LINK_COLOR,
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,28 +120,41 @@ 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++));
channel.mBrowsable = cursor.getInt(index++) == 1;
+ channel.mSearchable = cursor.getInt(index++) == 1;
channel.mLocked = cursor.getInt(index++) == 1;
channel.mAppLinkText = cursor.getString(index++);
channel.mAppLinkColor = cursor.getInt(index++);
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. */
@@ -138,6 +168,7 @@ public final class Channel {
private String mDescription;
private String mVideoFormat;
private boolean mBrowsable;
+ private boolean mSearchable;
private boolean mLocked;
private boolean mIsPassthrough;
private String mAppLinkText;
@@ -147,8 +178,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.
@@ -187,7 +220,6 @@ public final class Channel {
return mDisplayName;
}
- @VisibleForTesting
public String getDescription() {
return mDescription;
}
@@ -230,10 +262,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;
}
/**
@@ -266,6 +302,11 @@ public final class Channel {
return mBrowsable;
}
+ /** Checks whether this channel is searchable or not. */
+ public boolean isSearchable() {
+ return mSearchable;
+ }
+
public boolean isLocked() {
return mLocked;
}
@@ -279,6 +320,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 +346,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
@@ -314,8 +363,10 @@ public final class Channel {
+ ", videoFormat=" + mVideoFormat
+ ", isPassthrough=" + mIsPassthrough
+ ", browsable=" + mBrowsable
+ + ", searchable=" + mSearchable
+ ", locked=" + mLocked
- + ", appLinkText=" + mAppLinkText + "}";
+ + ", appLinkText=" + mAppLinkText
+ + ", recordingProhibited=" + mRecordingProhibited + "}";
}
void copyFrom(Channel other) {
@@ -332,6 +383,7 @@ public final class Channel {
mVideoFormat = other.mVideoFormat;
mIsPassthrough = other.mIsPassthrough;
mBrowsable = other.mBrowsable;
+ mSearchable = other.mSearchable;
mLocked = other.mLocked;
mAppLinkText = other.mAppLinkText;
mAppLinkColor = other.mAppLinkColor;
@@ -340,6 +392,8 @@ public final class Channel {
mAppLinkIntentUri = other.mAppLinkIntentUri;
mAppLinkIntent = other.mAppLinkIntent;
mAppLinkType = other.mAppLinkType;
+ mRecordingProhibited = other.mRecordingProhibited;
+ mChannelLogoExist = other.mChannelLogoExist;
}
/**
@@ -389,8 +443,7 @@ public final class Channel {
mChannel.mDisplayName = "name";
mChannel.mDescription = "description";
mChannel.mBrowsable = true;
- mChannel.mLocked = false;
- mChannel.mIsPassthrough = false;
+ mChannel.mSearchable = true;
}
public Builder(Channel other) {
@@ -422,7 +475,7 @@ public final class Channel {
@VisibleForTesting
public Builder setDisplayNumber(String displayNumber) {
- mChannel.mDisplayNumber = displayNumber;
+ mChannel.mDisplayNumber = normalizeDisplayNumber(displayNumber);
return this;
}
@@ -448,6 +501,11 @@ public final class Channel {
return this;
}
+ public Builder setSearchable(boolean searchable) {
+ mChannel.mSearchable = searchable;
+ return this;
+ }
+
public Builder setLocked(boolean locked) {
mChannel.mLocked = locked;
return this;
@@ -485,6 +543,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 +587,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 +733,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..6f93fbd1 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -21,13 +21,17 @@ 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.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
@@ -43,6 +47,7 @@ import com.android.tv.util.PermissionUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -59,7 +64,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
* This class is not thread-safe and under an assumption that its public methods are called in
* only the main thread.
*/
-@MainThread
+@AnyThread
public class ChannelDataManager {
private static final String TAG = "ChannelDataManager";
private static final boolean DEBUG = false;
@@ -74,10 +79,10 @@ public class ChannelDataManager {
private final List<Runnable> mPostRunnablesAfterChannelUpdate = new ArrayList<>();
private final Set<Listener> mListeners = new CopyOnWriteArraySet<>();
- private final Map<Long, ChannelWrapper> mChannelWrapperMap = new HashMap<>();
- private final Map<String, MutableInt> mChannelCountMap = new HashMap<>();
+ // Use container class to support multi-thread safety. This value can be set only on the main
+ // thread.
+ volatile private UnmodifiableChannelData mData = new UnmodifiableChannelData();
private final Channel.DefaultComparator mChannelComparator;
- private final List<Channel> mChannels = new ArrayList<>();
private final Handler mHandler;
private final Set<Long> mBrowsableUpdateChannelIds = new HashSet<>();
@@ -92,15 +97,17 @@ public class ChannelDataManager {
@Override
public void onInputAdded(String inputId) {
boolean channelAdded = false;
- for (ChannelWrapper channel : mChannelWrapperMap.values()) {
+ ChannelData data = new ChannelData(mData);
+ for (ChannelWrapper channel : mData.channelWrapperMap.values()) {
if (channel.mChannel.getInputId().equals(inputId)) {
channel.mInputRemoved = false;
- addChannel(channel.mChannel);
+ addChannel(data, channel.mChannel);
channelAdded = true;
}
}
if (channelAdded) {
- Collections.sort(mChannels, mChannelComparator);
+ Collections.sort(data.channels, mChannelComparator);
+ mData = new UnmodifiableChannelData(data);
notifyChannelListUpdated();
}
}
@@ -109,7 +116,7 @@ public class ChannelDataManager {
public void onInputRemoved(String inputId) {
boolean channelRemoved = false;
ArrayList<ChannelWrapper> removedChannels = new ArrayList<>();
- for (ChannelWrapper channel : mChannelWrapperMap.values()) {
+ for (ChannelWrapper channel : mData.channelWrapperMap.values()) {
if (channel.mChannel.getInputId().equals(inputId)) {
channel.mInputRemoved = true;
channelRemoved = true;
@@ -117,13 +124,15 @@ public class ChannelDataManager {
}
}
if (channelRemoved) {
- clearChannels();
- for (ChannelWrapper channelWrapper : mChannelWrapperMap.values()) {
+ ChannelData data = new ChannelData();
+ data.channelWrapperMap.putAll(mData.channelWrapperMap);
+ for (ChannelWrapper channelWrapper : data.channelWrapperMap.values()) {
if (!channelWrapper.mInputRemoved) {
- addChannel(channelWrapper.mChannel);
+ addChannel(data, channelWrapper.mChannel);
}
}
- Collections.sort(mChannels, mChannelComparator);
+ Collections.sort(data.channels, mChannelComparator);
+ mData = new UnmodifiableChannelData(data);
notifyChannelListUpdated();
for (ChannelWrapper channel : removedChannels) {
channel.notifyChannelRemoved();
@@ -132,10 +141,12 @@ public class ChannelDataManager {
}
};
+ @MainThread
public ChannelDataManager(Context context, TvInputManagerHelper inputManager) {
this(context, inputManager, context.getContentResolver());
}
+ @MainThread
@VisibleForTesting
ChannelDataManager(Context context, TvInputManagerHelper inputManager,
ContentResolver contentResolver) {
@@ -167,6 +178,7 @@ public class ChannelDataManager {
/**
* Starts the manager. If data is ready, {@link Listener#onLoadFinished()} will be called.
*/
+ @MainThread
public void start() {
if (mStarted) {
return;
@@ -184,6 +196,7 @@ public class ChannelDataManager {
* Stops the manager. It clears manager states and runs pending DB operations. Added listeners
* aren't automatically removed by this method.
*/
+ @MainThread
@VisibleForTesting
public void stop() {
if (!mStarted) {
@@ -192,12 +205,10 @@ public class ChannelDataManager {
mStarted = false;
mDbLoadFinished = false;
- ChannelLogoFetcher.stopFetchingChannelLogos();
mInputManager.removeCallback(mTvInputCallback);
mContentResolver.unregisterContentObserver(mChannelObserver);
mHandler.removeCallbacksAndMessages(null);
- mChannelWrapperMap.clear();
clearChannels();
mPostRunnablesAfterChannelUpdate.clear();
if (mChannelsUpdateTask != null) {
@@ -233,7 +244,7 @@ public class ChannelDataManager {
* Adds a {@link ChannelListener} for a specific channel with the channel ID {@code channelId}.
*/
public void addChannelListener(Long channelId, ChannelListener listener) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
+ ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId);
if (channelWrapper == null) {
return;
}
@@ -245,7 +256,7 @@ public class ChannelDataManager {
* {@code channelId}.
*/
public void removeChannelListener(Long channelId, ChannelListener listener) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
+ ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId);
if (channelWrapper == null) {
return;
}
@@ -263,14 +274,14 @@ public class ChannelDataManager {
* Returns the number of channels.
*/
public int getChannelCount() {
- return mChannels.size();
+ return mData.channels.size();
}
/**
* Returns a list of channels.
*/
public List<Channel> getChannelList() {
- return Collections.unmodifiableList(mChannels);
+ return new ArrayList<>(mData.channels);
}
/**
@@ -278,7 +289,7 @@ public class ChannelDataManager {
*/
public List<Channel> getBrowsableChannelList() {
List<Channel> channels = new ArrayList<>();
- for (Channel channel : mChannels) {
+ for (Channel channel : mData.channels) {
if (channel.isBrowsable()) {
channels.add(channel);
}
@@ -292,7 +303,7 @@ public class ChannelDataManager {
* @param inputId The ID of the input.
*/
public int getChannelCountForInput(String inputId) {
- MutableInt count = mChannelCountMap.get(inputId);
+ MutableInt count = mData.channelCountMap.get(inputId);
return count == null ? 0 : count.value;
}
@@ -303,17 +314,14 @@ public class ChannelDataManager {
* In that case this method is used to check if the channel exists in the DB.
*/
public boolean doesChannelExistInDb(long channelId) {
- return mChannelWrapperMap.get(channelId) != null;
+ return mData.channelWrapperMap.get(channelId) != null;
}
/**
* Returns true if and only if there exists at least one channel and all channels are hidden.
*/
public boolean areAllChannelsHidden() {
- if (mChannels.isEmpty()) {
- return false;
- }
- for (Channel channel : mChannels) {
+ for (Channel channel : mData.channels) {
if (channel.isBrowsable()) {
return false;
}
@@ -325,7 +333,7 @@ public class ChannelDataManager {
* Gets the channel with the channel ID {@code channelId}.
*/
public Channel getChannel(Long channelId) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
+ ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId);
if (channelWrapper == null || channelWrapper.mInputRemoved) {
return null;
}
@@ -349,7 +357,7 @@ public class ChannelDataManager {
*/
public void updateBrowsable(Long channelId, boolean browsable,
boolean skipNotifyChannelBrowsableChanged) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
+ ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId);
if (channelWrapper == null) {
return;
}
@@ -407,7 +415,7 @@ public class ChannelDataManager {
* The value change will be applied to DB when applyPendingDbOperation is called.
*/
public void updateLocked(Long channelId, boolean locked) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
+ ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId);
if (channelWrapper == null) {
return;
}
@@ -427,10 +435,11 @@ public class ChannelDataManager {
* to DB.
*/
public void applyUpdatedValuesToDb() {
+ ChannelData data = mData;
ArrayList<Long> browsableIds = new ArrayList<>();
ArrayList<Long> unbrowsableIds = new ArrayList<>();
for (Long id : mBrowsableUpdateChannelIds) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(id);
+ ChannelWrapper channelWrapper = data.channelWrapperMap.get(id);
if (channelWrapper == null) {
continue;
}
@@ -452,10 +461,10 @@ public class ChannelDataManager {
}
editor.apply();
} else {
- if (browsableIds.size() != 0) {
+ if (!browsableIds.isEmpty()) {
updateOneColumnValue(column, 1, browsableIds);
}
- if (unbrowsableIds.size() != 0) {
+ if (!unbrowsableIds.isEmpty()) {
updateOneColumnValue(column, 0, unbrowsableIds);
}
}
@@ -464,7 +473,7 @@ public class ChannelDataManager {
ArrayList<Long> lockedIds = new ArrayList<>();
ArrayList<Long> unlockedIds = new ArrayList<>();
for (Long id : mLockedUpdateChannelIds) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(id);
+ ChannelWrapper channelWrapper = data.channelWrapperMap.get(id);
if (channelWrapper == null) {
continue;
}
@@ -476,10 +485,10 @@ public class ChannelDataManager {
channelWrapper.mLockedInDb = channelWrapper.mChannel.isLocked();
}
column = TvContract.Channels.COLUMN_LOCKED;
- if (lockedIds.size() != 0) {
+ if (!lockedIds.isEmpty()) {
updateOneColumnValue(column, 1, lockedIds);
}
- if (unlockedIds.size() != 0) {
+ if (!unlockedIds.isEmpty()) {
updateOneColumnValue(column, 0, unlockedIds);
}
mLockedUpdateChannelIds.clear();
@@ -492,22 +501,24 @@ public class ChannelDataManager {
}
}
- private void addChannel(Channel channel) {
- mChannels.add(channel);
+ @MainThread
+ private void addChannel(ChannelData data, Channel channel) {
+ data.channels.add(channel);
String inputId = channel.getInputId();
- MutableInt count = mChannelCountMap.get(inputId);
+ MutableInt count = data.channelCountMap.get(inputId);
if (count == null) {
- mChannelCountMap.put(inputId, new MutableInt(1));
+ data.channelCountMap.put(inputId, new MutableInt(1));
} else {
count.value++;
}
}
+ @MainThread
private void clearChannels() {
- mChannels.clear();
- mChannelCountMap.clear();
+ mData = new UnmodifiableChannelData();
}
+ @MainThread
private void handleUpdateChannels() {
if (mChannelsUpdateTask != null) {
mChannelsUpdateTask.cancel(true);
@@ -525,6 +536,9 @@ public class ChannelDataManager {
}
}
+ /**
+ * A listener for ChannelDataManager. The callbacks are called on the main thread.
+ */
public interface Listener {
/**
* Called when data load is finished.
@@ -543,6 +557,9 @@ public class ChannelDataManager {
void onChannelBrowsableChanged();
}
+ /**
+ * A listener for individual channel change. The callbacks are called on the main thread.
+ */
public interface ChannelListener {
/**
* Called when the channel has been removed in DB.
@@ -590,9 +607,36 @@ public class ChannelDataManager {
}
}
+ private class CheckChannelLogoExistTask extends AsyncTask<Void, Void, Boolean> {
+ private final Channel mChannel;
+
+ CheckChannelLogoExistTask(Channel channel) {
+ mChannel = channel;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ try (AssetFileDescriptor f = mContext.getContentResolver().openAssetFileDescriptor(
+ TvContract.buildChannelLogoUri(mChannel.getId()), "r")) {
+ return true;
+ } catch (SQLiteException | IOException | NullPointerException e) {
+ // File not found or asset file not found.
+ }
+ return false;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ ChannelWrapper wrapper = mData.channelWrapperMap.get(mChannel.getId());
+ if (wrapper != null) {
+ wrapper.mChannel.setChannelLogoExist(result);
+ }
+ }
+ }
+
private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask {
- public QueryAllChannelsTask(ContentResolver contentResolver) {
+ QueryAllChannelsTask(ContentResolver contentResolver) {
super(contentResolver);
}
@@ -603,7 +647,9 @@ public class ChannelDataManager {
if (DEBUG) Log.e(TAG, "onPostExecute with null channels");
return;
}
- Set<Long> removedChannelIds = new HashSet<>(mChannelWrapperMap.keySet());
+ ChannelData data = new ChannelData();
+ data.channelWrapperMap.putAll(mData.channelWrapperMap);
+ Set<Long> removedChannelIds = new HashSet<>(data.channelWrapperMap.keySet());
List<ChannelWrapper> removedChannelWrappers = new ArrayList<>();
List<ChannelWrapper> updatedChannelWrappers = new ArrayList<>();
@@ -625,13 +671,15 @@ 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);
+ data.channelWrapperMap.put(channel.getId(), channelWrapper);
if (!channelWrapper.mInputRemoved) {
channelAdded = true;
}
} else {
- channelWrapper = mChannelWrapperMap.get(channelId);
+ channelWrapper = data.channelWrapperMap.get(channelId);
if (!channelWrapper.mChannel.hasSameReadOnlyInfo(channel)) {
// Channel data updated
Channel oldChannel = channelWrapper.mChannel;
@@ -640,9 +688,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);
@@ -663,19 +711,19 @@ public class ChannelDataManager {
}
for (long id : removedChannelIds) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.remove(id);
+ ChannelWrapper channelWrapper = data.channelWrapperMap.remove(id);
if (!channelWrapper.mInputRemoved) {
channelRemoved = true;
removedChannelWrappers.add(channelWrapper);
}
}
- clearChannels();
- for (ChannelWrapper channelWrapper : mChannelWrapperMap.values()) {
+ for (ChannelWrapper channelWrapper : data.channelWrapperMap.values()) {
if (!channelWrapper.mInputRemoved) {
- addChannel(channelWrapper.mChannel);
+ addChannel(data, channelWrapper.mChannel);
}
}
- Collections.sort(mChannels, mChannelComparator);
+ Collections.sort(data.channels, mChannelComparator);
+ mData = new UnmodifiableChannelData(data);
if (!mDbLoadFinished) {
mDbLoadFinished = true;
@@ -693,7 +741,6 @@ public class ChannelDataManager {
r.run();
}
mPostRunnablesAfterChannelUpdate.clear();
- ChannelLogoFetcher.startFetchingChannelLogos(mContext);
}
}
@@ -705,10 +752,9 @@ public class ChannelDataManager {
private void updateOneColumnValue(
final String columnName, final int columnValue, final List<Long> ids) {
if (!PermissionUtils.hasAccessAllEpg(mContext)) {
- // TODO: support this feature for non-system LC app. b/23939816
return;
}
- AsyncDbTask.execute(new Runnable() {
+ AsyncDbTask.executeOnDbThread(new Runnable() {
@Override
public void run() {
String selection = Utils.buildSelectionForIds(Channels._ID, ids);
@@ -723,6 +769,7 @@ public class ChannelDataManager {
return channel.getInputId() + "|" + channel.getId();
}
+ @MainThread
private static class ChannelDataManagerHandler extends WeakHandler<ChannelDataManager> {
public ChannelDataManagerHandler(ChannelDataManager channelDataManager) {
super(Looper.getMainLooper(), channelDataManager);
@@ -735,4 +782,51 @@ public class ChannelDataManager {
}
}
}
+
+ /**
+ * Container class which includes channel data that needs to be synced. This class is
+ * modifiable and used for changing channel data.
+ * e.g. TvInputCallback, or AsyncDbTask.onPostExecute.
+ */
+ @MainThread
+ private static class ChannelData {
+ final Map<Long, ChannelWrapper> channelWrapperMap;
+ final Map<String, MutableInt> channelCountMap;
+ final List<Channel> channels;
+
+ ChannelData() {
+ channelWrapperMap = new HashMap<>();
+ channelCountMap = new HashMap<>();
+ channels = new ArrayList<>();
+ }
+
+ ChannelData(ChannelData data) {
+ channelWrapperMap = new HashMap<>(data.channelWrapperMap);
+ channelCountMap = new HashMap<>(data.channelCountMap);
+ channels = new ArrayList<>(data.channels);
+ }
+
+ ChannelData(Map<Long, ChannelWrapper> channelWrapperMap,
+ Map<String, MutableInt> channelCountMap, List<Channel> channels) {
+ this.channelWrapperMap = channelWrapperMap;
+ this.channelCountMap = channelCountMap;
+ this.channels = channels;
+ }
+ }
+
+ /** Unmodifiable channel data. */
+ @MainThread
+ private static class UnmodifiableChannelData extends ChannelData {
+ UnmodifiableChannelData() {
+ super(Collections.unmodifiableMap(new HashMap<>()),
+ Collections.unmodifiableMap(new HashMap<>()),
+ Collections.unmodifiableList(new ArrayList<>()));
+ }
+
+ UnmodifiableChannelData(ChannelData data) {
+ super(Collections.unmodifiableMap(data.channelWrapperMap),
+ Collections.unmodifiableMap(data.channelCountMap),
+ Collections.unmodifiableList(data.channels));
+ }
+ }
}
diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java
index 5a549f83..132cab7a 100644
--- a/src/com/android/tv/data/ChannelLogoFetcher.java
+++ b/src/com/android/tv/data/ChannelLogoFetcher.java
@@ -16,160 +16,74 @@
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.MainThread;
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) {
+ @MainThread
+ public static 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);
+ sFetchTask = null;
}
- }
-
- /**
- * 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.getApplicationContext(), 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;
- public FetchLogoTask(Context context, List<Channel> channels) {
+ private FetchLogoTask(Context context, List<Channel> channels) {
mContext = context;
mChannels = channels;
}
@@ -180,83 +94,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 +148,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 +164,35 @@ 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);
- }
- }
- }
+ 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;
}
@Override
protected void onPostExecute(Void result) {
- synchronized (sLock) {
- if (sFetchTask == this) {
- sFetchTask = null;
- }
- }
+ sFetchTask = 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/PreviewDataManager.java b/src/com/android/tv/data/PreviewDataManager.java
new file mode 100644
index 00000000..01a58520
--- /dev/null
+++ b/src/com/android/tv/data/PreviewDataManager.java
@@ -0,0 +1,636 @@
+/*
+ * Copyright (C) 2017 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.data;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.support.annotation.IntDef;
+import android.support.annotation.MainThread;
+import android.support.media.tv.ChannelLogoUtils;
+import android.support.media.tv.PreviewProgram;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.tv.R;
+import com.android.tv.util.PermissionUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Class to manage the preview data.
+ */
+@TargetApi(Build.VERSION_CODES.O)
+@MainThread
+public class PreviewDataManager {
+ private static final String TAG = "PreviewDataManager";
+ // STOPSHIP: set it to false.
+ private static final boolean DEBUG = true;
+
+ /**
+ * Invalid preview channel ID.
+ */
+ public static final long INVALID_PREVIEW_CHANNEL_ID = -1;
+ @IntDef({TYPE_DEFAULT_PREVIEW_CHANNEL, TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PreviewChannelType{}
+
+ /**
+ * Type of default preview channel
+ */
+ public static final long TYPE_DEFAULT_PREVIEW_CHANNEL = 1;
+ /**
+ * Type of recorded program channel
+ */
+ public static final long TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL = 2;
+
+ private final Context mContext;
+ private final ContentResolver mContentResolver;
+ private boolean mLoadFinished;
+ private PreviewData mPreviewData = new PreviewData();
+ private final Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>();
+
+ private QueryPreviewDataTask mQueryPreviewTask;
+ private final Map<Long, CreatePreviewChannelTask> mCreatePreviewChannelTasks =
+ new HashMap<>();
+ private final Map<Long, UpdatePreviewProgramTask> mUpdatePreviewProgramTasks = new HashMap<>();
+
+ private final int mPreviewChannelLogoWidth;
+ private final int mPreviewChannelLogoHeight;
+
+ public PreviewDataManager(Context context) {
+ mContext = context.getApplicationContext();
+ mContentResolver = context.getContentResolver();
+ mPreviewChannelLogoWidth = mContext.getResources().getDimensionPixelSize(
+ R.dimen.preview_channel_logo_width);
+ mPreviewChannelLogoHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.preview_channel_logo_height);
+ }
+
+ /**
+ * Starts the preview data manager.
+ */
+ public void start() {
+ if (mQueryPreviewTask == null) {
+ mQueryPreviewTask = new QueryPreviewDataTask();
+ mQueryPreviewTask.execute();
+ }
+ }
+
+ /**
+ * Stops the preview data manager.
+ */
+ public void stop() {
+ if (mQueryPreviewTask != null) {
+ mQueryPreviewTask.cancel(true);
+ }
+ for (CreatePreviewChannelTask createPreviewChannelTask
+ : mCreatePreviewChannelTasks.values()) {
+ createPreviewChannelTask.cancel(true);
+ }
+ for (UpdatePreviewProgramTask updatePreviewProgramTask
+ : mUpdatePreviewProgramTasks.values()) {
+ updatePreviewProgramTask.cancel(true);
+ }
+
+ mQueryPreviewTask = null;
+ mCreatePreviewChannelTasks.clear();
+ mUpdatePreviewProgramTasks.clear();
+ }
+
+ /**
+ * Gets preview channel ID from the preview channel type.
+ */
+ public @PreviewChannelType long getPreviewChannelId(long previewChannelType) {
+ return mPreviewData.getPreviewChannelId(previewChannelType);
+ }
+
+ /**
+ * Creates default preview channel.
+ */
+ public void createDefaultPreviewChannel(
+ OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
+ createPreviewChannel(TYPE_DEFAULT_PREVIEW_CHANNEL, onPreviewChannelCreationResultListener);
+ }
+
+ /**
+ * Creates a preview channel for specific channel type.
+ */
+ public void createPreviewChannel(@PreviewChannelType long previewChannelType,
+ OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
+ CreatePreviewChannelTask currentRunningCreateTask =
+ mCreatePreviewChannelTasks.get(previewChannelType);
+ if (currentRunningCreateTask == null) {
+ CreatePreviewChannelTask createPreviewChannelTask = new CreatePreviewChannelTask(
+ previewChannelType);
+ createPreviewChannelTask.addOnPreviewChannelCreationResultListener(
+ onPreviewChannelCreationResultListener);
+ createPreviewChannelTask.execute();
+ mCreatePreviewChannelTasks.put(previewChannelType, createPreviewChannelTask);
+ } else {
+ currentRunningCreateTask.addOnPreviewChannelCreationResultListener(
+ onPreviewChannelCreationResultListener);
+ }
+ }
+
+ /**
+ * Returns {@code true} if the preview data is loaded.
+ */
+ public boolean isLoadFinished() {
+ return mLoadFinished;
+ }
+
+ /**
+ * Adds listener.
+ */
+ public void addListener(PreviewDataListener previewDataListener) {
+ mPreviewDataListeners.add(previewDataListener);
+ }
+
+ /**
+ * Removes listener.
+ */
+ public void removeListener(PreviewDataListener previewDataListener) {
+ mPreviewDataListeners.remove(previewDataListener);
+ }
+
+ /**
+ * Updates the preview programs table for a specific preview channel.
+ */
+ public void updatePreviewProgramsForChannel(long previewChannelId,
+ Set<PreviewProgramContent> programs, PreviewDataListener previewDataListener) {
+ UpdatePreviewProgramTask currentRunningUpdateTask =
+ mUpdatePreviewProgramTasks.get(previewChannelId);
+ if (currentRunningUpdateTask != null
+ && currentRunningUpdateTask.getPrograms().equals(programs)) {
+ currentRunningUpdateTask.addPreviewDataListener(previewDataListener);
+ return;
+ }
+ UpdatePreviewProgramTask updatePreviewProgramTask =
+ new UpdatePreviewProgramTask(previewChannelId, programs);
+ updatePreviewProgramTask.addPreviewDataListener(previewDataListener);
+ if (currentRunningUpdateTask != null) {
+ currentRunningUpdateTask.cancel(true);
+ currentRunningUpdateTask.saveStatus();
+ updatePreviewProgramTask.addPreviewDataListeners(
+ currentRunningUpdateTask.getPreviewDataListeners());
+ }
+ updatePreviewProgramTask.execute();
+ mUpdatePreviewProgramTasks.put(previewChannelId, updatePreviewProgramTask);
+ }
+
+ private void notifyPreviewDataLoadFinished() {
+ for (PreviewDataListener l : mPreviewDataListeners) {
+ l.onPreviewDataLoadFinished();
+ }
+ }
+
+ public interface PreviewDataListener {
+ /**
+ * Called when the preview data is loaded.
+ */
+ void onPreviewDataLoadFinished();
+
+ /**
+ * Called when the preview data is updated.
+ */
+ void onPreviewDataUpdateFinished();
+ }
+
+ public interface OnPreviewChannelCreationResultListener {
+ /**
+ * Called when the creation of preview channel is finished.
+ * @param createdPreviewChannelId The preview channel ID if created successfully,
+ * otherwise it's {@value #INVALID_PREVIEW_CHANNEL_ID}.
+ */
+ void onPreviewChannelCreationResult(long createdPreviewChannelId);
+ }
+
+ private final class QueryPreviewDataTask extends AsyncTask<Void, Void, PreviewData> {
+ private final String PARAM_PREVIEW = "preview";
+ private final String mChannelSelection = TvContract.Channels.COLUMN_PACKAGE_NAME + "=?";
+
+ @Override
+ protected PreviewData doInBackground(Void... voids) {
+ // Query preview channels and programs.
+ if (DEBUG) Log.d(TAG, "QueryPreviewDataTask.doInBackground");
+ PreviewData previewData = new PreviewData();
+ try {
+ Uri previewChannelsUri =
+ PreviewDataUtils.addQueryParamToUri(
+ TvContract.Channels.CONTENT_URI,
+ new Pair<>(PARAM_PREVIEW, String.valueOf(true)));
+ String packageName = mContext.getPackageName();
+ if (PermissionUtils.hasAccessAllEpg(mContext)) {
+ try (Cursor cursor =
+ mContentResolver.query(
+ previewChannelsUri,
+ android.support.media.tv.Channel.PROJECTION,
+ mChannelSelection,
+ new String[] {packageName},
+ null)) {
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ android.support.media.tv.Channel previewChannel =
+ android.support.media.tv.Channel.fromCursor(cursor);
+ Long previewChannelType = previewChannel.getInternalProviderFlag1();
+ if (previewChannelType != null) {
+ previewData.addPreviewChannelId(
+ previewChannelType, previewChannel.getId());
+ }
+ }
+ }
+ }
+ } else {
+ try (Cursor cursor =
+ mContentResolver.query(
+ previewChannelsUri,
+ android.support.media.tv.Channel.PROJECTION,
+ null,
+ null,
+ null)) {
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ android.support.media.tv.Channel previewChannel =
+ android.support.media.tv.Channel.fromCursor(cursor);
+ Long previewChannelType = previewChannel.getInternalProviderFlag1();
+ if (previewChannel.getPackageName() == packageName
+ && previewChannelType != null) {
+ previewData.addPreviewChannelId(
+ previewChannelType, previewChannel.getId());
+ }
+ }
+ }
+ }
+ }
+
+ for (long previewChannelId : previewData.getAllPreviewChannelIds().values()) {
+ Uri previewProgramsUriForPreviewChannel =
+ TvContract.buildPreviewProgramsUriForChannel(previewChannelId);
+ try (Cursor previewProgramCursor =
+ mContentResolver.query(
+ previewProgramsUriForPreviewChannel,
+ PreviewProgram.PROJECTION,
+ null,
+ null,
+ null)) {
+ if (previewProgramCursor != null) {
+ while (previewProgramCursor.moveToNext()) {
+ PreviewProgram previewProgram =
+ PreviewProgram.fromCursor(previewProgramCursor);
+ previewData.addPreviewProgram(previewProgram);
+ }
+ }
+ }
+ }
+ } catch (SQLException e) {
+ Log.w(TAG, "Unable to get preview data", e);
+ }
+ return previewData;
+ }
+
+ @Override
+ protected void onPostExecute(PreviewData result) {
+ super.onPostExecute(result);
+ if (mQueryPreviewTask == this) {
+ mQueryPreviewTask = null;
+ mPreviewData = new PreviewData(result);
+ mLoadFinished = true;
+ notifyPreviewDataLoadFinished();
+ }
+ }
+ }
+
+ private final class CreatePreviewChannelTask extends AsyncTask<Void, Void, Long> {
+ private final long mPreviewChannelType;
+ private Set<OnPreviewChannelCreationResultListener>
+ mOnPreviewChannelCreationResultListeners = new CopyOnWriteArraySet<>();
+
+ public CreatePreviewChannelTask(long previewChannelType) {
+ mPreviewChannelType = previewChannelType;
+ }
+
+ public void addOnPreviewChannelCreationResultListener(
+ OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
+ if (onPreviewChannelCreationResultListener != null) {
+ mOnPreviewChannelCreationResultListeners.add(
+ onPreviewChannelCreationResultListener);
+ }
+ }
+
+ @Override
+ protected Long doInBackground(Void... params) {
+ if (DEBUG) Log.d(TAG, "CreatePreviewChannelTask.doInBackground");
+ long previewChannelId;
+ try {
+ Uri channelUri = mContentResolver.insert(TvContract.Channels.CONTENT_URI,
+ PreviewDataUtils.createPreviewChannel(mContext, mPreviewChannelType)
+ .toContentValues());
+ if (channelUri != null) {
+ previewChannelId = ContentUris.parseId(channelUri);
+ } else {
+ Log.e(TAG, "Fail to insert preview channel");
+ return INVALID_PREVIEW_CHANNEL_ID;
+ }
+ } catch (UnsupportedOperationException | NumberFormatException e) {
+ Log.e(TAG, "Fail to get channel ID");
+ return INVALID_PREVIEW_CHANNEL_ID;
+ }
+ Drawable appIcon = mContext.getApplicationInfo().loadIcon(mContext.getPackageManager());
+ if (appIcon != null && appIcon instanceof BitmapDrawable) {
+ ChannelLogoUtils.storeChannelLogo(mContext, previewChannelId,
+ Bitmap.createScaledBitmap(((BitmapDrawable) appIcon).getBitmap(),
+ mPreviewChannelLogoWidth, mPreviewChannelLogoHeight, false));
+ }
+ return previewChannelId;
+ }
+
+ @Override
+ protected void onPostExecute(Long result) {
+ super.onPostExecute(result);
+ if (result != INVALID_PREVIEW_CHANNEL_ID) {
+ mPreviewData.addPreviewChannelId(mPreviewChannelType, result);
+ }
+ for (OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener
+ : mOnPreviewChannelCreationResultListeners) {
+ onPreviewChannelCreationResultListener.onPreviewChannelCreationResult(result);
+ }
+ mCreatePreviewChannelTasks.remove(mPreviewChannelType);
+ }
+ }
+
+ /**
+ * Updates the whole data which belongs to the package in preview programs table for a
+ * specific preview channel with a set of {@link PreviewProgramContent}.
+ */
+ private final class UpdatePreviewProgramTask extends AsyncTask<Void, Void, Void> {
+ private long mPreviewChannelId;
+ private Set<PreviewProgramContent> mPrograms;
+ private Map<Long, Long> mCurrentProgramId2PreviewProgramId;
+ private Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>();
+
+ public UpdatePreviewProgramTask(long previewChannelId,
+ Set<PreviewProgramContent> programs) {
+ mPreviewChannelId = previewChannelId;
+ mPrograms = programs;
+ if (mPreviewData.getPreviewProgramIds(previewChannelId) == null) {
+ mCurrentProgramId2PreviewProgramId = new HashMap<>();
+ } else {
+ mCurrentProgramId2PreviewProgramId = new HashMap<>(
+ mPreviewData.getPreviewProgramIds(previewChannelId));
+ }
+ }
+
+ public void addPreviewDataListener(PreviewDataListener previewDataListener) {
+ if (previewDataListener != null) {
+ mPreviewDataListeners.add(previewDataListener);
+ }
+ }
+
+ public void addPreviewDataListeners(Set<PreviewDataListener> previewDataListeners) {
+ if (previewDataListeners != null) {
+ mPreviewDataListeners.addAll(previewDataListeners);
+ }
+ }
+
+ public Set<PreviewProgramContent> getPrograms() {
+ return mPrograms;
+ }
+
+ public Set<PreviewDataListener> getPreviewDataListeners() {
+ return mPreviewDataListeners;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (DEBUG) Log.d(TAG, "UpdatePreviewProgamTask.doInBackground");
+ Map<Long, Long> uncheckedPrograms = new HashMap<>(mCurrentProgramId2PreviewProgramId);
+ for (PreviewProgramContent program : mPrograms) {
+ if (isCancelled()) {
+ return null;
+ }
+ Long existingPreviewProgramId = uncheckedPrograms.remove(program.getId());
+ if (existingPreviewProgramId != null) {
+ if (DEBUG) Log.d(TAG, "Preview program " + existingPreviewProgramId + " " +
+ "already exists for program " + program.getId());
+ continue;
+ }
+ try {
+ Uri programUri = mContentResolver.insert(TvContract.PreviewPrograms.CONTENT_URI,
+ PreviewDataUtils.createPreviewProgramFromContent(program)
+ .toContentValues());
+ if (programUri != null) {
+ long previewProgramId = ContentUris.parseId(programUri);
+ mCurrentProgramId2PreviewProgramId.put(program.getId(), previewProgramId);
+ if (DEBUG) Log.d(TAG, "Add new preview program " + previewProgramId);
+ } else {
+ Log.e(TAG, "Fail to insert preview program");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Fail to get preview program ID");
+ }
+ }
+
+ for (Long key : uncheckedPrograms.keySet()) {
+ if (isCancelled()) {
+ return null;
+ }
+ try {
+ if (DEBUG) Log.d(TAG, "Remove preview program " + uncheckedPrograms.get(key));
+ mContentResolver.delete(TvContract.buildPreviewProgramUri(
+ uncheckedPrograms.get(key)), null, null);
+ mCurrentProgramId2PreviewProgramId.remove(key);
+ } catch (Exception e) {
+ Log.e(TAG, "Fail to remove preview program " + uncheckedPrograms.get(key));
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ mPreviewData.setPreviewProgramIds(
+ mPreviewChannelId, mCurrentProgramId2PreviewProgramId);
+ mUpdatePreviewProgramTasks.remove(mPreviewChannelId);
+ for (PreviewDataListener previewDataListener : mPreviewDataListeners) {
+ previewDataListener.onPreviewDataUpdateFinished();
+ }
+ }
+
+ public void saveStatus() {
+ mPreviewData.setPreviewProgramIds(
+ mPreviewChannelId, mCurrentProgramId2PreviewProgramId);
+ }
+ }
+
+ /**
+ * Class to store the query result of preview data.
+ */
+ private static final class PreviewData {
+ private Map<Long, Long> mPreviewChannelType2Id = new HashMap<>();
+ private Map<Long, Map<Long, Long>> mProgramId2PreviewProgramId = new HashMap<>();
+
+ PreviewData() {
+ mPreviewChannelType2Id = new HashMap<>();
+ mProgramId2PreviewProgramId = new HashMap<>();
+ }
+
+ PreviewData(PreviewData previewData) {
+ mPreviewChannelType2Id = new HashMap<>(previewData.mPreviewChannelType2Id);
+ mProgramId2PreviewProgramId = new HashMap<>(previewData.mProgramId2PreviewProgramId);
+ }
+
+ public void addPreviewProgram(PreviewProgram previewProgram) {
+ long previewChannelId = previewProgram.getChannelId();
+ Map<Long, Long> programId2PreviewProgram =
+ mProgramId2PreviewProgramId.get(previewChannelId);
+ if (programId2PreviewProgram == null) {
+ programId2PreviewProgram = new HashMap<>();
+ }
+ mProgramId2PreviewProgramId.put(previewChannelId, programId2PreviewProgram);
+ if (previewProgram.getInternalProviderId() != null) {
+ programId2PreviewProgram.put(
+ Long.parseLong(previewProgram.getInternalProviderId()),
+ previewProgram.getId());
+ }
+ }
+
+ public @PreviewChannelType long getPreviewChannelId(long previewChannelType) {
+ Long result = mPreviewChannelType2Id.get(previewChannelType);
+ return result == null ? INVALID_PREVIEW_CHANNEL_ID : result;
+ }
+
+ public Map<Long, Long> getAllPreviewChannelIds() {
+ return mPreviewChannelType2Id;
+ }
+
+ public void addPreviewChannelId(long previewChannelType, long previewChannelId) {
+ mPreviewChannelType2Id.put(previewChannelType, previewChannelId);
+ }
+
+ public void removePreviewChannelId(long previewChannelType) {
+ mPreviewChannelType2Id.remove(previewChannelType);
+ }
+
+ public void removePreviewChannel(long previewChannelId) {
+ removePreviewChannelId(previewChannelId);
+ removePreviewProgramIds(previewChannelId);
+ }
+
+ public Map<Long, Long> getPreviewProgramIds(long previewChannelId) {
+ return mProgramId2PreviewProgramId.get(previewChannelId);
+ }
+
+ public Map<Long, Map<Long, Long>> getAllPreviewProgramIds() {
+ return mProgramId2PreviewProgramId;
+ }
+
+ public void setPreviewProgramIds(
+ long previewChannelId, Map<Long, Long> programId2PreviewProgramId) {
+ mProgramId2PreviewProgramId.put(previewChannelId, programId2PreviewProgramId);
+ }
+
+ public void removePreviewProgramIds(long previewChannelId) {
+ mProgramId2PreviewProgramId.remove(previewChannelId);
+ }
+ }
+
+ /**
+ * A utils class for preview data.
+ */
+ public final static class PreviewDataUtils {
+ /**
+ * Creates a preview channel.
+ */
+ public static android.support.media.tv.Channel createPreviewChannel(
+ Context context, @PreviewChannelType long previewChannelType) {
+ if (previewChannelType == TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL) {
+ return createRecordedProgramPreviewChannel(context, previewChannelType);
+ }
+ return createDefaultPreviewChannel(context, previewChannelType);
+ }
+
+ private static android.support.media.tv.Channel createDefaultPreviewChannel(
+ Context context, @PreviewChannelType long previewChannelType) {
+ android.support.media.tv.Channel.Builder builder =
+ new android.support.media.tv.Channel.Builder();
+ CharSequence appLabel =
+ context.getApplicationInfo().loadLabel(context.getPackageManager());
+ CharSequence appDescription =
+ context.getApplicationInfo().loadDescription(context.getPackageManager());
+ builder.setType(TvContract.Channels.TYPE_PREVIEW)
+ .setDisplayName(appLabel == null ? null : appLabel.toString())
+ .setDescription(appDescription == null ? null : appDescription.toString())
+ .setAppLinkIntentUri(TvContract.Channels.CONTENT_URI)
+ .setInternalProviderFlag1(previewChannelType);
+ return builder.build();
+ }
+
+ private static android.support.media.tv.Channel createRecordedProgramPreviewChannel(
+ Context context, @PreviewChannelType long previewChannelType) {
+ android.support.media.tv.Channel.Builder builder =
+ new android.support.media.tv.Channel.Builder();
+ builder.setType(TvContract.Channels.TYPE_PREVIEW)
+ .setDisplayName(context.getResources().getString(
+ R.string.recorded_programs_preview_channel))
+ .setAppLinkIntentUri(TvContract.Channels.CONTENT_URI)
+ .setInternalProviderFlag1(previewChannelType);
+ return builder.build();
+ }
+
+ /**
+ * Creates a preview program.
+ */
+ public static PreviewProgram createPreviewProgramFromContent(
+ PreviewProgramContent program) {
+ PreviewProgram.Builder builder = new PreviewProgram.Builder();
+ builder.setChannelId(program.getPreviewChannelId())
+ .setType(program.getType())
+ .setLive(program.getLive())
+ .setTitle(program.getTitle())
+ .setDescription(program.getDescription())
+ .setPosterArtUri(program.getPosterArtUri())
+ .setIntentUri(program.getIntentUri())
+ .setPreviewVideoUri(program.getPreviewVideoUri())
+ .setInternalProviderId(Long.toString(program.getId()));
+ return builder.build();
+ }
+
+ /**
+ * Appends query parameters to a Uri.
+ */
+ public static Uri addQueryParamToUri(Uri uri, Pair<String, String> param) {
+ return uri.buildUpon().appendQueryParameter(param.first, param.second).build();
+ }
+ }
+}
diff --git a/src/com/android/tv/data/PreviewProgramContent.java b/src/com/android/tv/data/PreviewProgramContent.java
new file mode 100644
index 00000000..39f5051d
--- /dev/null
+++ b/src/com/android/tv/data/PreviewProgramContent.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2017 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.data;
+
+import android.content.Context;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.data.RecordedProgram;
+
+import java.util.Objects;
+
+/**
+ * A class to store the content of preview programs.
+ */
+public class PreviewProgramContent {
+ private final static String PARAM_INPUT = "input";
+
+ private long mId;
+ private long mPreviewChannelId;
+ private int mType;
+ private boolean mLive;
+ private String mTitle;
+ private String mDescription;
+ private Uri mPosterArtUri;
+ private Uri mIntentUri;
+ private Uri mPreviewVideoUri;
+
+ /**
+ * Create preview program content from {@link Program}
+ */
+ public static PreviewProgramContent createFromProgram(Context context,
+ long previewChannelId, Program program) {
+ Channel channel = TvApplication.getSingletons(context).getChannelDataManager()
+ .getChannel(program.getChannelId());
+ if (channel == null) {
+ return null;
+ }
+ String channelDisplayName = channel.getDisplayName();
+ return new PreviewProgramContent.Builder()
+ .setId(program.getId())
+ .setPreviewChannelId(previewChannelId)
+ .setType(TvContract.PreviewPrograms.TYPE_CHANNEL)
+ .setLive(true)
+ .setTitle(program.getTitle())
+ .setDescription(!TextUtils.isEmpty(channelDisplayName)
+ ? channelDisplayName : channel.getDisplayNumber())
+ .setPosterArtUri(Uri.parse(program.getPosterArtUri()))
+ .setIntentUri(channel.getUri())
+ .setPreviewVideoUri(PreviewDataManager.PreviewDataUtils.addQueryParamToUri(
+ channel.getUri(), new Pair<>(PARAM_INPUT, channel.getInputId())))
+ .build();
+ }
+
+ /**
+ * Create preview program content from {@link RecordedProgram}
+ */
+ public static PreviewProgramContent createFromRecordedProgram(
+ Context context, long previewChannelId, RecordedProgram recordedProgram) {
+ Channel channel = TvApplication.getSingletons(context).getChannelDataManager()
+ .getChannel(recordedProgram.getChannelId());
+ String channelDisplayName = null;
+ if (channel != null) {
+ channelDisplayName = channel.getDisplayName();
+ }
+ Uri recordedProgramUri = TvContract.buildRecordedProgramUri(recordedProgram.getId());
+ return new PreviewProgramContent.Builder()
+ .setId(recordedProgram.getId())
+ .setPreviewChannelId(previewChannelId)
+ .setType(TvContract.PreviewPrograms.TYPE_CLIP)
+ .setTitle(recordedProgram.getTitle())
+ .setDescription(channelDisplayName != null ? channelDisplayName : "")
+ .setPosterArtUri(Uri.parse(recordedProgram.getPosterArtUri()))
+ .setIntentUri(recordedProgramUri)
+ .setPreviewVideoUri(PreviewDataManager.PreviewDataUtils.addQueryParamToUri(
+ recordedProgramUri, new Pair<>(PARAM_INPUT, recordedProgram.getInputId())))
+ .build();
+ }
+
+ private PreviewProgramContent() { }
+
+ public void copyFrom(PreviewProgramContent other) {
+ if (this == other) {
+ return;
+ }
+ mId = other.mId;
+ mPreviewChannelId = other.mPreviewChannelId;
+ mType = other.mType;
+ mLive = other.mLive;
+ mTitle = other.mTitle;
+ mDescription = other.mDescription;
+ mPosterArtUri = other.mPosterArtUri;
+ mIntentUri = other.mIntentUri;
+ mPreviewVideoUri = other.mPreviewVideoUri;
+ }
+
+ /**
+ * Returns the id, which is an identification. It usually comes from the original data which
+ * create the {@PreviewProgramContent}.
+ */
+ public long getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the preview channel id which the preview program belongs to.
+ */
+ public long getPreviewChannelId() {
+ return mPreviewChannelId;
+ }
+
+ /**
+ * Returns the type of the preview program.
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns whether the preview program is live or not.
+ */
+ public boolean getLive() {
+ return mLive;
+ }
+
+ /**
+ * Returns the title of the preview program.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns the description of the preview program.
+ */
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Returns the poster art uri of the preview program.
+ */
+ public Uri getPosterArtUri() {
+ return mPosterArtUri;
+ }
+
+ /**
+ * Returns the intent uri of the preview program.
+ */
+ public Uri getIntentUri() {
+ return mIntentUri;
+ }
+
+ /**
+ * Returns the preview video uri of the preview program.
+ */
+ public Uri getPreviewVideoUri() {
+ return mPreviewVideoUri;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof PreviewProgramContent)) {
+ return false;
+ }
+ PreviewProgramContent previewProgramContent = (PreviewProgramContent) other;
+ return previewProgramContent.mId == mId
+ && previewProgramContent.mPreviewChannelId == mPreviewChannelId
+ && previewProgramContent.mType == mType
+ && previewProgramContent.mLive == mLive
+ && Objects.equals(previewProgramContent.mTitle, mTitle)
+ && Objects.equals(previewProgramContent.mDescription, mDescription)
+ && Objects.equals(previewProgramContent.mPosterArtUri, mPosterArtUri)
+ && Objects.equals(previewProgramContent.mIntentUri, mIntentUri)
+ && Objects.equals(previewProgramContent.mPreviewVideoUri, mPreviewVideoUri);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mId, mPreviewChannelId, mType, mLive, mTitle, mDescription,
+ mPosterArtUri, mIntentUri, mPreviewVideoUri);
+ }
+
+ public static final class Builder {
+ private final PreviewProgramContent mPreviewProgramContent;
+
+ public Builder() {
+ mPreviewProgramContent = new PreviewProgramContent();
+ }
+
+ public Builder setId(long id) {
+ mPreviewProgramContent.mId = id;
+ return this;
+ }
+
+ public Builder setPreviewChannelId(long previewChannelId) {
+ mPreviewProgramContent.mPreviewChannelId = previewChannelId;
+ return this;
+ }
+
+ public Builder setType(int type) {
+ mPreviewProgramContent.mType = type;
+ return this;
+ }
+
+ public Builder setLive(boolean live) {
+ mPreviewProgramContent.mLive = live;
+ return this;
+ }
+
+ public Builder setTitle(String title) {
+ mPreviewProgramContent.mTitle = title;
+ return this;
+ }
+
+ public Builder setDescription(String description) {
+ mPreviewProgramContent.mDescription = description;
+ return this;
+ }
+
+ public Builder setPosterArtUri(Uri posterArtUri) {
+ mPreviewProgramContent.mPosterArtUri = posterArtUri;
+ return this;
+ }
+
+ public Builder setIntentUri(Uri intentUri) {
+ mPreviewProgramContent.mIntentUri = intentUri;
+ return this;
+ }
+
+ public Builder setPreviewVideoUri(Uri previewVideoUri) {
+ mPreviewProgramContent.mPreviewVideoUri = previewVideoUri;
+ return this;
+ }
+
+ public PreviewProgramContent build() {
+ PreviewProgramContent previewProgramContent = new PreviewProgramContent();
+ previewProgramContent.copyFrom(mPreviewProgramContent);
+ return previewProgramContent;
+ }
+ }
+}
diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java
index b9cd3d8d..071c7024 100644
--- a/src/com/android/tv/data/Program.java
+++ b/src/com/android/tv/data/Program.java
@@ -16,21 +16,23 @@
package com.android.tv.data;
+import android.annotation.SuppressLint;
+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.Programs;
+import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
-import android.support.v4.os.BuildCompat;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.R;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.CollectionUtils;
import com.android.tv.common.TvContentRatingCache;
@@ -88,9 +90,11 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
public static final String[] PROJECTION = createProjection();
private static String[] createProjection() {
- return CollectionUtils
- .concatAll(PROJECTION_BASE, BuildCompat.isAtLeastN() ? PROJECTION_ADDED_IN_NYC
- : PROJECTION_DEPRECATED_IN_NYC);
+ return CollectionUtils.concatAll(
+ PROJECTION_BASE,
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
+ ? PROJECTION_ADDED_IN_NYC
+ : PROJECTION_DEPRECATED_IN_NYC);
}
/**
@@ -135,7 +139,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder);
}
index++;
- if (BuildCompat.isAtLeastN()) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setSeasonNumber(cursor.getString(index++));
builder.setSeasonTitle(cursor.getString(index++));
builder.setEpisodeNumber(cursor.getString(index++));
@@ -213,11 +217,6 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
private TvContentRating[] mContentRatings;
private boolean mRecordingProhibited;
- /**
- * TODO(DVR): Need to fill the following data.
- */
- private boolean mRecordingScheduled;
-
private Program() {
// Do nothing.
}
@@ -268,46 +267,12 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
/**
* Returns the episode title.
*/
- public String getEpisodeTitle() {
- return mEpisodeTitle;
- }
-
- /**
- * Returns season number, episode number and episode title for display.
- */
@Override
- public String getEpisodeDisplayTitle(Context context) {
- if (!TextUtils.isEmpty(mEpisodeNumber)) {
- String episodeTitle = mEpisodeTitle == null ? "" : mEpisodeTitle;
- if (TextUtils.equals(mSeasonNumber, "0")) {
- // Do not show "S0: ".
- return String.format(context.getResources().getString(
- R.string.display_episode_title_format_no_season_number),
- mEpisodeNumber, episodeTitle);
- } else {
- return String.format(context.getResources().getString(
- R.string.display_episode_title_format),
- mSeasonNumber, mEpisodeNumber, episodeTitle);
- }
- }
+ public String getEpisodeTitle() {
return mEpisodeTitle;
}
@Override
- public String getTitleWithEpisodeNumber(Context context) {
- if (TextUtils.isEmpty(mTitle)) {
- return mTitle;
- }
- if (TextUtils.isEmpty(mSeasonNumber) || mSeasonNumber.equals("0")) {
- return TextUtils.isEmpty(mEpisodeNumber) ? mTitle : context.getString(
- R.string.program_title_with_episode_number_no_season, mTitle, mEpisodeNumber);
- } else {
- return context.getString(R.string.program_title_with_episode_number, mTitle,
- mSeasonNumber, mEpisodeNumber);
- }
- }
-
- @Override
public String getSeasonNumber() {
return mSeasonNumber;
}
@@ -361,6 +326,8 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
return mCriticScores;
}
+ @Nullable
+ @Override
public TvContentRating[] getContentRatings() {
return mContentRatings;
}
@@ -495,6 +462,63 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
return builder.append("}").toString();
}
+ /**
+ * Translates a {@link Program} to {@link ContentValues} that are ready to be written into
+ * Database.
+ */
+ @SuppressLint("InlinedApi")
+ @SuppressWarnings("deprecation")
+ public 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());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ putValue(values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER,
+ program.getSeasonNumber());
+ putValue(values, TvContract.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, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription());
+ putValue(values, TvContract.Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription());
+ putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri());
+ putValue(values, TvContract.Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri());
+ String[] canonicalGenres = program.getCanonicalGenres();
+ if (canonicalGenres != null && canonicalGenres.length > 0) {
+ putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE,
+ TvContract.Programs.Genres.encode(canonicalGenres));
+ } else {
+ putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, "");
+ }
+ putValue(values, Programs.COLUMN_CONTENT_RATING,
+ TvContentRatingCache.contentRatingsToString(program.getContentRatings()));
+ 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,
+ InternalDataUtils.serializeInternalProviderData(program));
+ return values;
+ }
+
+ private static void putValue(ContentValues contentValues, String key, String value) {
+ if (TextUtils.isEmpty(value)) {
+ contentValues.putNull(key);
+ } else {
+ contentValues.put(key, value);
+ }
+ }
+
+ private static void putValue(ContentValues contentValues, String key, byte[] value) {
+ if (value == null || value.length == 0) {
+ contentValues.putNull(key);
+ } else {
+ contentValues.put(key, value);
+ }
+ }
+
public void copyFrom(Program other) {
if (this == other) {
return;
@@ -524,13 +548,6 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
}
/**
- * Checks whether the program is episodic or not.
- */
- public boolean isEpisodic() {
- return mSeriesId != null;
- }
-
- /**
* A Builder for the Program class
*/
public static final class Builder {
@@ -799,8 +816,12 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
*/
public Program build() {
// Generate the series ID for the episodic program of other TV input.
- if (TextUtils.isEmpty(mProgram.mSeriesId)
+ if (TextUtils.isEmpty(mProgram.mTitle)) {
+ // If title is null, series cannot be generated for this program.
+ setSeriesId(null);
+ } else if (TextUtils.isEmpty(mProgram.mSeriesId)
&& !TextUtils.isEmpty(mProgram.mEpisodeNumber)) {
+ // If series ID is not set, generate it for the episodic program of other TV input.
setSeriesId(BaseProgram.generateSeriesId(mProgram.mPackageName, mProgram.mTitle));
}
Program program = new Program();
@@ -820,17 +841,20 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
}
/**
- * Loads the program poster art and returns it via {@code callback}.<p>
+ * Loads the program poster art and returns it via {@code callback}.
* <p>
* Note that it may directly call {@code callback} if the program poster art already is loaded.
+ *
+ * @return {@code true} if the load is complete and the callback is executed.
*/
@UiThread
- public void loadPosterArt(Context context, int posterArtWidth, int posterArtHeight,
+ public boolean loadPosterArt(Context context, int posterArtWidth, int posterArtHeight,
ImageLoader.ImageLoaderCallback callback) {
if (mPosterArtUri == null) {
- return;
+ return false;
}
- ImageLoader.loadBitmap(context, mPosterArtUri, posterArtWidth, posterArtHeight, callback);
+ return ImageLoader.loadBitmap(
+ context, mPosterArtUri, posterArtWidth, posterArtHeight, callback);
}
public static boolean isDuplicate(Program p1, Program p2) {
diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java
index d2af33a7..8cb5e74a 100644
--- a/src/com/android/tv/data/ProgramDataManager.java
+++ b/src/com/android/tv/data/ProgramDataManager.java
@@ -26,6 +26,7 @@ import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
import android.util.ArraySet;
@@ -35,8 +36,6 @@ import android.util.LruCache;
import com.android.tv.common.MemoryManageable;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.data.epg.EpgFetcher;
-import com.android.tv.experiments.Experiments;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.Clock;
import com.android.tv.util.MultiLongSparseArray;
@@ -51,6 +50,7 @@ import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@MainThread
@@ -85,10 +85,12 @@ public class ProgramDataManager implements MemoryManageable {
private final Clock mClock;
private final ContentResolver mContentResolver;
private boolean mStarted;
+ // Updated only on the main thread.
+ private volatile boolean mCurrentProgramsLoadFinished;
private ProgramsUpdateTask mProgramsUpdateTask;
private final LongSparseArray<UpdateCurrentProgramForChannelTask> mProgramUpdateTaskMap =
new LongSparseArray<>();
- private final Map<Long, Program> mChannelIdCurrentProgramMap = new HashMap<>();
+ private final Map<Long, Program> mChannelIdCurrentProgramMap = new ConcurrentHashMap<>();
private final MultiLongSparseArray<OnCurrentProgramUpdatedListener>
mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>();
private final Handler mHandler;
@@ -109,17 +111,14 @@ public class ProgramDataManager implements MemoryManageable {
private boolean mPauseProgramUpdate = false;
private final LruCache<Long, Program> mZeroLengthProgramCache = new LruCache<>(10);
- private final EpgFetcher mEpgFetcher;
+ @MainThread
public ProgramDataManager(Context context) {
- this(context.getContentResolver(), Clock.SYSTEM, Looper.myLooper(),
- EpgFetcher.getInstance(context));
+ this(context.getContentResolver(), Clock.SYSTEM, Looper.myLooper());
}
@VisibleForTesting
- ProgramDataManager(ContentResolver contentResolver, Clock time, Looper looper,
- EpgFetcher epgFetcher) {
- mEpgFetcher = epgFetcher;
+ ProgramDataManager(ContentResolver contentResolver, Clock time, Looper looper) {
mClock = time;
mContentResolver = contentResolver;
mHandler = new MyHandler(looper);
@@ -175,9 +174,6 @@ public class ProgramDataManager implements MemoryManageable {
}
mContentResolver.registerContentObserver(Programs.CONTENT_URI,
true, mProgramObserver);
- if (mEpgFetcher != null && Experiments.CLOUD_EPG.get()) {
- mEpgFetcher.start();
- }
}
/**
@@ -190,10 +186,6 @@ public class ProgramDataManager implements MemoryManageable {
return;
}
mStarted = false;
-
- if (mEpgFetcher != null) {
- mEpgFetcher.stop();
- }
mContentResolver.unregisterContentObserver(mProgramObserver);
mHandler.removeCallbacksAndMessages(null);
@@ -205,13 +197,23 @@ public class ProgramDataManager implements MemoryManageable {
}
}
- /**
- * Returns the current program at the specified channel.
- */
+ @AnyThread
+ public boolean isCurrentProgramsLoadFinished() {
+ return mCurrentProgramsLoadFinished;
+ }
+
+ /** Returns the current program at the specified channel. */
+ @AnyThread
public Program getCurrentProgram(long channelId) {
return mChannelIdCurrentProgramMap.get(channelId);
}
+ /** Returns all the current programs. */
+ @AnyThread
+ public List<Program> getCurrentPrograms() {
+ return new ArrayList<>(mChannelIdCurrentProgramMap.values());
+ }
+
/**
* Reloads program data.
*/
@@ -338,19 +340,19 @@ public class ProgramDataManager implements MemoryManageable {
}
private void notifyCurrentProgramUpdate(long channelId, Program program) {
-
for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners
.get(channelId)) {
listener.onCurrentProgramUpdated(channelId, program);
- }
+ }
for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners
.get(Channel.INVALID_ID)) {
listener.onCurrentProgramUpdated(channelId, program);
- }
+ }
}
private void updateCurrentProgram(long channelId, Program program) {
- Program previousProgram = mChannelIdCurrentProgramMap.put(channelId, program);
+ Program previousProgram = program == null ? mChannelIdCurrentProgramMap.remove(channelId)
+ : mChannelIdCurrentProgramMap.put(channelId, program);
if (!Objects.equals(program, previousProgram)) {
if (mPrefetchEnabled) {
removePreviousProgramsAndUpdateCurrentProgramInCache(channelId, program);
@@ -581,22 +583,22 @@ public class ProgramDataManager implements MemoryManageable {
protected void onPostExecute(List<Program> programs) {
if (DEBUG) Log.d(TAG, "ProgramsUpdateTask done");
mProgramsUpdateTask = null;
- if (programs == null) {
- return;
- }
- Set<Long> removedChannelIds = new HashSet<>(mChannelIdCurrentProgramMap.keySet());
- for (Program program : programs) {
- long channelId = program.getChannelId();
- updateCurrentProgram(channelId, program);
- removedChannelIds.remove(channelId);
- }
- for (Long channelId : removedChannelIds) {
- if (mPrefetchEnabled) {
- mChannelIdProgramCache.remove(channelId);
+ if (programs != null) {
+ Set<Long> removedChannelIds = new HashSet<>(mChannelIdCurrentProgramMap.keySet());
+ for (Program program : programs) {
+ long channelId = program.getChannelId();
+ updateCurrentProgram(channelId, program);
+ removedChannelIds.remove(channelId);
+ }
+ for (Long channelId : removedChannelIds) {
+ if (mPrefetchEnabled) {
+ mChannelIdProgramCache.remove(channelId);
+ }
+ mChannelIdCurrentProgramMap.remove(channelId);
+ notifyCurrentProgramUpdate(channelId, null);
}
- mChannelIdCurrentProgramMap.remove(channelId);
- notifyCurrentProgramUpdate(channelId, null);
}
+ mCurrentProgramsLoadFinished = true;
}
}
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/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java
index 59319338..3edd7b1a 100644
--- a/src/com/android/tv/data/WatchedHistoryManager.java
+++ b/src/com/android/tv/data/WatchedHistoryManager.java
@@ -10,15 +10,14 @@ import android.os.Looper;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
import android.util.Log;
import com.android.tv.common.SharedPreferencesUtils;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
@@ -28,15 +27,15 @@ import java.util.concurrent.TimeUnit;
*
* <p>When there is no access to watched table of TvProvider,
* this class is used to build up watched history and to compute recent channels.
+ * <p>Note that this class is not thread safe. Please use this on one thread.
*/
public class WatchedHistoryManager {
private final static String TAG = "WatchedHistoryManager";
- private final boolean DEBUG = false;
+ private final static boolean DEBUG = false;
private static final int MAX_HISTORY_SIZE = 10000;
private static final String PREF_KEY_LAST_INDEX = "last_index";
private static final long MIN_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
- private static final long RECENT_CHANNEL_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5);
private final List<WatchedRecord> mWatchedHistory = new ArrayList<>();
private final List<WatchedRecord> mPendingRecords = new ArrayList<>();
@@ -92,11 +91,7 @@ public class WatchedHistoryManager {
WatchedHistoryManager(Context context, int maxHistorySize) {
mContext = context.getApplicationContext();
mMaxHistorySize = maxHistorySize;
- if (Looper.myLooper() == null) {
- mHandler = new Handler(Looper.getMainLooper());
- } else {
- mHandler = new Handler();
- }
+ mHandler = new Handler();
}
/**
@@ -107,56 +102,70 @@ public class WatchedHistoryManager {
return;
}
mStarted = true;
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- mSharedPreferences = mContext.getSharedPreferences(
- SharedPreferencesUtils.SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE);
- mLastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
- if (mLastIndex >= 0 && mLastIndex < mMaxHistorySize) {
- for (int i = 0; i <= mLastIndex; ++i) {
- WatchedRecord record =
- decode(mSharedPreferences.getString(getSharedPreferencesKey(i),
- null));
- if (record != null) {
- mWatchedHistory.add(record);
- }
- }
- } else if (mLastIndex >= mMaxHistorySize) {
- for (long i = mLastIndex - mMaxHistorySize + 1; i <= mLastIndex; ++i) {
- WatchedRecord record = decode(mSharedPreferences.getString(
- getSharedPreferencesKey(i), null));
- if (record != null) {
- mWatchedHistory.add(record);
- }
- }
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ loadWatchedHistory();
+ return null;
}
- return null;
- }
- @Override
- protected void onPostExecute(Void params) {
- mLoaded = true;
- if (DEBUG) {
- Log.d(TAG, "Loaded: size=" + mWatchedHistory.size() + " index=" + mLastIndex);
+ @Override
+ protected void onPostExecute(Void params) {
+ onLoadFinished();
}
- if (!mPendingRecords.isEmpty()) {
- Editor editor = mSharedPreferences.edit();
- for (WatchedRecord record : mPendingRecords) {
- mWatchedHistory.add(record);
- ++mLastIndex;
- editor.putString(getSharedPreferencesKey(mLastIndex), encode(record));
- }
- editor.putLong(PREF_KEY_LAST_INDEX, mLastIndex).apply();
- mPendingRecords.clear();
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ loadWatchedHistory();
+ onLoadFinished();
+ }
+ }
+
+ @WorkerThread
+ private void loadWatchedHistory() {
+ mSharedPreferences = mContext.getSharedPreferences(
+ SharedPreferencesUtils.SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE);
+ mLastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
+ if (mLastIndex >= 0 && mLastIndex < mMaxHistorySize) {
+ for (int i = 0; i <= mLastIndex; ++i) {
+ WatchedRecord record =
+ decode(mSharedPreferences.getString(getSharedPreferencesKey(i),
+ null));
+ if (record != null) {
+ mWatchedHistory.add(record);
}
- if (mListener != null) {
- mListener.onLoadFinished();
+ }
+ } else if (mLastIndex >= mMaxHistorySize) {
+ for (long i = mLastIndex - mMaxHistorySize + 1; i <= mLastIndex; ++i) {
+ WatchedRecord record = decode(mSharedPreferences.getString(
+ getSharedPreferencesKey(i), null));
+ if (record != null) {
+ mWatchedHistory.add(record);
}
- mSharedPreferences.registerOnSharedPreferenceChangeListener(
- mOnSharedPreferenceChangeListener);
}
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ private void onLoadFinished() {
+ mLoaded = true;
+ if (DEBUG) {
+ Log.d(TAG, "Loaded: size=" + mWatchedHistory.size() + " index=" + mLastIndex);
+ }
+ if (!mPendingRecords.isEmpty()) {
+ Editor editor = mSharedPreferences.edit();
+ for (WatchedRecord record : mPendingRecords) {
+ mWatchedHistory.add(record);
+ ++mLastIndex;
+ editor.putString(getSharedPreferencesKey(mLastIndex), encode(record));
+ }
+ editor.putLong(PREF_KEY_LAST_INDEX, mLastIndex).apply();
+ mPendingRecords.clear();
+ }
+ if (mListener != null) {
+ mListener.onLoadFinished();
+ }
+ mSharedPreferences.registerOnSharedPreferenceChangeListener(
+ mOnSharedPreferenceChangeListener);
}
@VisibleForTesting
@@ -204,52 +213,6 @@ public class WatchedHistoryManager {
return Collections.unmodifiableList(mWatchedHistory);
}
- /**
- * Returns the list of recently watched channels.
- */
- public List<Channel> buildRecentChannel(ChannelDataManager channelDataManager, int maxCount) {
- List<Channel> list = new ArrayList<>();
- Map<Long, Long> durationMap = new HashMap<>();
- for (int i = mWatchedHistory.size() - 1; i >= 0; --i) {
- WatchedRecord record = mWatchedHistory.get(i);
- long channelId = record.channelId;
- Channel channel = channelDataManager.getChannel(channelId);
- if (channel == null || !channel.isBrowsable()) {
- continue;
- }
- Long duration = durationMap.get(channelId);
- if (duration == null) {
- duration = 0L;
- }
- if (duration >= RECENT_CHANNEL_THRESHOLD_MS) {
- continue;
- }
- if (list.isEmpty()) {
- // We put the first recent channel regardless of RECENT_CHANNEL_THREASHOLD.
- // It has the similar functionality as the previous channel in a usual remote
- // controller.
- list.add(channel);
- durationMap.put(channelId, RECENT_CHANNEL_THRESHOLD_MS);
- } else {
- duration += record.duration;
- durationMap.put(channelId, duration);
- if (duration >= RECENT_CHANNEL_THRESHOLD_MS) {
- list.add(channel);
- }
- }
- if (list.size() >= maxCount) {
- break;
- }
- }
- if (DEBUG) {
- Log.d(TAG, "Build recent channel");
- for (Channel channel : list) {
- Log.d(TAG, "recent channel: " + channel);
- }
- }
- return list;
- }
-
@VisibleForTesting
WatchedRecord getRecord(int reverseIndex) {
return mWatchedHistory.get(mWatchedHistory.size() - 1 - reverseIndex);
diff --git a/src/com/android/tv/data/epg/EpgFetchHelper.java b/src/com/android/tv/data/epg/EpgFetchHelper.java
new file mode 100644
index 00000000..5693c877
--- /dev/null
+++ b/src/com/android/tv/data/epg/EpgFetchHelper.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2017 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.data.epg;
+
+import android.content.ContentProviderOperation;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs;
+import android.os.RemoteException;
+import android.preference.PreferenceManager;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.data.Program;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/** The helper class for {@link com.android.tv.data.epg.EpgFetcher} */
+class EpgFetchHelper {
+ private static final String TAG = "EpgFetchHelper";
+ private static final boolean DEBUG = false;
+
+ private static final long PROGRAM_QUERY_DURATION_MS = TimeUnit.DAYS.toMillis(30);
+ private static final int BATCH_OPERATION_COUNT = 100;
+
+ // Value: Long
+ private static final String KEY_LAST_UPDATED_EPG_TIMESTAMP =
+ "com.android.tv.data.epg.EpgFetcher.LastUpdatedEpgTimestamp";
+ // Value: String
+ private static final String KEY_LAST_LINEUP_ID =
+ "com.android.tv.data.epg.EpgFetcher.LastLineupId";
+
+ private static long sLastEpgUpdatedTimestamp = -1;
+ private static String sLastLineupId;
+
+ private EpgFetchHelper() { }
+
+ /**
+ * Updates newly fetched EPG data for the given channel to local providers. The method will
+ * compare the broadcasting time and try to match each newly fetched program with old programs
+ * of that channel in the database one by one. It will update the matched old program, or insert
+ * the new program if there is no matching program can be found in the database and at the same
+ * time remove those old programs which conflicts with the inserted one.
+
+ * @param channelId the target channel ID.
+ * @param fetchedPrograms the newly fetched program data.
+ * @return {@code true} if new program data are successfully updated. Otherwise {@code false}.
+ */
+ static boolean updateEpgData(Context context, long channelId, List<Program> fetchedPrograms) {
+ final int fetchedProgramsCount = fetchedPrograms.size();
+ if (fetchedProgramsCount == 0) {
+ return false;
+ }
+ boolean updated = false;
+ long startTimeMs = System.currentTimeMillis();
+ long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION_MS;
+ List<Program> oldPrograms = queryPrograms(context, channelId, startTimeMs, endTimeMs);
+ int oldProgramsIndex = 0;
+ int newProgramsIndex = 0;
+
+ // 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<>();
+ while (newProgramsIndex < fetchedProgramsCount) {
+ Program oldProgram = oldProgramsIndex < oldPrograms.size()
+ ? oldPrograms.get(oldProgramsIndex) : null;
+ Program newProgram = fetchedPrograms.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 (hasSameTitleAndOverlap(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.getId()))
+ .withValues(Program.toContentValues(newProgram))
+ .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.getId()))
+ .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(Programs.CONTENT_URI)
+ .withValues(Program.toContentValues(newProgram))
+ .build());
+ }
+ // Throttle the batch operation not to cause TransactionTooLargeException.
+ if (ops.size() > BATCH_OPERATION_COUNT || newProgramsIndex >= fetchedProgramsCount) {
+ try {
+ if (DEBUG) {
+ int size = ops.size();
+ Log.d(TAG, "Running " + size + " operations for channel " + channelId);
+ for (int i = 0; i < size; ++i) {
+ Log.d(TAG, "Operation(" + i + "): " + ops.get(i));
+ }
+ }
+ context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ updated = true;
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Failed to insert programs.", e);
+ return updated;
+ }
+ ops.clear();
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Updated " + fetchedProgramsCount + " programs for channel " + channelId);
+ }
+ return updated;
+ }
+
+ private static List<Program> queryPrograms(Context context, long channelId,
+ long startTimeMs, long endTimeMs) {
+ try (Cursor c = context.getContentResolver().query(
+ TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
+ Program.PROJECTION, null, null, Programs.COLUMN_START_TIME_UTC_MILLIS)) {
+ if (c == null) {
+ return Collections.emptyList();
+ }
+ ArrayList<Program> programs = new ArrayList<>();
+ while (c.moveToNext()) {
+ programs.add(Program.fromCursor(c));
+ }
+ return programs;
+ }
+ }
+
+ /**
+ * Returns {@code true} if the {@code oldProgram} needs to be updated with the
+ * {@code newProgram}.
+ */
+ private static boolean hasSameTitleAndOverlap(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 TextUtils.equals(oldProgram.getTitle(), newProgram.getTitle())
+ && oldProgram.getStartTimeUtcMillis() <= newProgram.getEndTimeUtcMillis()
+ && newProgram.getStartTimeUtcMillis() <= oldProgram.getEndTimeUtcMillis();
+ }
+
+ /**
+ * Sets the last known lineup ID into shared preferences for future usage. If channels are not
+ * re-scanned, EPG fetcher can directly use this value instead of checking the correct lineup ID
+ * every time when it needs to fetch EPG data.
+ */
+ @WorkerThread
+ synchronized static void setLastLineupId(Context context, String lineupId) {
+ if (DEBUG) {
+ if (lineupId == null) {
+ Log.d(TAG, "Clear stored lineup id: " + sLastLineupId);
+ }
+ }
+ sLastLineupId = lineupId;
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .putString(KEY_LAST_LINEUP_ID, lineupId).apply();
+ }
+
+ /**
+ * Gets the last known lineup ID from shared preferences.
+ */
+ synchronized static String getLastLineupId(Context context) {
+ if (sLastLineupId == null) {
+ sLastLineupId = PreferenceManager.getDefaultSharedPreferences(context)
+ .getString(KEY_LAST_LINEUP_ID, null);
+ }
+ if (DEBUG) Log.d(TAG, "Last lineup is " + sLastLineupId);
+ return sLastLineupId;
+ }
+
+ /**
+ * Sets the last updated timestamp of EPG data into shared preferences. If the EPG data is not
+ * out-dated, it's not necessary for EPG fetcher to fetch EPG again.
+ */
+ @WorkerThread
+ synchronized static void setLastEpgUpdatedTimestamp(Context context, long timestamp) {
+ sLastEpgUpdatedTimestamp = timestamp;
+ PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(
+ KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).apply();
+ }
+
+ /**
+ * Gets the last updated timestamp of EPG data.
+ */
+ synchronized static long getLastEpgUpdatedTimestamp(Context context) {
+ if (sLastEpgUpdatedTimestamp < 0) {
+ sLastEpgUpdatedTimestamp = PreferenceManager.getDefaultSharedPreferences(context)
+ .getLong(KEY_LAST_UPDATED_EPG_TIMESTAMP, 0);
+ }
+ return sLastEpgUpdatedTimestamp;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java
index 3b093b6a..24f8b826 100644
--- a/src/com/android/tv/data/epg/EpgFetcher.java
+++ b/src/com/android/tv/data/epg/EpgFetcher.java
@@ -16,570 +16,720 @@
package com.android.tv.data.epg;
-import android.Manifest;
-import android.annotation.SuppressLint;
-import android.content.ContentProviderOperation;
-import android.content.ContentValues;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
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;
-import android.media.tv.TvContract;
-import android.media.tv.TvContract.Programs;
-import android.media.tv.TvContract.Programs.Genres;
import android.media.tv.TvInputInfo;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
-import android.os.RemoteException;
-import android.preference.PreferenceManager;
+import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.v4.os.BuildCompat;
+import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.Features;
import com.android.tv.TvApplication;
-import com.android.tv.common.WeakHandler;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.TvCommonUtils;
+import com.android.tv.config.RemoteConfigUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
-import com.android.tv.data.InternalDataUtils;
+import com.android.tv.data.ChannelLogoFetcher;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
+import com.android.tv.perf.EventNames;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
+import com.android.tv.tuner.util.PostalCodeUtils;
import com.android.tv.util.LocationUtils;
-import com.android.tv.util.RecurringRunner;
+import com.android.tv.util.NetworkTrafficTags;
import com.android.tv.util.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
-import java.util.Objects;
+import java.util.Map;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
- * An utility class to fetch the EPG. This class isn't thread-safe.
+ * The service class to fetch EPG routinely or on-demand during channel scanning
+ *
+ * <p>Since the default executor of {@link AsyncTask} is {@link AsyncTask#SERIAL_EXECUTOR}, only one
+ * task can run at a time. Because fetching EPG takes long time, the fetching task shouldn't run on
+ * the serial executor. Instead, it should run on the {@link AsyncTask#THREAD_POOL_EXECUTOR}.
*/
public class EpgFetcher {
private static final String TAG = "EpgFetcher";
private static final boolean DEBUG = false;
- private static final int MSG_FETCH_EPG = 1;
+ private static final int EPG_ROUTINELY_FETCHING_JOB_ID = 101;
+
+ private static final long INITIAL_BACKOFF_MS = TimeUnit.SECONDS.toMillis(10);
+
+ private static final int REASON_EPG_READER_NOT_READY = 1;
+ private static final int REASON_LOCATION_INFO_UNAVAILABLE = 2;
+ private static final int REASON_LOCATION_PERMISSION_NOT_GRANTED = 3;
+ private static final int REASON_NO_EPG_DATA_RETURNED = 4;
+ private static final int REASON_NO_NEW_EPG = 5;
- 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 PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30);
+ private static final long FETCH_DURING_SCAN_WAIT_TIME_MS = TimeUnit.SECONDS.toMillis(10);
- private static final int BATCH_OPERATION_COUNT = 100;
+ private static final long FETCH_DURING_SCAN_DURATION_SEC = TimeUnit.HOURS.toSeconds(3);
+ private static final long FAST_FETCH_DURATION_SEC = TimeUnit.DAYS.toSeconds(2);
- private static final String SUPPORTED_COUNTRY_CODE = Locale.US.getCountry();
- private static final String CONTENT_RATING_SEPARATOR = ",";
+ private static final int DEFAULT_ROUTINE_INTERVAL_HOUR = 4;
+ private static final String KEY_ROUTINE_INTERVAL = "live_channels_epg_fetcher_interval_hour";
- // Value: Long
- private static final String KEY_LAST_UPDATED_EPG_TIMESTAMP =
- "com.android.tv.data.epg.EpgFetcher.LastUpdatedEpgTimestamp";
- // Value: String
- private static final String KEY_LAST_LINEUP_ID =
- "com.android.tv.data.epg.EpgFetcher.LastLineupId";
+ private static final int MSG_PREPARE_FETCH_DURING_SCAN = 1;
+ private static final int MSG_CHANNEL_UPDATED_DURING_SCAN = 2;
+ private static final int MSG_FINISH_FETCH_DURING_SCAN = 3;
+ private static final int MSG_RETRY_PREPARE_FETCH_DURING_SCAN = 4;
+
+ private static final int QUERY_CHANNEL_COUNT = 50;
+ private static final int MINIMUM_CHANNELS_TO_DECIDE_LINEUP = 3;
private static EpgFetcher sInstance;
private final Context mContext;
private final ChannelDataManager mChannelDataManager;
private final EpgReader mEpgReader;
- private EpgFetcherHandler mHandler;
- private RecurringRunner mRecurringRunner;
- private boolean mStarted;
-
- private long mLastEpgTimestamp = -1;
- private String mLineupId;
-
- public static synchronized EpgFetcher getInstance(Context context) {
+ private final PerformanceMonitor mPerformanceMonitor;
+ private FetchAsyncTask mFetchTask;
+ private FetchDuringScanHandler mFetchDuringScanHandler;
+ private long mEpgTimeStamp;
+ private List<Lineup> mPossibleLineups;
+ private final Object mPossibleLineupsLock = new Object();
+ private final Object mFetchDuringScanHandlerLock = new Object();
+ // A flag to block the re-entrance of onChannelScanStarted and onChannelScanFinished.
+ private boolean mScanStarted;
+
+ private final long mRoutineIntervalMs;
+ private final long mEpgDataExpiredTimeLimitMs;
+ private final long mFastFetchDurationSec;
+
+ public static EpgFetcher getInstance(Context context) {
if (sInstance == null) {
- sInstance = new EpgFetcher(context.getApplicationContext());
+ sInstance = new EpgFetcher(context);
}
return sInstance;
}
- /**
- * Creates and returns {@link EpgReader}.
- */
- public static EpgReader createEpgReader(Context context) {
+ /** Creates and returns {@link EpgReader}. */
+ public static EpgReader createEpgReader(Context context, String region) {
return new StubEpgReader(context);
}
private EpgFetcher(Context context) {
- mContext = context;
- mEpgReader = new StubEpgReader(mContext);
- mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager();
- mChannelDataManager.addListener(new ChannelDataManager.Listener() {
- @Override
- public void onLoadFinished() {
- if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()");
- handleChannelChanged();
+ mContext = context.getApplicationContext();
+ ApplicationSingletons applicationSingletons = TvApplication.getSingletons(mContext);
+ mChannelDataManager = applicationSingletons.getChannelDataManager();
+ mPerformanceMonitor = applicationSingletons.getPerformanceMonitor();
+ mEpgReader = createEpgReader(mContext, LocationUtils.getCurrentCountry(mContext));
+
+ int remoteInteval =
+ (int) RemoteConfigUtils.getRemoteConfig(
+ context, KEY_ROUTINE_INTERVAL, DEFAULT_ROUTINE_INTERVAL_HOUR);
+ mRoutineIntervalMs =
+ remoteInteval < 0
+ ? TimeUnit.HOURS.toMillis(DEFAULT_ROUTINE_INTERVAL_HOUR)
+ : TimeUnit.HOURS.toMillis(remoteInteval);
+ mEpgDataExpiredTimeLimitMs = mRoutineIntervalMs * 2;
+ mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + mRoutineIntervalMs / 1000;
+ }
+
+ /**
+ * Starts the routine service of EPG fetching. It use {@link JobScheduler} to schedule the EPG
+ * fetching routine. The EPG fetching routine will be started roughly every 4 hours, unless
+ * the channel scanning of tuner input is started.
+ */
+ @MainThread
+ public void startRoutineService() {
+ JobScheduler jobScheduler =
+ (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ for (JobInfo job : jobScheduler.getAllPendingJobs()) {
+ if (job.getId() == EPG_ROUTINELY_FETCHING_JOB_ID) {
+ return;
}
+ }
+ JobInfo job =
+ new JobInfo.Builder(
+ EPG_ROUTINELY_FETCHING_JOB_ID,
+ new ComponentName(mContext, EpgFetchService.class))
+ .setPeriodic(mRoutineIntervalMs)
+ .setBackoffCriteria(INITIAL_BACKOFF_MS, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
+ .setPersisted(true)
+ .build();
+ jobScheduler.schedule(job);
+ Log.i(TAG, "EPG fetching routine service started.");
+ }
+ /**
+ * Fetches EPG immediately if current EPG data are out-dated, i.e., not successfully updated
+ * by routine fetching service due to various reasons.
+ */
+ @MainThread
+ public void fetchImmediatelyIfNeeded() {
+ if (TvCommonUtils.isRunningInTest()) {
+ // Do not run EpgFetcher in test.
+ return;
+ }
+ new AsyncTask<Void, Void, Long>() {
@Override
- public void onChannelListUpdated() {
- if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()");
- handleChannelChanged();
+ protected Long doInBackground(Void... args) {
+ return EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext);
}
@Override
- public void onChannelBrowsableChanged() {
- if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelBrowsableChanged()");
- handleChannelChanged();
+ protected void onPostExecute(Long result) {
+ if (System.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
+ > mEpgDataExpiredTimeLimitMs) {
+ Log.i(TAG, "EPG data expired. Start fetching immediately.");
+ fetchImmediately();
+ }
}
- });
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
- private void handleChannelChanged() {
- if (mStarted) {
- if (needToStop()) {
- stop();
- }
- } else {
- start();
- }
- }
+ /**
+ * Fetches EPG immediately.
+ */
+ @MainThread
+ public void fetchImmediately() {
+ if (!mChannelDataManager.isDbLoadFinished()) {
+ mChannelDataManager.addListener(new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ mChannelDataManager.removeListener(this);
+ executeFetchTaskIfPossible(null, null);
+ }
- private boolean needToStop() {
- return !canStart();
- }
+ @Override
+ public void onChannelListUpdated() { }
- private boolean canStart() {
- if (DEBUG) Log.d(TAG, "canStart()");
- boolean hasInternalTunerChannel = false;
- for (TvInputInfo input : TvApplication.getSingletons(mContext).getTvInputManagerHelper()
- .getTvInputInfos(true, true)) {
- String inputId = input.getId();
- if (Utils.isInternalTvInput(mContext, inputId)
- && mChannelDataManager.getChannelCountForInput(inputId) > 0) {
- hasInternalTunerChannel = true;
- break;
- }
- }
- if (!hasInternalTunerChannel) {
- if (DEBUG) Log.d(TAG, "No internal tuner channels.");
- return false;
+ @Override
+ public void onChannelBrowsableChanged() { }
+ });
+ } else {
+ executeFetchTaskIfPossible(null, null);
}
+ }
- 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;
+ /**
+ * Notifies EPG fetch service that channel scanning is started.
+ */
+ @MainThread
+ public void onChannelScanStarted() {
+ if (mScanStarted || !Features.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) {
+ return;
}
-
- 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());
- return false;
+ mScanStarted = true;
+ stopFetchingJob();
+ synchronized (mFetchDuringScanHandlerLock) {
+ if (mFetchDuringScanHandler == null) {
+ HandlerThread thread = new HandlerThread("EpgFetchDuringScan");
+ thread.start();
+ mFetchDuringScanHandler = new FetchDuringScanHandler(thread.getLooper());
}
- } catch (SecurityException e) {
- Log.w(TAG, "No permission to get the current location", e);
- return false;
- } catch (IOException e) {
- Log.w(TAG, "IO Exception when getting the current location", e);
+ mFetchDuringScanHandler.sendEmptyMessage(MSG_PREPARE_FETCH_DURING_SCAN);
}
- return true;
+ Log.i(TAG, "EPG fetching on channel scanning started.");
}
/**
- * Starts fetching EPG.
+ * Notifies EPG fetch service that channel scanning is finished.
*/
@MainThread
- public void start() {
- if (DEBUG) Log.d(TAG, "start()");
- if (mStarted) {
- if (DEBUG) Log.d(TAG, "EpgFetcher thread already started.");
+ public void onChannelScanFinished() {
+ if (!mScanStarted) {
return;
}
- if (!canStart()) {
- return;
+ mScanStarted = false;
+ mFetchDuringScanHandler.sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN);
+ }
+
+ @MainThread
+ private void stopFetchingJob() {
+ if (DEBUG) Log.d(TAG, "Try to stop routinely fetching job...");
+ if (mFetchTask != null) {
+ mFetchTask.cancel(true);
+ mFetchTask = null;
+ Log.i(TAG, "EPG routinely fetching job stopped.");
}
- mStarted = true;
- if (DEBUG) Log.d(TAG, "Starting EpgFetcher thread.");
- HandlerThread handlerThread = new HandlerThread("EpgFetcher");
- handlerThread.start();
- mHandler = new EpgFetcherHandler(handlerThread.getLooper(), this);
- mRecurringRunner = new RecurringRunner(mContext, EPG_PREFETCH_RECURRING_PERIOD_MS,
- new EpgRunner(), null);
- mRecurringRunner.start();
- if (DEBUG) Log.d(TAG, "EpgFetcher thread started successfully.");
}
- /**
- * Starts fetching EPG immediately if possible without waiting for the timer.
- */
@MainThread
- public void startImmediately() {
- start();
- if (mStarted) {
- if (DEBUG) Log.d(TAG, "Starting fetcher immediately");
- fetchEpg();
+ private boolean executeFetchTaskIfPossible(JobService service, JobParameters params) {
+ SoftPreconditions.checkState(mChannelDataManager.isDbLoadFinished());
+ if (!TvCommonUtils.isRunningInTest() && checkFetchPrerequisite()) {
+ mFetchTask = new FetchAsyncTask(service, params);
+ mFetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ return true;
}
+ return false;
}
- /**
- * Stops fetching EPG.
- */
@MainThread
- public void stop() {
- if (DEBUG) Log.d(TAG, "stop()");
- if (!mStarted) {
- return;
+ private boolean checkFetchPrerequisite() {
+ if (DEBUG) Log.d(TAG, "Check prerequisite of routinely fetching job.");
+ if (!Features.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) {
+ Log.i(TAG, "Cannot start routine service: country not supported: "
+ + LocationUtils.getCurrentCountry(mContext));
+ return false;
+ }
+ if (mFetchTask != null) {
+ // Fetching job is already running or ready to run, no need to start again.
+ return false;
+ }
+ if (mFetchDuringScanHandler != null) {
+ if (DEBUG) Log.d(TAG, "Cannot start routine service: scanning channels.");
+ return false;
+ }
+ if (getTunerChannelCount() == 0) {
+ if (DEBUG) Log.d(TAG, "Cannot start routine service: no internal tuner channels.");
+ return false;
}
- mStarted = false;
- mRecurringRunner.stop();
- mHandler.removeCallbacksAndMessages(null);
- mHandler.getLooper().quit();
+ if (!TextUtils.isEmpty(EpgFetchHelper.getLastLineupId(mContext))) {
+ return true;
+ }
+ if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ return true;
+ }
+ return true;
}
- private void fetchEpg() {
- fetchEpg(0);
+ @MainThread
+ private int getTunerChannelCount() {
+ for (TvInputInfo input : TvApplication.getSingletons(mContext)
+ .getTvInputManagerHelper().getTvInputInfos(true, true)) {
+ String inputId = input.getId();
+ if (Utils.isInternalTvInput(mContext, inputId)) {
+ return mChannelDataManager.getChannelCountForInput(inputId);
+ }
+ }
+ return 0;
}
- private void fetchEpg(long delay) {
- mHandler.removeMessages(MSG_FETCH_EPG);
- mHandler.sendEmptyMessageDelayed(MSG_FETCH_EPG, delay);
+ @AnyThread
+ private void clearUnusedLineups(@Nullable String lineupId) {
+ synchronized (mPossibleLineupsLock) {
+ if (mPossibleLineups == null) {
+ return;
+ }
+ for (Lineup lineup : mPossibleLineups) {
+ if (!TextUtils.equals(lineupId, lineup.id)) {
+ mEpgReader.clearCachedChannels(lineup.id);
+ }
+ }
+ mPossibleLineups = null;
+ }
}
- private void onFetchEpg() {
- if (DEBUG) Log.d(TAG, "Start fetching EPG.");
+ @WorkerThread
+ private Integer prepareFetchEpg(boolean forceUpdatePossibleLineups) {
if (!mEpgReader.isAvailable()) {
- if (DEBUG) Log.d(TAG, "EPG reader is not temporarily available.");
- fetchEpg(EPG_READER_INIT_WAIT_MS);
- return;
+ Log.i(TAG, "EPG reader is temporarily unavailable.");
+ return REASON_EPG_READER_NOT_READY;
}
- String lineupId = getLastLineupId();
- if (lineupId == null) {
- Address address;
- try {
- address = LocationUtils.getCurrentAddress(mContext);
- } catch (IOException e) {
- if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e);
- fetchEpg(LOCATION_ERROR_WAIT_MS);
- return;
- } catch (SecurityException e) {
- Log.w(TAG, "No permission to get the current location.");
- return;
+ // Checks the EPG Timestamp.
+ mEpgTimeStamp = mEpgReader.getEpgTimestamp();
+ if (mEpgTimeStamp <= EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)) {
+ if (DEBUG) Log.d(TAG, "No new EPG.");
+ return REASON_NO_NEW_EPG;
+ }
+ // Updates postal code.
+ boolean postalCodeChanged = false;
+ try {
+ postalCodeChanged = PostalCodeUtils.updatePostalCode(mContext);
+ } catch (IOException e) {
+ if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e);
+ if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ return REASON_LOCATION_INFO_UNAVAILABLE;
}
- if (address == null) {
- if (DEBUG) Log.d(TAG, "Null address returned.");
- fetchEpg(LOCATION_INIT_WAIT_MS);
- return;
+ } catch (SecurityException e) {
+ Log.w(TAG, "No permission to get the current location.");
+ if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ return REASON_LOCATION_PERMISSION_NOT_GRANTED;
}
- if (DEBUG) Log.d(TAG, "Current location is " + address);
-
- lineupId = getLineupForAddress(address);
- if (lineupId != null) {
- if (DEBUG) Log.d(TAG, "Saving lineup " + lineupId + "found for " + address);
- setLastLineupId(lineupId);
- } else {
- if (DEBUG) Log.d(TAG, "No lineup found for " + address);
- return;
+ } catch (PostalCodeUtils.NoPostalCodeException e) {
+ Log.i(TAG, "Cannot get address or postal code.");
+ return REASON_LOCATION_INFO_UNAVAILABLE;
+ }
+ // Updates possible lineups if necessary.
+ SoftPreconditions.checkState(mPossibleLineups == null, TAG, "Possible lineups not reset.");
+ if (postalCodeChanged || forceUpdatePossibleLineups
+ || EpgFetchHelper.getLastLineupId(mContext) == null) {
+ // To prevent main thread being blocked, though theoretically it should not happen.
+ List<Lineup> possibleLineups =
+ mEpgReader.getLineups(PostalCodeUtils.getLastPostalCode(mContext));
+ if (possibleLineups.isEmpty()) {
+ return REASON_NO_EPG_DATA_RETURNED;
+ }
+ for (Lineup lineup : possibleLineups) {
+ mEpgReader.preloadChannels(lineup.id);
+ }
+ synchronized (mPossibleLineupsLock) {
+ mPossibleLineups = possibleLineups;
}
+ EpgFetchHelper.setLastLineupId(mContext, null);
}
+ return null;
+ }
- // Check the EPG Timestamp.
- long epgTimestamp = mEpgReader.getEpgTimestamp();
- if (epgTimestamp <= getLastUpdatedEpgTimestamp()) {
- if (DEBUG) Log.d(TAG, "No new EPG.");
+ @WorkerThread
+ private void batchFetchEpg(List<Channel> channels, long durationSec) {
+ Log.i(TAG, "Start batch fetching (" + durationSec + ")...." + channels.size());
+ if (channels.size() == 0) {
return;
}
-
- boolean updated = false;
- List<Channel> channels = mEpgReader.getChannels(lineupId);
+ List<Long> queryChannelIds = new ArrayList<>(QUERY_CHANNEL_COUNT);
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;
+ queryChannelIds.add(channel.getId());
+ if (queryChannelIds.size() >= QUERY_CHANNEL_COUNT) {
+ batchUpdateEpg(mEpgReader.getPrograms(queryChannelIds, durationSec));
+ queryChannelIds.clear();
}
}
-
- final boolean epgUpdated = updated;
- setLastUpdatedEpgTimestamp(epgTimestamp);
- mHandler.removeMessages(MSG_FETCH_EPG);
- if (DEBUG) Log.d(TAG, "Fetching EPG is finished.");
+ if (!queryChannelIds.isEmpty()) {
+ batchUpdateEpg(mEpgReader.getPrograms(queryChannelIds, durationSec));
+ }
}
- @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);
+ @WorkerThread
+ private void batchUpdateEpg(Map<Long, List<Program>> allPrograms) {
+ for (Map.Entry<Long, List<Program>> entry : allPrograms.entrySet()) {
+ List<Program> programs = entry.getValue();
+ if (programs == null) {
+ continue;
}
+ Collections.sort(programs);
+ Log.i(TAG, "Batch fetched " + programs.size() + " programs for channel "
+ + entry.getKey());
+ EpgFetchHelper.updateEpgData(mContext, entry.getKey(), programs);
}
- return lineup;
}
@Nullable
- private String getLineupForPostalCode(String postalCode) {
- List<Lineup> lineups = mEpgReader.getLineups(postalCode);
- 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;
+ @WorkerThread
+ private String pickBestLineupId(List<Channel> currentChannelList) {
+ String maxLineupId = null;
+ synchronized (mPossibleLineupsLock) {
+ if (mPossibleLineups == null) {
+ return null;
+ }
+ int maxCount = 0;
+ for (Lineup lineup : mPossibleLineups) {
+ int count = getMatchedChannelCount(lineup.id, currentChannelList);
+ Log.i(TAG, lineup.name + " (" + lineup.id + ") - " + count + " matches");
+ if (count > maxCount) {
+ maxCount = count;
+ maxLineupId = lineup.id;
+ }
}
}
- return null;
+ return maxLineupId;
}
- private long getLastUpdatedEpgTimestamp() {
- if (mLastEpgTimestamp < 0) {
- mLastEpgTimestamp = PreferenceManager.getDefaultSharedPreferences(mContext).getLong(
- KEY_LAST_UPDATED_EPG_TIMESTAMP, 0);
+ @WorkerThread
+ private int getMatchedChannelCount(String lineupId, List<Channel> currentChannelList) {
+ // Construct a list of display numbers for existing channels.
+ if (currentChannelList.isEmpty()) {
+ if (DEBUG) Log.d(TAG, "No existing channel to compare");
+ return 0;
+ }
+ List<String> numbers = new ArrayList<>(currentChannelList.size());
+ for (Channel channel : currentChannelList) {
+ // We only support channels from internal tuner inputs.
+ if (Utils.isInternalTvInput(mContext, channel.getInputId())) {
+ numbers.add(channel.getDisplayNumber());
+ }
}
- return mLastEpgTimestamp;
+ numbers.retainAll(mEpgReader.getChannelNumbers(lineupId));
+ return numbers.size();
}
- private void setLastUpdatedEpgTimestamp(long timestamp) {
- mLastEpgTimestamp = timestamp;
- PreferenceManager.getDefaultSharedPreferences(mContext).edit().putLong(
- KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).commit();
- }
+ public static class EpgFetchService extends JobService {
+ private EpgFetcher mEpgFetcher;
- private String getLastLineupId() {
- if (mLineupId == null) {
- mLineupId = PreferenceManager.getDefaultSharedPreferences(mContext)
- .getString(KEY_LAST_LINEUP_ID, null);
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ TvApplication.setCurrentRunningProcess(this, true);
+ mEpgFetcher = EpgFetcher.getInstance(this);
}
- if (DEBUG) Log.d(TAG, "Last lineup_id " + mLineupId);
- return mLineupId;
- }
- private void setLastLineupId(String lineupId) {
- mLineupId = lineupId;
- PreferenceManager.getDefaultSharedPreferences(mContext).edit()
- .putString(KEY_LAST_LINEUP_ID, lineupId).commit();
- }
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ if (!mEpgFetcher.mChannelDataManager.isDbLoadFinished()) {
+ mEpgFetcher.mChannelDataManager.addListener(new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ mEpgFetcher.mChannelDataManager.removeListener(this);
+ if (!mEpgFetcher.executeFetchTaskIfPossible(EpgFetchService.this, params)) {
+ jobFinished(params, false);
+ }
+ }
- private boolean updateEpg(long channelId, List<Program> newPrograms) {
- final int fetchedProgramsCount = newPrograms.size();
- if (fetchedProgramsCount == 0) {
+ @Override
+ public void onChannelListUpdated() { }
+
+ @Override
+ public void onChannelBrowsableChanged() { }
+ });
+ return true;
+ } else {
+ return mEpgFetcher.executeFetchTaskIfPossible(this, params);
+ }
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ mEpgFetcher.stopFetchingJob();
return false;
}
- boolean updated = false;
- 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++;
- }
+ }
+
+ private class FetchAsyncTask extends AsyncTask<Void, Void, Integer> {
+ private final JobService mService;
+ private final JobParameters mParams;
+ private List<Channel> mCurrentChannelList;
+ private TimerEvent mTimerEvent;
+
+ private FetchAsyncTask(JobService service, JobParameters params) {
+ mService = service;
+ mParams = params;
}
- // 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<>();
- while (newProgramsIndex < fetchedProgramsCount) {
- // TODO: Extract to method and make test.
- 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 (isSameTitleAndOverlap(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.getId()))
- .withValues(toContentValues(newProgram))
- .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.getId()))
- .build());
- oldProgramsIndex++;
+
+ @Override
+ protected void onPreExecute() {
+ mTimerEvent = mPerformanceMonitor.startTimer();
+ mCurrentChannelList = mChannelDataManager.getChannelList();
+ }
+
+ @Override
+ protected Integer doInBackground(Void... args) {
+ final int oldTag = TrafficStats.getThreadStatsTag();
+ TrafficStats.setThreadStatsTag(NetworkTrafficTags.EPG_FETCH);
+ try {
+ if (DEBUG) Log.d(TAG, "Start EPG routinely fetching.");
+ Integer failureReason = prepareFetchEpg(false);
+ // InterruptedException might be caught by RPC, we should check it here.
+ if (failureReason != null || this.isCancelled()) {
+ return failureReason;
+ }
+ String lineupId = EpgFetchHelper.getLastLineupId(mContext);
+ lineupId = lineupId == null ? pickBestLineupId(mCurrentChannelList) : lineupId;
+ if (lineupId != null) {
+ Log.i(TAG, "Selecting the lineup " + lineupId);
+ // During normal fetching process, the lineup ID should be confirmed since all
+ // channels are known, clear up possible lineups to save resources.
+ EpgFetchHelper.setLastLineupId(mContext, lineupId);
+ clearUnusedLineups(lineupId);
} else {
- // No match. The new program does not match any of the old programs. Insert
- // it as a new program.
- addNewProgram = true;
- newProgramsIndex++;
+ Log.i(TAG, "Failed to get lineup id");
+ return REASON_NO_EPG_DATA_RETURNED;
}
- } else {
- // No old programs. Just insert new programs.
- addNewProgram = true;
- newProgramsIndex++;
- }
- if (addNewProgram) {
- ops.add(ContentProviderOperation
- .newInsert(TvContract.Programs.CONTENT_URI)
- .withValues(toContentValues(newProgram))
- .build());
- }
- // Throttle the batch operation not to cause TransactionTooLargeException.
- if (ops.size() > BATCH_OPERATION_COUNT || newProgramsIndex >= fetchedProgramsCount) {
- try {
- if (DEBUG) {
- int size = ops.size();
- Log.d(TAG, "Running " + size + " operations for channel " + channelId);
- for (int i = 0; i < size; ++i) {
- Log.d(TAG, "Operation(" + i + "): " + ops.get(i));
- }
+ final List<Channel> channels = mEpgReader.getChannels(lineupId);
+ // InterruptedException might be caught by RPC, we should check it here.
+ if (this.isCancelled()) {
+ return null;
+ }
+ if (channels.isEmpty()) {
+ Log.i(TAG, "Failed to get EPG channels.");
+ return REASON_NO_EPG_DATA_RETURNED;
+ }
+ if (System.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
+ > mEpgDataExpiredTimeLimitMs) {
+ batchFetchEpg(channels, mFastFetchDurationSec);
+ }
+ new Handler(mContext.getMainLooper())
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ ChannelLogoFetcher.startFetchingChannelLogos(
+ mContext, channels);
+ }
+ });
+ for (Channel channel : channels) {
+ if (this.isCancelled()) {
+ return null;
}
- mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
- updated = true;
- } catch (RemoteException | OperationApplicationException e) {
- Log.e(TAG, "Failed to insert programs.", e);
- return updated;
+ long channelId = channel.getId();
+ List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channelId));
+ // InterruptedException might be caught by RPC, we should check it here.
+ Collections.sort(programs);
+ Log.i(TAG, "Fetched " + programs.size() + " programs for channel " + channelId);
+ EpgFetchHelper.updateEpgData(mContext, channelId, programs);
}
- ops.clear();
+ EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, mEpgTimeStamp);
+ if (DEBUG) Log.d(TAG, "Fetching EPG is finished.");
+ return null;
+ } finally {
+ TrafficStats.setThreadStatsTag(oldTag);
}
}
- if (DEBUG) {
- Log.d(TAG, "Updated " + fetchedProgramsCount + " programs for channel " + channelId);
- }
- return updated;
- }
- private List<Program> queryPrograms(long channelId, long startTimeMs, long endTimeMs) {
- try (Cursor c = mContext.getContentResolver().query(
- TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
- Program.PROJECTION, null, null, Programs.COLUMN_START_TIME_UTC_MILLIS)) {
- if (c == null) {
- return Collections.emptyList();
- }
- ArrayList<Program> programs = new ArrayList<>();
- while (c.moveToNext()) {
- programs.add(Program.fromCursor(c));
+ @Override
+ protected void onPostExecute(Integer failureReason) {
+ mFetchTask = null;
+ if (failureReason == null || failureReason == REASON_LOCATION_PERMISSION_NOT_GRANTED
+ || failureReason == REASON_NO_NEW_EPG) {
+ jobFinished(false);
+ } else {
+ // Applies back-off policy
+ jobFinished(true);
}
- return programs;
+ mPerformanceMonitor.stopTimer(mTimerEvent, EventNames.FETCH_EPG_TASK);
+ mPerformanceMonitor.recordMemory(EventNames.FETCH_EPG_TASK);
}
- }
- /**
- * Returns {@code true} if the {@code oldProgram} program needs to be updated with the
- * {@code newProgram} program.
- */
- private boolean isSameTitleAndOverlap(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 Objects.equals(oldProgram.getTitle(), newProgram.getTitle())
- && oldProgram.getStartTimeUtcMillis() <= newProgram.getEndTimeUtcMillis()
- && newProgram.getStartTimeUtcMillis() <= oldProgram.getEndTimeUtcMillis();
- }
+ @Override
+ protected void onCancelled(Integer failureReason) {
+ clearUnusedLineups(null);
+ jobFinished(false);
+ }
- @SuppressLint("InlinedApi")
- @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());
- if (BuildCompat.isAtLeastN()) {
- putValue(values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER,
- program.getSeasonNumber());
- putValue(values, TvContract.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, 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());
- String[] canonicalGenres = program.getCanonicalGenres();
- if (canonicalGenres != null && canonicalGenres.length > 0) {
- putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE,
- Genres.encode(canonicalGenres));
- } else {
- putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, "");
- }
- TvContentRating[] ratings = program.getContentRatings();
- if (ratings != null && ratings.length > 0) {
- StringBuilder sb = new StringBuilder(ratings[0].flattenToString());
- for (int i = 1; i < ratings.length; ++i) {
- sb.append(CONTENT_RATING_SEPARATOR);
- sb.append(ratings[i].flattenToString());
+ private void jobFinished(boolean reschedule) {
+ if (mService != null && mParams != null) {
+ // Task is executed from JobService, need to report jobFinished.
+ mService.jobFinished(mParams, reschedule);
}
- putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, sb.toString());
- } else {
- putValue(values, TvContract.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,
- InternalDataUtils.serializeInternalProviderData(program));
- return values;
- }
-
- private static void putValue(ContentValues contentValues, String key, String value) {
- if (TextUtils.isEmpty(value)) {
- contentValues.putNull(key);
- } else {
- contentValues.put(key, value);
}
}
- private static void putValue(ContentValues contentValues, String key, byte[] value) {
- if (value == null || value.length == 0) {
- contentValues.putNull(key);
- } else {
- contentValues.put(key, value);
- }
- }
+ @WorkerThread
+ private class FetchDuringScanHandler extends Handler {
+ private final Set<Long> mFetchedChannelIdsDuringScan = new HashSet<>();
+ private String mPossibleLineupId;
+
+ private final ChannelDataManager.Listener mDuringScanChannelListener =
+ new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()");
+ if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP
+ && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
+ Message.obtain(FetchDuringScanHandler.this,
+ MSG_CHANNEL_UPDATED_DURING_SCAN, new ArrayList<>(
+ mChannelDataManager.getChannelList())).sendToTarget();
+ }
+ }
+
+ @Override
+ public void onChannelListUpdated() {
+ if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()");
+ if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP
+ && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
+ Message.obtain(FetchDuringScanHandler.this,
+ MSG_CHANNEL_UPDATED_DURING_SCAN,
+ mChannelDataManager.getChannelList()).sendToTarget();
+ }
+ }
+
+ @Override
+ public void onChannelBrowsableChanged() {
+ // Do nothing
+ }
+ };
- private static class EpgFetcherHandler extends WeakHandler<EpgFetcher> {
- public EpgFetcherHandler (@NonNull Looper looper, EpgFetcher ref) {
- super(looper, ref);
+ @AnyThread
+ private FetchDuringScanHandler(Looper looper) {
+ super(looper);
}
@Override
- public void handleMessage(Message msg, @NonNull EpgFetcher epgFetcher) {
+ public void handleMessage(Message msg) {
switch (msg.what) {
- case MSG_FETCH_EPG:
- epgFetcher.onFetchEpg();
+ case MSG_PREPARE_FETCH_DURING_SCAN:
+ case MSG_RETRY_PREPARE_FETCH_DURING_SCAN:
+ onPrepareFetchDuringScan();
break;
- default:
- super.handleMessage(msg);
+ case MSG_CHANNEL_UPDATED_DURING_SCAN:
+ if (!hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
+ onChannelUpdatedDuringScan((List<Channel>) msg.obj);
+ }
+ break;
+ case MSG_FINISH_FETCH_DURING_SCAN:
+ removeMessages(MSG_RETRY_PREPARE_FETCH_DURING_SCAN);
+ if (hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
+ sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN);
+ } else {
+ onFinishFetchDuringScan();
+ }
break;
}
}
- }
- private class EpgRunner implements Runnable {
- @Override
- public void run() {
- fetchEpg();
+ private void onPrepareFetchDuringScan() {
+ Integer failureReason = prepareFetchEpg(true);
+ if (failureReason != null) {
+ sendEmptyMessageDelayed(
+ MSG_RETRY_PREPARE_FETCH_DURING_SCAN, FETCH_DURING_SCAN_WAIT_TIME_MS);
+ return;
+ }
+ mChannelDataManager.addListener(mDuringScanChannelListener);
+ }
+
+ private void onChannelUpdatedDuringScan(List<Channel> currentChannelList) {
+ String lineupId = pickBestLineupId(currentChannelList);
+ Log.i(TAG, "Fast fetch channels for lineup ID: " + lineupId);
+ if (TextUtils.isEmpty(lineupId)) {
+ if (TextUtils.isEmpty(mPossibleLineupId)) {
+ return;
+ }
+ } else if (!TextUtils.equals(lineupId, mPossibleLineupId)) {
+ mFetchedChannelIdsDuringScan.clear();
+ mPossibleLineupId = lineupId;
+ }
+ List<Long> currentChannelIds = new ArrayList<>();
+ for (Channel channel : currentChannelList) {
+ currentChannelIds.add(channel.getId());
+ }
+ mFetchedChannelIdsDuringScan.retainAll(currentChannelIds);
+ List<Channel> newChannels = new ArrayList<>();
+ for (Channel channel : mEpgReader.getChannels(mPossibleLineupId)) {
+ if (!mFetchedChannelIdsDuringScan.contains(channel.getId())) {
+ newChannels.add(channel);
+ mFetchedChannelIdsDuringScan.add(channel.getId());
+ }
+ }
+ batchFetchEpg(newChannels, FETCH_DURING_SCAN_DURATION_SEC);
+ }
+
+ private void onFinishFetchDuringScan() {
+ mChannelDataManager.removeListener(mDuringScanChannelListener);
+ EpgFetchHelper.setLastLineupId(mContext, mPossibleLineupId);
+ clearUnusedLineups(null);
+ mFetchedChannelIdsDuringScan.clear();
+ synchronized (mFetchDuringScanHandlerLock) {
+ if (!hasMessages(MSG_PREPARE_FETCH_DURING_SCAN)) {
+ removeCallbacksAndMessages(null);
+ getLooper().quit();
+ mFetchDuringScanHandler = null;
+ }
+ }
+ // Clear timestamp to make routine service start right away.
+ EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, 0);
+ Log.i(TAG, "EPG Fetching during channel scanning finished.");
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ fetchImmediately();
+ }
+ });
}
}
}
diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java
index 4f3b6f52..c5aeca27 100644
--- a/src/com/android/tv/data/epg/EpgReader.java
+++ b/src/com/android/tv/data/epg/EpgReader.java
@@ -16,15 +16,17 @@
package com.android.tv.data.epg;
+import android.support.annotation.AnyThread;
import android.support.annotation.NonNull;
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.
@@ -42,25 +44,48 @@ public interface EpgReader {
*/
long getEpgTimestamp();
+ /** Sets the region code. */
+ void setRegionCode(String regionCode);
+
+ /** Returns the lineups list. */
+ List<Lineup> getLineups(@NonNull String postalCode);
+
/**
- * Returns the channels list.
+ * 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);
+ /** Pre-loads and caches channels for a given lineup. */
+ void preloadChannels(@NonNull String lineupId);
+
/**
- * Returns the lineups list.
+ * Clears cached channels for a given lineup.
*/
- List<Lineup> getLineups(@NonNull String postalCode);
+ @AnyThread
+ void clearCachedChannels(@NonNull String lineupId);
/**
- * Returns the programs for the given channel. The result is sorted by the start time.
- * Note that the {@code Program} doesn't have valid program ID because it's not retrieved from
- * TvProvider.
+ * 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<Program> getPrograms(long channelId);
/**
- * Returns the series information for the given series ID.
+ * 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.
*/
- SeriesInfo getSeriesInfo(String seriesId);
-}
+ Map<Long, List<Program>> getPrograms(@NonNull List<Long> channelIds, long duration);
+
+ /** Returns the series information for the given series ID. */
+ SeriesInfo getSeriesInfo(@NonNull String seriesId);
+} \ No newline at end of file
diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java
index 64093f89..ab6935ad 100644
--- a/src/com/android/tv/data/epg/StubEpgReader.java
+++ b/src/com/android/tv/data/epg/StubEpgReader.java
@@ -18,13 +18,15 @@ package com.android.tv.data.epg;
import android.content.Context;
+import android.support.annotation.NonNull;
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,22 +46,47 @@ public class StubEpgReader implements EpgReader{
}
@Override
- public List<Channel> getChannels(String lineupId) {
+ public void setRegionCode(String regionCode) {
+ // Do nothing
+ }
+
+ @Override
+ public List<Lineup> getLineups(@NonNull String postalCode) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<String> getChannelNumbers(@NonNull String lineupId) {
return Collections.emptyList();
}
@Override
- public List<Lineup> getLineups(String postalCode) {
+ public List<Channel> getChannels(@NonNull String lineupId) {
return Collections.emptyList();
}
@Override
+ public void preloadChannels(@NonNull String lineupId) {
+ // Do nothing
+ }
+
+ @Override
+ public void clearCachedChannels(@NonNull String lineupId) {
+ // Do nothing
+ }
+
+ @Override
public List<Program> getPrograms(long channelId) {
return Collections.emptyList();
}
@Override
- public SeriesInfo getSeriesInfo(String seriesId) {
+ public Map<Long, List<Program>> getPrograms(@NonNull List<Long> channelIds, long duration) {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public SeriesInfo getSeriesInfo(@NonNull String seriesId) {
return null;
}
-}
+} \ No newline at end of file