aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/data/epg
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/data/epg')
-rw-r--r--src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java86
-rw-r--r--src/com/android/tv/data/epg/EpgFetchHelper.java109
-rw-r--r--src/com/android/tv/data/epg/EpgFetchService.java70
-rw-r--r--src/com/android/tv/data/epg/EpgFetcher.java710
-rw-r--r--src/com/android/tv/data/epg/EpgFetcherImpl.java811
-rw-r--r--src/com/android/tv/data/epg/EpgInputWhiteList.java103
-rw-r--r--src/com/android/tv/data/epg/EpgReader.java46
-rw-r--r--src/com/android/tv/data/epg/StubEpgReader.java26
8 files changed, 1187 insertions, 774 deletions
diff --git a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java b/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java
new file mode 100644
index 00000000..795ad5c4
--- /dev/null
+++ b/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2018 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 com.android.tv.data.api.Channel;
+
+/**
+ * Hand copy of generated Autovalue class.
+ *
+ * TODO get autovalue working
+ */
+final class AutoValue_EpgReader_EpgChannel extends EpgReader.EpgChannel {
+
+ private final Channel channel;
+ private final String epgChannelId;
+
+ AutoValue_EpgReader_EpgChannel(
+ Channel channel,
+ String epgChannelId) {
+ if (channel == null) {
+ throw new NullPointerException("Null channel");
+ }
+ this.channel = channel;
+ if (epgChannelId == null) {
+ throw new NullPointerException("Null epgChannelId");
+ }
+ this.epgChannelId = epgChannelId;
+ }
+
+ @Override
+ public Channel getChannel() {
+ return channel;
+ }
+
+ @Override
+ public String getEpgChannelId() {
+ return epgChannelId;
+ }
+
+ @Override
+ public String toString() {
+ return "EpgChannel{"
+ + "channel=" + channel + ", "
+ + "epgChannelId=" + epgChannelId
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof EpgReader.EpgChannel) {
+ EpgReader.EpgChannel that = (EpgReader.EpgChannel) o;
+ return (this.channel.equals(that.getChannel()))
+ && (this.epgChannelId.equals(that.getEpgChannelId()));
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= this.channel.hashCode();
+ h *= 1000003;
+ h ^= this.epgChannelId.hashCode();
+ return h;
+ }
+
+}
+
diff --git a/src/com/android/tv/data/epg/EpgFetchHelper.java b/src/com/android/tv/data/epg/EpgFetchHelper.java
index 5693c877..3c7112ec 100644
--- a/src/com/android/tv/data/epg/EpgFetchHelper.java
+++ b/src/com/android/tv/data/epg/EpgFetchHelper.java
@@ -27,15 +27,15 @@ import android.preference.PreferenceManager;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
-
+import com.android.tv.common.CommonConstants;
+import com.android.tv.common.util.Clock;
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} */
+/** The helper class for {@link EpgFetcher} */
class EpgFetchHelper {
private static final String TAG = "EpgFetchHelper";
private static final boolean DEBUG = false;
@@ -45,15 +45,15 @@ class EpgFetchHelper {
// Value: Long
private static final String KEY_LAST_UPDATED_EPG_TIMESTAMP =
- "com.android.tv.data.epg.EpgFetcher.LastUpdatedEpgTimestamp";
+ CommonConstants.BASE_PACKAGE + ".data.epg.EpgFetcher.LastUpdatedEpgTimestamp";
// Value: String
private static final String KEY_LAST_LINEUP_ID =
- "com.android.tv.data.epg.EpgFetcher.LastLineupId";
+ CommonConstants.BASE_PACKAGE + ".data.epg.EpgFetcher.LastLineupId";
private static long sLastEpgUpdatedTimestamp = -1;
private static String sLastLineupId;
- private EpgFetchHelper() { }
+ private EpgFetchHelper() {}
/**
* Updates newly fetched EPG data for the given channel to local providers. The method will
@@ -61,18 +61,19 @@ class EpgFetchHelper {
* 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) {
+ static boolean updateEpgData(
+ Context context, Clock clock, long channelId, List<Program> fetchedPrograms) {
final int fetchedProgramsCount = fetchedPrograms.size();
if (fetchedProgramsCount == 0) {
return false;
}
boolean updated = false;
- long startTimeMs = System.currentTimeMillis();
+ long startTimeMs = clock.currentTimeMillis();
long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION_MS;
List<Program> oldPrograms = queryPrograms(context, channelId, startTimeMs, endTimeMs);
int oldProgramsIndex = 0;
@@ -82,8 +83,10 @@ class EpgFetchHelper {
// 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 oldProgram =
+ oldProgramsIndex < oldPrograms.size()
+ ? oldPrograms.get(oldProgramsIndex)
+ : null;
Program newProgram = fetchedPrograms.get(newProgramsIndex);
boolean addNewProgram = false;
if (oldProgram != null) {
@@ -95,18 +98,20 @@ class EpgFetchHelper {
// 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());
+ 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());
+ 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
@@ -120,10 +125,10 @@ class EpgFetchHelper {
newProgramsIndex++;
}
if (addNewProgram) {
- ops.add(ContentProviderOperation
- .newInsert(Programs.CONTENT_URI)
- .withValues(Program.toContentValues(newProgram))
- .build());
+ 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) {
@@ -150,11 +155,17 @@ class EpgFetchHelper {
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)) {
+ 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();
}
@@ -167,8 +178,8 @@ class EpgFetchHelper {
}
/**
- * Returns {@code true} if the {@code oldProgram} needs to be updated with the
- * {@code newProgram}.
+ * 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
@@ -186,24 +197,25 @@ class EpgFetchHelper {
* every time when it needs to fetch EPG data.
*/
@WorkerThread
- synchronized static void setLastLineupId(Context context, String lineupId) {
+ static synchronized 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();
+ 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) {
+ /** Gets the last known lineup ID from shared preferences. */
+ static synchronized String getLastLineupId(Context context) {
if (sLastLineupId == null) {
- sLastLineupId = PreferenceManager.getDefaultSharedPreferences(context)
- .getString(KEY_LAST_LINEUP_ID, null);
+ sLastLineupId =
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .getString(KEY_LAST_LINEUP_ID, null);
}
if (DEBUG) Log.d(TAG, "Last lineup is " + sLastLineupId);
return sLastLineupId;
@@ -214,20 +226,21 @@ class EpgFetchHelper {
* out-dated, it's not necessary for EPG fetcher to fetch EPG again.
*/
@WorkerThread
- synchronized static void setLastEpgUpdatedTimestamp(Context context, long timestamp) {
+ static synchronized void setLastEpgUpdatedTimestamp(Context context, long timestamp) {
sLastEpgUpdatedTimestamp = timestamp;
- PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(
- KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).apply();
+ 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) {
+ /** Gets the last updated timestamp of EPG data. */
+ static synchronized long getLastEpgUpdatedTimestamp(Context context) {
if (sLastEpgUpdatedTimestamp < 0) {
- sLastEpgUpdatedTimestamp = PreferenceManager.getDefaultSharedPreferences(context)
- .getLong(KEY_LAST_UPDATED_EPG_TIMESTAMP, 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/EpgFetchService.java b/src/com/android/tv/data/epg/EpgFetchService.java
new file mode 100644
index 00000000..aa4f3588
--- /dev/null
+++ b/src/com/android/tv/data/epg/EpgFetchService.java
@@ -0,0 +1,70 @@
+/*
+ * 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.app.job.JobParameters;
+import android.app.job.JobService;
+import com.android.tv.Starter;
+import com.android.tv.TvSingletons;
+import com.android.tv.data.ChannelDataManager;
+
+/** JobService to Fetch EPG data. */
+public class EpgFetchService extends JobService {
+ private EpgFetcher mEpgFetcher;
+ private ChannelDataManager mChannelDataManager;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Starter.start(this);
+ TvSingletons tvSingletons = TvSingletons.getSingletons(getApplicationContext());
+ mEpgFetcher = tvSingletons.getEpgFetcher();
+ mChannelDataManager = tvSingletons.getChannelDataManager();
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ if (!mChannelDataManager.isDbLoadFinished()) {
+ mChannelDataManager.addListener(
+ new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ mChannelDataManager.removeListener(this);
+ if (!mEpgFetcher.executeFetchTaskIfPossible(
+ EpgFetchService.this, params)) {
+ jobFinished(params, false);
+ }
+ }
+
+ @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;
+ }
+}
diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java
index 24f8b826..9c24613d 100644
--- a/src/com/android/tv/data/epg/EpgFetcher.java
+++ b/src/com/android/tv/data/epg/EpgFetcher.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -16,720 +16,44 @@
package com.android.tv.data.epg;
-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.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.support.annotation.AnyThread;
import android.support.annotation.MainThread;
-import android.support.annotation.Nullable;
-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.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.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.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.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-/**
- * 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 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 FETCH_DURING_SCAN_WAIT_TIME_MS = TimeUnit.SECONDS.toMillis(10);
-
- 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 int DEFAULT_ROUTINE_INTERVAL_HOUR = 4;
- private static final String KEY_ROUTINE_INTERVAL = "live_channels_epg_fetcher_interval_hour";
-
- 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 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);
- }
- return sInstance;
- }
-
- /** Creates and returns {@link EpgReader}. */
- public static EpgReader createEpgReader(Context context, String region) {
- return new StubEpgReader(context);
- }
-
- private EpgFetcher(Context context) {
- 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;
- }
+/** Fetch EPG routinely or on-demand during channel scanning */
+public interface EpgFetcher {
/**
* 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.
+ * 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 fetchImmediatelyIfNeeded() {
- if (TvCommonUtils.isRunningInTest()) {
- // Do not run EpgFetcher in test.
- return;
- }
- new AsyncTask<Void, Void, Long>() {
- @Override
- protected Long doInBackground(Void... args) {
- return EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext);
- }
-
- @Override
- 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);
- }
-
- /**
- * 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);
- }
-
- @Override
- public void onChannelListUpdated() { }
-
- @Override
- public void onChannelBrowsableChanged() { }
- });
- } else {
- executeFetchTaskIfPossible(null, null);
- }
- }
+ void startRoutineService();
/**
- * Notifies EPG fetch service that channel scanning is 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 onChannelScanStarted() {
- if (mScanStarted || !Features.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) {
- return;
- }
- mScanStarted = true;
- stopFetchingJob();
- synchronized (mFetchDuringScanHandlerLock) {
- if (mFetchDuringScanHandler == null) {
- HandlerThread thread = new HandlerThread("EpgFetchDuringScan");
- thread.start();
- mFetchDuringScanHandler = new FetchDuringScanHandler(thread.getLooper());
- }
- mFetchDuringScanHandler.sendEmptyMessage(MSG_PREPARE_FETCH_DURING_SCAN);
- }
- Log.i(TAG, "EPG fetching on channel scanning started.");
- }
+ void fetchImmediatelyIfNeeded();
- /**
- * Notifies EPG fetch service that channel scanning is finished.
- */
+ /** Fetches EPG immediately. */
@MainThread
- public void onChannelScanFinished() {
- if (!mScanStarted) {
- return;
- }
- mScanStarted = false;
- mFetchDuringScanHandler.sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN);
- }
+ void fetchImmediately();
+ /** Notifies EPG fetch service that channel scanning is started. */
@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.");
- }
- }
+ void onChannelScanStarted();
+ /** Notifies EPG fetch service that channel scanning is finished. */
@MainThread
- 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;
- }
+ void onChannelScanFinished();
@MainThread
- 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;
- }
- if (!TextUtils.isEmpty(EpgFetchHelper.getLastLineupId(mContext))) {
- return true;
- }
- if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
- return true;
- }
- return true;
- }
+ boolean executeFetchTaskIfPossible(JobService jobService, JobParameters params);
@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;
- }
-
- @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;
- }
- }
-
- @WorkerThread
- private Integer prepareFetchEpg(boolean forceUpdatePossibleLineups) {
- if (!mEpgReader.isAvailable()) {
- Log.i(TAG, "EPG reader is temporarily unavailable.");
- return REASON_EPG_READER_NOT_READY;
- }
- // 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;
- }
- } 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;
- }
- } 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;
- }
-
- @WorkerThread
- private void batchFetchEpg(List<Channel> channels, long durationSec) {
- Log.i(TAG, "Start batch fetching (" + durationSec + ")...." + channels.size());
- if (channels.size() == 0) {
- return;
- }
- List<Long> queryChannelIds = new ArrayList<>(QUERY_CHANNEL_COUNT);
- for (Channel channel : channels) {
- queryChannelIds.add(channel.getId());
- if (queryChannelIds.size() >= QUERY_CHANNEL_COUNT) {
- batchUpdateEpg(mEpgReader.getPrograms(queryChannelIds, durationSec));
- queryChannelIds.clear();
- }
- }
- if (!queryChannelIds.isEmpty()) {
- batchUpdateEpg(mEpgReader.getPrograms(queryChannelIds, durationSec));
- }
- }
-
- @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);
- }
- }
-
- @Nullable
- @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 maxLineupId;
- }
-
- @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());
- }
- }
- numbers.retainAll(mEpgReader.getChannelNumbers(lineupId));
- return numbers.size();
- }
-
- public static class EpgFetchService extends JobService {
- private EpgFetcher mEpgFetcher;
-
- @Override
- public void onCreate() {
- super.onCreate();
- TvApplication.setCurrentRunningProcess(this, true);
- mEpgFetcher = EpgFetcher.getInstance(this);
- }
-
- @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);
- }
- }
-
- @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;
- }
- }
-
- 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;
- }
-
- @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 {
- Log.i(TAG, "Failed to get lineup id");
- return REASON_NO_EPG_DATA_RETURNED;
- }
- 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;
- }
- 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);
- }
- EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, mEpgTimeStamp);
- if (DEBUG) Log.d(TAG, "Fetching EPG is finished.");
- return null;
- } finally {
- TrafficStats.setThreadStatsTag(oldTag);
- }
- }
-
- @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);
- }
- mPerformanceMonitor.stopTimer(mTimerEvent, EventNames.FETCH_EPG_TASK);
- mPerformanceMonitor.recordMemory(EventNames.FETCH_EPG_TASK);
- }
-
- @Override
- protected void onCancelled(Integer failureReason) {
- clearUnusedLineups(null);
- jobFinished(false);
- }
-
- private void jobFinished(boolean reschedule) {
- if (mService != null && mParams != null) {
- // Task is executed from JobService, need to report jobFinished.
- mService.jobFinished(mParams, reschedule);
- }
- }
- }
-
- @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
- }
- };
-
- @AnyThread
- private FetchDuringScanHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_PREPARE_FETCH_DURING_SCAN:
- case MSG_RETRY_PREPARE_FETCH_DURING_SCAN:
- onPrepareFetchDuringScan();
- break;
- 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 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();
- }
- });
- }
- }
+ void stopFetchingJob();
}
diff --git a/src/com/android/tv/data/epg/EpgFetcherImpl.java b/src/com/android/tv/data/epg/EpgFetcherImpl.java
new file mode 100644
index 00000000..2aaaa5b2
--- /dev/null
+++ b/src/com/android/tv/data/epg/EpgFetcherImpl.java
@@ -0,0 +1,811 @@
+/*
+ * Copyright (C) 2016 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.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.database.Cursor;
+import android.media.tv.TvContract;
+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.support.annotation.AnyThread;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.tv.TvFeatures;
+import com.android.tv.TvSingletons;
+import com.android.tv.common.BuildConfig;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.config.api.RemoteConfigValue;
+import com.android.tv.common.util.Clock;
+import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.util.LocationUtils;
+import com.android.tv.common.util.NetworkTrafficTags;
+import com.android.tv.common.util.PermissionUtils;
+import com.android.tv.common.util.PostalCodeUtils;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ChannelImpl;
+import com.android.tv.data.ChannelLogoFetcher;
+import com.android.tv.data.Lineup;
+import com.android.tv.data.Program;
+import com.android.tv.data.api.Channel;
+import com.android.tv.perf.EventNames;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
+import com.android.tv.util.Utils;
+import com.google.android.tv.partner.support.EpgInput;
+import com.google.android.tv.partner.support.EpgInputs;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 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 EpgFetcherImpl implements EpgFetcher {
+ private static final String TAG = "EpgFetcherImpl";
+ private static final boolean DEBUG = false;
+
+ private static final int EPG_ROUTINELY_FETCHING_JOB_ID = 101;
+
+ private static final long INITIAL_BACKOFF_MS = TimeUnit.SECONDS.toMillis(10);
+
+ @VisibleForTesting static final int REASON_EPG_READER_NOT_READY = 1;
+ @VisibleForTesting static final int REASON_LOCATION_INFO_UNAVAILABLE = 2;
+ @VisibleForTesting static final int REASON_LOCATION_PERMISSION_NOT_GRANTED = 3;
+ @VisibleForTesting static final int REASON_NO_EPG_DATA_RETURNED = 4;
+ @VisibleForTesting static final int REASON_NO_NEW_EPG = 5;
+ @VisibleForTesting static final int REASON_ERROR = 6;
+ @VisibleForTesting static final int REASON_CLOUD_EPG_FAILURE = 7;
+ @VisibleForTesting static final int REASON_NO_BUILT_IN_CHANNELS = 8;
+
+ private static final long FETCH_DURING_SCAN_WAIT_TIME_MS = TimeUnit.SECONDS.toMillis(10);
+
+ 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 RemoteConfigValue<Long> ROUTINE_INTERVAL_HOUR =
+ RemoteConfigValue.create("live_channels_epg_fetcher_interval_hour", 4);
+
+ 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 final Context mContext;
+ private final ChannelDataManager mChannelDataManager;
+ private final EpgReader mEpgReader;
+ 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;
+ private Clock mClock;
+
+ public static EpgFetcher create(Context context) {
+ context = context.getApplicationContext();
+ TvSingletons tvSingletons = TvSingletons.getSingletons(context);
+ ChannelDataManager channelDataManager = tvSingletons.getChannelDataManager();
+ PerformanceMonitor performanceMonitor = tvSingletons.getPerformanceMonitor();
+ EpgReader epgReader = tvSingletons.providesEpgReader().get();
+ Clock clock = tvSingletons.getClock();
+ long routineIntervalMs = ROUTINE_INTERVAL_HOUR.get(tvSingletons.getRemoteConfig());
+
+ return new EpgFetcherImpl(
+ context,
+ channelDataManager,
+ epgReader,
+ performanceMonitor,
+ clock,
+ routineIntervalMs);
+ }
+
+ @VisibleForTesting
+ EpgFetcherImpl(
+ Context context,
+ ChannelDataManager channelDataManager,
+ EpgReader epgReader,
+ PerformanceMonitor performanceMonitor,
+ Clock clock,
+ long routineIntervalMs) {
+ mContext = context;
+ mChannelDataManager = channelDataManager;
+ mEpgReader = epgReader;
+ mPerformanceMonitor = performanceMonitor;
+ mClock = clock;
+ mRoutineIntervalMs =
+ routineIntervalMs <= 0
+ ? TimeUnit.HOURS.toMillis(ROUTINE_INTERVAL_HOUR.getDefaultValue())
+ : TimeUnit.HOURS.toMillis(routineIntervalMs);
+ mEpgDataExpiredTimeLimitMs = routineIntervalMs * 2;
+ mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + routineIntervalMs / 1000;
+ }
+
+ private static Set<Channel> getExistingChannelsForMyPackage(Context context) {
+ HashSet<Channel> channels = new HashSet<>();
+ String selection = null;
+ String[] selectionArgs = null;
+ String myPackageName = context.getPackageName();
+ if (PermissionUtils.hasAccessAllEpg(context)) {
+ selection = "package_name=?";
+ selectionArgs = new String[] {myPackageName};
+ }
+ try (Cursor c =
+ context.getContentResolver()
+ .query(
+ TvContract.Channels.CONTENT_URI,
+ ChannelImpl.PROJECTION,
+ selection,
+ selectionArgs,
+ null)) {
+ if (c != null) {
+ while (c.moveToNext()) {
+ Channel channel = ChannelImpl.fromCursor(c);
+ if (DEBUG) Log.d(TAG, "Found " + channel);
+ if (myPackageName.equals(channel.getPackageName())) {
+ channels.add(channel);
+ }
+ }
+ }
+ }
+ if (DEBUG)
+ Log.d(TAG, "Found " + channels.size() + " channels for package " + myPackageName);
+ return channels;
+ }
+
+ @Override
+ @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.");
+ }
+
+ @Override
+ @MainThread
+ public void fetchImmediatelyIfNeeded() {
+ if (CommonUtils.isRunningInTest()) {
+ // Do not run EpgFetcher in test.
+ return;
+ }
+ new AsyncTask<Void, Void, Long>() {
+ @Override
+ protected Long doInBackground(Void... args) {
+ return EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext);
+ }
+
+ @Override
+ protected void onPostExecute(Long result) {
+ if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
+ > mEpgDataExpiredTimeLimitMs) {
+ Log.i(TAG, "EPG data expired. Start fetching immediately.");
+ fetchImmediately();
+ }
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ @MainThread
+ public void fetchImmediately() {
+ if (DEBUG) Log.d(TAG, "fetchImmediately");
+ if (!mChannelDataManager.isDbLoadFinished()) {
+ mChannelDataManager.addListener(
+ new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ mChannelDataManager.removeListener(this);
+ executeFetchTaskIfPossible(null, null);
+ }
+
+ @Override
+ public void onChannelListUpdated() {}
+
+ @Override
+ public void onChannelBrowsableChanged() {}
+ });
+ } else {
+ executeFetchTaskIfPossible(null, null);
+ }
+ }
+
+ @Override
+ @MainThread
+ public void onChannelScanStarted() {
+ if (mScanStarted || !TvFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) {
+ return;
+ }
+ mScanStarted = true;
+ stopFetchingJob();
+ synchronized (mFetchDuringScanHandlerLock) {
+ if (mFetchDuringScanHandler == null) {
+ HandlerThread thread = new HandlerThread("EpgFetchDuringScan");
+ thread.start();
+ mFetchDuringScanHandler = new FetchDuringScanHandler(thread.getLooper());
+ }
+ mFetchDuringScanHandler.sendEmptyMessage(MSG_PREPARE_FETCH_DURING_SCAN);
+ }
+ Log.i(TAG, "EPG fetching on channel scanning started.");
+ }
+
+ @Override
+ @MainThread
+ public void onChannelScanFinished() {
+ if (!mScanStarted) {
+ return;
+ }
+ mScanStarted = false;
+ mFetchDuringScanHandler.sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN);
+ }
+
+ @MainThread
+ @Override
+ public 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.");
+ }
+ }
+
+ @MainThread
+ @Override
+ public boolean executeFetchTaskIfPossible(JobService service, JobParameters params) {
+ if (DEBUG) Log.d(TAG, "executeFetchTaskIfPossible");
+ SoftPreconditions.checkState(mChannelDataManager.isDbLoadFinished());
+ if (!CommonUtils.isRunningInTest() && checkFetchPrerequisite()) {
+ mFetchTask = createFetchTask(service, params);
+ mFetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ return true;
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ FetchAsyncTask createFetchTask(JobService service, JobParameters params) {
+ return new FetchAsyncTask(service, params);
+ }
+
+ @MainThread
+ private boolean checkFetchPrerequisite() {
+ if (DEBUG) Log.d(TAG, "Check prerequisite of routinely fetching job.");
+ if (!TvFeatures.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;
+ }
+ return true;
+ }
+
+ @MainThread
+ private int getTunerChannelCount() {
+ for (TvInputInfo input :
+ TvSingletons.getSingletons(mContext)
+ .getTvInputManagerHelper()
+ .getTvInputInfos(true, true)) {
+ String inputId = input.getId();
+ if (Utils.isInternalTvInput(mContext, inputId)) {
+ return mChannelDataManager.getChannelCountForInput(inputId);
+ }
+ }
+ return 0;
+ }
+
+ @AnyThread
+ private void clearUnusedLineups(@Nullable String lineupId) {
+ synchronized (mPossibleLineupsLock) {
+ if (mPossibleLineups == null) {
+ return;
+ }
+ for (Lineup lineup : mPossibleLineups) {
+ if (!TextUtils.equals(lineupId, lineup.getId())) {
+ mEpgReader.clearCachedChannels(lineup.getId());
+ }
+ }
+ mPossibleLineups = null;
+ }
+ }
+
+ @WorkerThread
+ private Integer prepareFetchEpg(boolean forceUpdatePossibleLineups) {
+ if (!mEpgReader.isAvailable()) {
+ Log.i(TAG, "EPG reader is temporarily unavailable.");
+ return REASON_EPG_READER_NOT_READY;
+ }
+ // 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;
+ }
+ } 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;
+ }
+ } 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.
+ String lastPostalCode = PostalCodeUtils.getLastPostalCode(mContext);
+ List<Lineup> possibleLineups = mEpgReader.getLineups(lastPostalCode);
+ if (possibleLineups.isEmpty()) {
+ Log.i(TAG, "No lineups found for " + lastPostalCode);
+ return REASON_NO_EPG_DATA_RETURNED;
+ }
+ for (Lineup lineup : possibleLineups) {
+ mEpgReader.preloadChannels(lineup.getId());
+ }
+ synchronized (mPossibleLineupsLock) {
+ mPossibleLineups = possibleLineups;
+ }
+ EpgFetchHelper.setLastLineupId(mContext, null);
+ }
+ return null;
+ }
+
+ @WorkerThread
+ private void batchFetchEpg(Set<EpgReader.EpgChannel> epgChannels, long durationSec) {
+ Log.i(TAG, "Start batch fetching (" + durationSec + ")...." + epgChannels.size());
+ if (epgChannels.size() == 0) {
+ return;
+ }
+ Set<EpgReader.EpgChannel> batch = new HashSet<>(QUERY_CHANNEL_COUNT);
+ for (EpgReader.EpgChannel epgChannel : epgChannels) {
+ batch.add(epgChannel);
+ if (batch.size() >= QUERY_CHANNEL_COUNT) {
+ batchUpdateEpg(mEpgReader.getPrograms(batch, durationSec));
+ batch.clear();
+ }
+ }
+ if (!batch.isEmpty()) {
+ batchUpdateEpg(mEpgReader.getPrograms(batch, durationSec));
+ }
+ }
+
+ @WorkerThread
+ private void batchUpdateEpg(Map<EpgReader.EpgChannel, Collection<Program>> allPrograms) {
+ for (Map.Entry<EpgReader.EpgChannel, Collection<Program>> entry : allPrograms.entrySet()) {
+ List<Program> programs = new ArrayList(entry.getValue());
+ if (programs == null) {
+ continue;
+ }
+ Collections.sort(programs);
+ Log.i(
+ TAG,
+ "Batch fetched " + programs.size() + " programs for channel " + entry.getKey());
+ EpgFetchHelper.updateEpgData(
+ mContext, mClock, entry.getKey().getChannel().getId(), programs);
+ }
+ }
+
+ @Nullable
+ @WorkerThread
+ private String pickBestLineupId(Set<Channel> currentChannels) {
+ String maxLineupId = null;
+ synchronized (mPossibleLineupsLock) {
+ if (mPossibleLineups == null) {
+ return null;
+ }
+ int maxCount = 0;
+ for (Lineup lineup : mPossibleLineups) {
+ int count = getMatchedChannelCount(lineup.getId(), currentChannels);
+ Log.i(TAG, lineup.getName() + " (" + lineup.getId() + ") - " + count + " matches");
+ if (count > maxCount) {
+ maxCount = count;
+ maxLineupId = lineup.getId();
+ }
+ }
+ }
+ return maxLineupId;
+ }
+
+ @WorkerThread
+ private int getMatchedChannelCount(String lineupId, Set<Channel> currentChannels) {
+ // Construct a list of display numbers for existing channels.
+ if (currentChannels.isEmpty()) {
+ if (DEBUG) Log.d(TAG, "No existing channel to compare");
+ return 0;
+ }
+ List<String> numbers = new ArrayList<>(currentChannels.size());
+ for (Channel channel : currentChannels) {
+ // We only support channels from internal tuner inputs.
+ if (Utils.isInternalTvInput(mContext, channel.getInputId())) {
+ numbers.add(channel.getDisplayNumber());
+ }
+ }
+ numbers.retainAll(mEpgReader.getChannelNumbers(lineupId));
+ return numbers.size();
+ }
+
+ @VisibleForTesting
+ class FetchAsyncTask extends AsyncTask<Void, Void, Integer> {
+ private final JobService mService;
+ private final JobParameters mParams;
+ private Set<Channel> mCurrentChannels;
+ private TimerEvent mTimerEvent;
+
+ private FetchAsyncTask(JobService service, JobParameters params) {
+ mService = service;
+ mParams = params;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ mTimerEvent = mPerformanceMonitor.startTimer();
+ mCurrentChannels = new HashSet<>(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 builtInResult = fetchEpgForBuiltInTuner();
+ boolean anyCloudEpgFailure = false;
+ boolean anyCloudEpgSuccess = false;
+ return builtInResult;
+ } finally {
+ TrafficStats.setThreadStatsTag(oldTag);
+ }
+ }
+
+ private Set<Channel> getExistingChannelsFor(String inputId) {
+ Set<Channel> result = new HashSet<>();
+ try (Cursor cursor =
+ mContext.getContentResolver()
+ .query(
+ TvContract.buildChannelsUriForInput(inputId),
+ ChannelImpl.PROJECTION,
+ null,
+ null,
+ null)) {
+ while (cursor.moveToNext()) {
+ result.add(ChannelImpl.fromCursor(cursor));
+ }
+ return result;
+ }
+ }
+
+ private Integer fetchEpgForBuiltInTuner() {
+ try {
+ 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(mCurrentChannels) : 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 {
+ Log.i(TAG, "Failed to get lineup id");
+ return REASON_NO_EPG_DATA_RETURNED;
+ }
+ Set<Channel> existingChannelsForMyPackage =
+ getExistingChannelsForMyPackage(mContext);
+ if (existingChannelsForMyPackage.isEmpty()) {
+ return REASON_NO_BUILT_IN_CHANNELS;
+ }
+ return fetchEpgFor(lineupId, existingChannelsForMyPackage);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to update EPG for builtin tuner", e);
+ return REASON_ERROR;
+ }
+ }
+
+ @Nullable
+ private Integer fetchEpgFor(String lineupId, Set<Channel> existingChannels) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Starting Fetching EPG is for "
+ + lineupId
+ + " with channelCount "
+ + existingChannels.size());
+ }
+ final Set<EpgReader.EpgChannel> channels =
+ mEpgReader.getChannels(existingChannels, 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 for " + lineupId);
+ return REASON_NO_EPG_DATA_RETURNED;
+ }
+ if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
+ > mEpgDataExpiredTimeLimitMs) {
+ batchFetchEpg(channels, mFastFetchDurationSec);
+ }
+ new Handler(mContext.getMainLooper())
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ ChannelLogoFetcher.startFetchingChannelLogos(
+ mContext, asChannelList(channels));
+ }
+ });
+ for (EpgReader.EpgChannel epgChannel : channels) {
+ if (this.isCancelled()) {
+ return null;
+ }
+ List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(epgChannel));
+ // InterruptedException might be caught by RPC, we should check it here.
+ Collections.sort(programs);
+ Log.i(
+ TAG,
+ "Fetched "
+ + programs.size()
+ + " programs for channel "
+ + epgChannel.getChannel());
+ EpgFetchHelper.updateEpgData(
+ mContext, mClock, epgChannel.getChannel().getId(), programs);
+ }
+ EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, mEpgTimeStamp);
+ if (DEBUG) Log.d(TAG, "Fetching EPG is for " + lineupId);
+ return null;
+ }
+
+ @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);
+ }
+ mPerformanceMonitor.stopTimer(mTimerEvent, EventNames.FETCH_EPG_TASK);
+ mPerformanceMonitor.recordMemory(EventNames.FETCH_EPG_TASK);
+ }
+
+ @Override
+ protected void onCancelled(Integer failureReason) {
+ clearUnusedLineups(null);
+ jobFinished(false);
+ }
+
+ private void jobFinished(boolean reschedule) {
+ if (mService != null && mParams != null) {
+ // Task is executed from JobService, need to report jobFinished.
+ mService.jobFinished(mParams, reschedule);
+ }
+ }
+ }
+
+ private List<Channel> asChannelList(Set<EpgReader.EpgChannel> epgChannels) {
+ List<Channel> result = new ArrayList<>(epgChannels.size());
+ for (EpgReader.EpgChannel epgChannel : epgChannels) {
+ result.add(epgChannel.getChannel());
+ }
+ return result;
+ }
+
+ @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,
+ getExistingChannelsForMyPackage(mContext))
+ .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,
+ getExistingChannelsForMyPackage(mContext))
+ .sendToTarget();
+ }
+ }
+
+ @Override
+ public void onChannelBrowsableChanged() {
+ // Do nothing
+ }
+ };
+
+ @AnyThread
+ private FetchDuringScanHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_PREPARE_FETCH_DURING_SCAN:
+ case MSG_RETRY_PREPARE_FETCH_DURING_SCAN:
+ onPrepareFetchDuringScan();
+ break;
+ case MSG_CHANNEL_UPDATED_DURING_SCAN:
+ if (!hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
+ onChannelUpdatedDuringScan((Set<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;
+ default:
+ // do nothing
+ }
+ }
+
+ 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(Set<Channel> currentChannels) {
+ String lineupId = pickBestLineupId(currentChannels);
+ 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 : currentChannels) {
+ currentChannelIds.add(channel.getId());
+ }
+ mFetchedChannelIdsDuringScan.retainAll(currentChannelIds);
+ Set<EpgReader.EpgChannel> newChannels = new HashSet<>();
+ for (EpgReader.EpgChannel epgChannel :
+ mEpgReader.getChannels(currentChannels, mPossibleLineupId)) {
+ if (!mFetchedChannelIdsDuringScan.contains(epgChannel.getChannel().getId())) {
+ newChannels.add(epgChannel);
+ mFetchedChannelIdsDuringScan.add(epgChannel.getChannel().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/EpgInputWhiteList.java b/src/com/android/tv/data/epg/EpgInputWhiteList.java
new file mode 100644
index 00000000..eada8b24
--- /dev/null
+++ b/src/com/android/tv/data/epg/EpgInputWhiteList.java
@@ -0,0 +1,103 @@
+/*
+ * 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.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.tv.common.BuildConfig;
+import com.android.tv.common.config.api.RemoteConfig;
+import com.android.tv.common.experiments.Experiments;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Checks if a package or a input is white listed. */
+public final class EpgInputWhiteList {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "EpgInputWhiteList";
+ @VisibleForTesting public static final String KEY = "live_channels_3rd_party_epg_inputs";
+ private static final String QA_DEV_INPUTS =
+ "com.example.partnersupportsampletvinput/.SampleTvInputService,"
+ + "com.android.tv.tuner.sample.dvb/.tvinput.SampleDvbTunerTvInputService";
+
+ /** Returns the package portion of a inputId */
+ @Nullable
+ public static String getPackageFromInput(@Nullable String inputId) {
+ return inputId == null ? null : inputId.substring(0, inputId.indexOf("/"));
+ }
+
+ private final RemoteConfig remoteConfig;
+
+ public EpgInputWhiteList(RemoteConfig remoteConfig) {
+ this.remoteConfig = remoteConfig;
+ }
+
+ public boolean isInputWhiteListed(String inputId) {
+ return getWhiteListedInputs().contains(inputId);
+ }
+
+ public boolean isPackageWhiteListed(String packageName) {
+ if (DEBUG) Log.d(TAG, "isPackageWhiteListed " + packageName);
+ Set<String> whiteList = getWhiteListedInputs();
+ for (String good : whiteList) {
+ try {
+ String goodPackage = getPackageFromInput(good);
+ if (goodPackage.equals(packageName)) {
+ return true;
+ }
+ } catch (Exception e) {
+ if (DEBUG) Log.d(TAG, "Error parsing package name of " + good, e);
+ continue;
+ }
+ }
+ return false;
+ }
+
+ private Set<String> getWhiteListedInputs() {
+ Set<String> result = toInputSet(remoteConfig.getString(KEY));
+ if (BuildConfig.ENG || Experiments.ENABLE_QA_FEATURES.get()) {
+ HashSet<String> moreInputs = new HashSet<>(toInputSet(QA_DEV_INPUTS));
+ if (result.isEmpty()) {
+ result = moreInputs;
+ } else {
+ result.addAll(moreInputs);
+ }
+ }
+ if (DEBUG) Log.d(TAG, "getWhiteListedInputs " + result);
+ return result;
+ }
+
+ @VisibleForTesting
+ static Set<String> toInputSet(String value) {
+ if (TextUtils.isEmpty(value)) {
+ return Collections.emptySet();
+ }
+ List<String> strings = Arrays.asList(value.split(","));
+ Set<String> result = new HashSet<>(strings.size());
+ for (String s : strings) {
+ String trimmed = s.trim();
+ if (!TextUtils.isEmpty(trimmed)) {
+ result.add(trimmed);
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java
index c5aeca27..7147905a 100644
--- a/src/com/android/tv/data/epg/EpgReader.java
+++ b/src/com/android/tv/data/epg/EpgReader.java
@@ -19,28 +19,37 @@ 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.data.api.Channel;
import com.android.tv.dvr.data.SeriesInfo;
-
+import java.util.Collection;
import java.util.List;
import java.util.Map;
+import java.util.Set;
-/**
- * An interface used to retrieve the EPG data. This class should be used in worker thread.
- */
+/** An interface used to retrieve the EPG data. This class should be used in worker thread. */
@WorkerThread
public interface EpgReader {
- /**
- * Checks if the reader is available.
- */
+
+ /** Value class that holds a EpgChannelId and its corresponding {@link Channel} */
+ // TODO(b/72052568): Get autovalue to work in aosp master
+ abstract class EpgChannel {
+ public static EpgChannel createEpgChannel(Channel channel, String epgChannelId) {
+ return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId);
+ }
+
+ public abstract Channel getChannel();
+
+ public abstract String getEpgChannelId();
+ }
+
+ /** Checks if the reader is available. */
boolean isAvailable();
/**
- * Returns the timestamp of the current EPG.
- * The format should be YYYYMMDDHHmmSS as a long value. ex) 20160308141500
+ * Returns the timestamp of the current EPG. The format should be YYYYMMDDHHmmSS as a long
+ * value. ex) 20160308141500
*/
long getEpgTimestamp();
@@ -61,31 +70,30 @@ public interface EpgReader {
* 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);
+ Set<EpgChannel> getChannels(Set<Channel> inputChannels, @NonNull String lineupId);
/** Pre-loads and caches channels for a given lineup. */
void preloadChannels(@NonNull String lineupId);
- /**
- * Clears cached channels for a given lineup.
- */
+ /** Clears cached channels for a given lineup. */
@AnyThread
void clearCachedChannels(@NonNull String lineupId);
/**
- * Returns the programs for the given channel. Must call {@link #getChannels(String)}
+ * Returns the programs for the given channel. Must call {@link #getChannels(Set, 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);
+ List<Program> getPrograms(EpgChannel epgChannel);
/**
* 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.
*/
- Map<Long, List<Program>> getPrograms(@NonNull List<Long> channelIds, long duration);
+ Map<EpgChannel, Collection<Program>> getPrograms(
+ @NonNull Set<EpgChannel> epgChannels, 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 ab6935ad..3b001481 100644
--- a/src/com/android/tv/data/epg/StubEpgReader.java
+++ b/src/com/android/tv/data/epg/StubEpgReader.java
@@ -17,23 +17,20 @@
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.data.api.Channel;
import com.android.tv.dvr.data.SeriesInfo;
-
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Set;
-/**
- * A stub class to read EPG.
- */
-public class StubEpgReader implements EpgReader{
- public StubEpgReader(@SuppressWarnings("unused") Context context) {
- }
+/** A stub class to read EPG. */
+public class StubEpgReader implements EpgReader {
+ public StubEpgReader(@SuppressWarnings("unused") Context context) {}
@Override
public boolean isAvailable() {
@@ -61,8 +58,8 @@ public class StubEpgReader implements EpgReader{
}
@Override
- public List<Channel> getChannels(@NonNull String lineupId) {
- return Collections.emptyList();
+ public Set<EpgChannel> getChannels(Set<Channel> inputChannels, @NonNull String lineupId) {
+ return Collections.emptySet();
}
@Override
@@ -76,12 +73,13 @@ public class StubEpgReader implements EpgReader{
}
@Override
- public List<Program> getPrograms(long channelId) {
+ public List<Program> getPrograms(EpgChannel epgChannel) {
return Collections.emptyList();
}
@Override
- public Map<Long, List<Program>> getPrograms(@NonNull List<Long> channelIds, long duration) {
+ public Map<EpgChannel, Collection<Program>> getPrograms(
+ @NonNull Set<EpgChannel> channels, long duration) {
return Collections.emptyMap();
}
@@ -89,4 +87,4 @@ public class StubEpgReader implements EpgReader{
public SeriesInfo getSeriesInfo(@NonNull String seriesId) {
return null;
}
-} \ No newline at end of file
+}