/* * 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 com.android.tv.common.flags.CloudEpgFlags; import com.android.tv.common.flags.LegacyFlags; 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, CloudEpgFlags cloudEpgFlags, LegacyFlags legacyFlags) { 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(cloudEpgFlags, legacyFlags); 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; } clearUnusedLineups(null); 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); } } }