/*
* 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.TvSingletons;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.buildtype.HasBuildType;
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.features.TvFeatures;
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 com.google.common.collect.ImmutableSet;
import com.android.tv.common.flags.BackendKnobsFlags;
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
*
*
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 long DEFAULT_ROUTINE_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 final EpgInputWhiteList mEpgInputWhiteList;
private final BackendKnobsFlags mBackendKnobsFlags;
private final HasBuildType.BuildType mBuildType;
private FetchAsyncTask mFetchTask;
private FetchDuringScanHandler mFetchDuringScanHandler;
private long mEpgTimeStamp;
private List 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 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();
EpgInputWhiteList epgInputWhiteList =
new EpgInputWhiteList(tvSingletons.getCloudEpgFlags());
BackendKnobsFlags backendKnobsFlags = tvSingletons.getBackendKnobs();
HasBuildType.BuildType buildType = tvSingletons.getBuildType();
return new EpgFetcherImpl(
context,
epgInputWhiteList,
channelDataManager,
epgReader,
performanceMonitor,
clock,
backendKnobsFlags,
buildType);
}
@VisibleForTesting
EpgFetcherImpl(
Context context,
EpgInputWhiteList epgInputWhiteList,
ChannelDataManager channelDataManager,
EpgReader epgReader,
PerformanceMonitor performanceMonitor,
Clock clock,
BackendKnobsFlags backendKnobsFlags,
HasBuildType.BuildType buildType) {
mContext = context;
mChannelDataManager = channelDataManager;
mEpgReader = epgReader;
mPerformanceMonitor = performanceMonitor;
mClock = clock;
mEpgInputWhiteList = epgInputWhiteList;
mBackendKnobsFlags = backendKnobsFlags;
mBuildType = buildType;
}
private long getFastFetchDurationSec() {
return FAST_FETCH_DURATION_SEC + getRoutineIntervalMs() / 1000;
}
private long getEpgDataExpiredTimeLimitMs() {
return getRoutineIntervalMs() * 2;
}
private long getRoutineIntervalMs() {
long routineIntervalHours = mBackendKnobsFlags.epgFetcherIntervalHour();
return routineIntervalHours <= 0
? TimeUnit.HOURS.toMillis(DEFAULT_ROUTINE_INTERVAL_HOUR)
: TimeUnit.HOURS.toMillis(routineIntervalHours);
}
private static Set getExistingChannelsForMyPackage(Context context) {
HashSet 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(getRoutineIntervalMs())
.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() {
@Override
protected Long doInBackground(Void... args) {
return EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext);
}
@Override
protected void onPostExecute(Long result) {
if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
> getEpgDataExpiredTimeLimitMs()) {
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;
}
if (!TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.isEnabled(mContext)
&& mBuildType != HasBuildType.BuildType.AOSP) {
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;
}
@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 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 epgChannels, long durationSec) {
Log.i(TAG, "Start batch fetching (" + durationSec + ")...." + epgChannels.size());
if (epgChannels.size() == 0) {
return;
}
Set 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> allPrograms) {
for (Map.Entry> entry : allPrograms.entrySet()) {
List 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 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 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 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();
}
private boolean isInputInWhiteList(EpgInput epgInput) {
if (mBuildType == HasBuildType.BuildType.AOSP) {
return false;
}
return (BuildConfig.ENG
&& epgInput.getInputId()
.equals(
"com.example.partnersupportsampletvinput/.SampleTvInputService"))
|| mEpgInputWhiteList.isInputWhiteListed(epgInput.getInputId());
}
@VisibleForTesting
class FetchAsyncTask extends AsyncTask {
private final JobService mService;
private final JobParameters mParams;
private Set 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;
if (TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.isEnabled(mContext)
&& mBuildType != HasBuildType.BuildType.AOSP) {
for (EpgInput epgInput : getEpgInputs()) {
if (DEBUG) Log.d(TAG, "Start EPG fetch for " + epgInput);
if (isCancelled()) {
break;
}
if (isInputInWhiteList(epgInput)) {
// TODO(b/66191312) check timestamp and result code and decide if update
// is needed.
Set channels = getExistingChannelsFor(epgInput.getInputId());
Integer result = fetchEpgFor(epgInput.getLineupId(), channels);
anyCloudEpgFailure = anyCloudEpgFailure || result != null;
anyCloudEpgSuccess = anyCloudEpgSuccess || result == null;
updateCloudEpgInput(epgInput, result);
} else {
Log.w(
TAG,
"Fetching the EPG for "
+ epgInput.getInputId()
+ " is not supported.");
}
}
}
if (builtInResult == null || builtInResult == REASON_NO_BUILT_IN_CHANNELS) {
return anyCloudEpgFailure
? ((Integer) REASON_CLOUD_EPG_FAILURE)
: anyCloudEpgSuccess ? null : builtInResult;
}
return builtInResult;
} finally {
TrafficStats.setThreadStatsTag(oldTag);
}
}
private void updateCloudEpgInput(EpgInput unusedEpgInput, Integer unusedResult) {
// TODO(b/66191312) write the result and timestamp to the input table
}
private Set getExistingChannelsFor(String inputId) {
Set result = new HashSet<>();
try (Cursor cursor =
mContext.getContentResolver()
.query(
TvContract.buildChannelsUriForInput(inputId),
ChannelImpl.PROJECTION,
null,
null,
null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
result.add(ChannelImpl.fromCursor(cursor));
}
}
return result;
}
}
private Set getEpgInputs() {
if (mBuildType == HasBuildType.BuildType.AOSP) {
return ImmutableSet.of();
}
Set epgInputs = EpgInputs.queryEpgInputs(mContext.getContentResolver());
if (DEBUG) Log.d(TAG, "getEpgInputs " + epgInputs);
return epgInputs;
}
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 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 existingChannels) {
if (DEBUG) {
Log.d(
TAG,
"Starting Fetching EPG is for "
+ lineupId
+ " with channelCount "
+ existingChannels.size());
}
final Set 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;
}
EpgFetchHelper.updateNetworkAffiliation(mContext, channels);
if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
> getEpgDataExpiredTimeLimitMs()) {
batchFetchEpg(channels, getFastFetchDurationSec());
}
new Handler(mContext.getMainLooper())
.post(
() ->
ChannelLogoFetcher.startFetchingChannelLogos(
mContext, asChannelList(channels)));
for (EpgReader.EpgChannel epgChannel : channels) {
if (this.isCancelled()) {
return null;
}
List 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 asChannelList(Set epgChannels) {
List 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 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) 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 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 currentChannelIds = new ArrayList<>();
for (Channel channel : currentChannels) {
currentChannelIds.add(channel.getId());
}
mFetchedChannelIdsDuringScan.retainAll(currentChannelIds);
Set 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());
}
}
if (!newChannels.isEmpty()) {
EpgFetchHelper.updateNetworkAffiliation(mContext, newChannels);
}
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(EpgFetcherImpl.this::fetchImmediately);
}
}
}