| /* |
| * 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 |
| * |
| * <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 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<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 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<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(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<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) |
| > 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<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(); |
| } |
| |
| 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<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; |
| 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<Channel> 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<Channel> getExistingChannelsFor(String inputId) { |
| Set<Channel> 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<EpgInput> getEpgInputs() { |
| if (mBuildType == HasBuildType.BuildType.AOSP) { |
| return ImmutableSet.of(); |
| } |
| Set<EpgInput> 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<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; |
| } |
| 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<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()); |
| } |
| } |
| 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); |
| } |
| } |
| } |