diff options
Diffstat (limited to 'src/com/android/tv/data/epg/EpgFetcherImpl.java')
-rw-r--r-- | src/com/android/tv/data/epg/EpgFetcherImpl.java | 811 |
1 files changed, 811 insertions, 0 deletions
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(); + } + }); + } + } +} |