Home | History | Annotate | Download | only in epg
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv.data.epg;
     18 
     19 import android.app.job.JobInfo;
     20 import android.app.job.JobParameters;
     21 import android.app.job.JobScheduler;
     22 import android.app.job.JobService;
     23 import android.content.ComponentName;
     24 import android.content.Context;
     25 import android.database.Cursor;
     26 import android.media.tv.TvContract;
     27 import android.media.tv.TvInputInfo;
     28 import android.net.TrafficStats;
     29 import android.os.AsyncTask;
     30 import android.os.Handler;
     31 import android.os.HandlerThread;
     32 import android.os.Looper;
     33 import android.os.Message;
     34 import android.support.annotation.AnyThread;
     35 import android.support.annotation.MainThread;
     36 import android.support.annotation.Nullable;
     37 import android.support.annotation.VisibleForTesting;
     38 import android.support.annotation.WorkerThread;
     39 import android.text.TextUtils;
     40 import android.util.Log;
     41 import com.android.tv.TvFeatures;
     42 import com.android.tv.TvSingletons;
     43 import com.android.tv.common.BuildConfig;
     44 import com.android.tv.common.SoftPreconditions;
     45 import com.android.tv.common.config.api.RemoteConfigValue;
     46 import com.android.tv.common.util.Clock;
     47 import com.android.tv.common.util.CommonUtils;
     48 import com.android.tv.common.util.LocationUtils;
     49 import com.android.tv.common.util.NetworkTrafficTags;
     50 import com.android.tv.common.util.PermissionUtils;
     51 import com.android.tv.common.util.PostalCodeUtils;
     52 import com.android.tv.data.ChannelDataManager;
     53 import com.android.tv.data.ChannelImpl;
     54 import com.android.tv.data.ChannelLogoFetcher;
     55 import com.android.tv.data.Lineup;
     56 import com.android.tv.data.Program;
     57 import com.android.tv.data.api.Channel;
     58 import com.android.tv.perf.EventNames;
     59 import com.android.tv.perf.PerformanceMonitor;
     60 import com.android.tv.perf.TimerEvent;
     61 import com.android.tv.util.Utils;
     62 import com.google.android.tv.partner.support.EpgInput;
     63 import com.google.android.tv.partner.support.EpgInputs;
     64 import java.io.IOException;
     65 import java.util.ArrayList;
     66 import java.util.Collection;
     67 import java.util.Collections;
     68 import java.util.HashSet;
     69 import java.util.List;
     70 import java.util.Map;
     71 import java.util.Set;
     72 import java.util.concurrent.TimeUnit;
     73 
     74 /**
     75  * The service class to fetch EPG routinely or on-demand during channel scanning
     76  *
     77  * <p>Since the default executor of {@link AsyncTask} is {@link AsyncTask#SERIAL_EXECUTOR}, only one
     78  * task can run at a time. Because fetching EPG takes long time, the fetching task shouldn't run on
     79  * the serial executor. Instead, it should run on the {@link AsyncTask#THREAD_POOL_EXECUTOR}.
     80  */
     81 public class EpgFetcherImpl implements EpgFetcher {
     82     private static final String TAG = "EpgFetcherImpl";
     83     private static final boolean DEBUG = false;
     84 
     85     private static final int EPG_ROUTINELY_FETCHING_JOB_ID = 101;
     86 
     87     private static final long INITIAL_BACKOFF_MS = TimeUnit.SECONDS.toMillis(10);
     88 
     89     @VisibleForTesting static final int REASON_EPG_READER_NOT_READY = 1;
     90     @VisibleForTesting static final int REASON_LOCATION_INFO_UNAVAILABLE = 2;
     91     @VisibleForTesting static final int REASON_LOCATION_PERMISSION_NOT_GRANTED = 3;
     92     @VisibleForTesting static final int REASON_NO_EPG_DATA_RETURNED = 4;
     93     @VisibleForTesting static final int REASON_NO_NEW_EPG = 5;
     94     @VisibleForTesting static final int REASON_ERROR = 6;
     95     @VisibleForTesting static final int REASON_CLOUD_EPG_FAILURE = 7;
     96     @VisibleForTesting static final int REASON_NO_BUILT_IN_CHANNELS = 8;
     97 
     98     private static final long FETCH_DURING_SCAN_WAIT_TIME_MS = TimeUnit.SECONDS.toMillis(10);
     99 
    100     private static final long FETCH_DURING_SCAN_DURATION_SEC = TimeUnit.HOURS.toSeconds(3);
    101     private static final long FAST_FETCH_DURATION_SEC = TimeUnit.DAYS.toSeconds(2);
    102 
    103     private static final RemoteConfigValue<Long> ROUTINE_INTERVAL_HOUR =
    104             RemoteConfigValue.create("live_channels_epg_fetcher_interval_hour", 4);
    105 
    106     private static final int MSG_PREPARE_FETCH_DURING_SCAN = 1;
    107     private static final int MSG_CHANNEL_UPDATED_DURING_SCAN = 2;
    108     private static final int MSG_FINISH_FETCH_DURING_SCAN = 3;
    109     private static final int MSG_RETRY_PREPARE_FETCH_DURING_SCAN = 4;
    110 
    111     private static final int QUERY_CHANNEL_COUNT = 50;
    112     private static final int MINIMUM_CHANNELS_TO_DECIDE_LINEUP = 3;
    113 
    114     private final Context mContext;
    115     private final ChannelDataManager mChannelDataManager;
    116     private final EpgReader mEpgReader;
    117     private final PerformanceMonitor mPerformanceMonitor;
    118     private FetchAsyncTask mFetchTask;
    119     private FetchDuringScanHandler mFetchDuringScanHandler;
    120     private long mEpgTimeStamp;
    121     private List<Lineup> mPossibleLineups;
    122     private final Object mPossibleLineupsLock = new Object();
    123     private final Object mFetchDuringScanHandlerLock = new Object();
    124     // A flag to block the re-entrance of onChannelScanStarted and onChannelScanFinished.
    125     private boolean mScanStarted;
    126 
    127     private final long mRoutineIntervalMs;
    128     private final long mEpgDataExpiredTimeLimitMs;
    129     private final long mFastFetchDurationSec;
    130     private Clock mClock;
    131 
    132     public static EpgFetcher create(Context context) {
    133         context = context.getApplicationContext();
    134         TvSingletons tvSingletons = TvSingletons.getSingletons(context);
    135         ChannelDataManager channelDataManager = tvSingletons.getChannelDataManager();
    136         PerformanceMonitor performanceMonitor = tvSingletons.getPerformanceMonitor();
    137         EpgReader epgReader = tvSingletons.providesEpgReader().get();
    138         Clock clock = tvSingletons.getClock();
    139         long routineIntervalMs = ROUTINE_INTERVAL_HOUR.get(tvSingletons.getRemoteConfig());
    140 
    141         return new EpgFetcherImpl(
    142                 context,
    143                 channelDataManager,
    144                 epgReader,
    145                 performanceMonitor,
    146                 clock,
    147                 routineIntervalMs);
    148     }
    149 
    150     @VisibleForTesting
    151     EpgFetcherImpl(
    152             Context context,
    153             ChannelDataManager channelDataManager,
    154             EpgReader epgReader,
    155             PerformanceMonitor performanceMonitor,
    156             Clock clock,
    157             long routineIntervalMs) {
    158         mContext = context;
    159         mChannelDataManager = channelDataManager;
    160         mEpgReader = epgReader;
    161         mPerformanceMonitor = performanceMonitor;
    162         mClock = clock;
    163         mRoutineIntervalMs =
    164                 routineIntervalMs <= 0
    165                         ? TimeUnit.HOURS.toMillis(ROUTINE_INTERVAL_HOUR.getDefaultValue())
    166                         : TimeUnit.HOURS.toMillis(routineIntervalMs);
    167         mEpgDataExpiredTimeLimitMs = routineIntervalMs * 2;
    168         mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + routineIntervalMs / 1000;
    169     }
    170 
    171     private static Set<Channel> getExistingChannelsForMyPackage(Context context) {
    172         HashSet<Channel> channels = new HashSet<>();
    173         String selection = null;
    174         String[] selectionArgs = null;
    175         String myPackageName = context.getPackageName();
    176         if (PermissionUtils.hasAccessAllEpg(context)) {
    177             selection = "package_name=?";
    178             selectionArgs = new String[] {myPackageName};
    179         }
    180         try (Cursor c =
    181                 context.getContentResolver()
    182                         .query(
    183                                 TvContract.Channels.CONTENT_URI,
    184                                 ChannelImpl.PROJECTION,
    185                                 selection,
    186                                 selectionArgs,
    187                                 null)) {
    188             if (c != null) {
    189                 while (c.moveToNext()) {
    190                     Channel channel = ChannelImpl.fromCursor(c);
    191                     if (DEBUG) Log.d(TAG, "Found " + channel);
    192                     if (myPackageName.equals(channel.getPackageName())) {
    193                         channels.add(channel);
    194                     }
    195                 }
    196             }
    197         }
    198         if (DEBUG)
    199             Log.d(TAG, "Found " + channels.size() + " channels for package " + myPackageName);
    200         return channels;
    201     }
    202 
    203     @Override
    204     @MainThread
    205     public void startRoutineService() {
    206         JobScheduler jobScheduler =
    207                 (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
    208         for (JobInfo job : jobScheduler.getAllPendingJobs()) {
    209             if (job.getId() == EPG_ROUTINELY_FETCHING_JOB_ID) {
    210                 return;
    211             }
    212         }
    213         JobInfo job =
    214                 new JobInfo.Builder(
    215                                 EPG_ROUTINELY_FETCHING_JOB_ID,
    216                                 new ComponentName(mContext, EpgFetchService.class))
    217                         .setPeriodic(mRoutineIntervalMs)
    218                         .setBackoffCriteria(INITIAL_BACKOFF_MS, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
    219                         .setPersisted(true)
    220                         .build();
    221         jobScheduler.schedule(job);
    222         Log.i(TAG, "EPG fetching routine service started.");
    223     }
    224 
    225     @Override
    226     @MainThread
    227     public void fetchImmediatelyIfNeeded() {
    228         if (CommonUtils.isRunningInTest()) {
    229             // Do not run EpgFetcher in test.
    230             return;
    231         }
    232         new AsyncTask<Void, Void, Long>() {
    233             @Override
    234             protected Long doInBackground(Void... args) {
    235                 return EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext);
    236             }
    237 
    238             @Override
    239             protected void onPostExecute(Long result) {
    240                 if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
    241                         > mEpgDataExpiredTimeLimitMs) {
    242                     Log.i(TAG, "EPG data expired. Start fetching immediately.");
    243                     fetchImmediately();
    244                 }
    245             }
    246         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    247     }
    248 
    249     @Override
    250     @MainThread
    251     public void fetchImmediately() {
    252         if (DEBUG) Log.d(TAG, "fetchImmediately");
    253         if (!mChannelDataManager.isDbLoadFinished()) {
    254             mChannelDataManager.addListener(
    255                     new ChannelDataManager.Listener() {
    256                         @Override
    257                         public void onLoadFinished() {
    258                             mChannelDataManager.removeListener(this);
    259                             executeFetchTaskIfPossible(null, null);
    260                         }
    261 
    262                         @Override
    263                         public void onChannelListUpdated() {}
    264 
    265                         @Override
    266                         public void onChannelBrowsableChanged() {}
    267                     });
    268         } else {
    269             executeFetchTaskIfPossible(null, null);
    270         }
    271     }
    272 
    273     @Override
    274     @MainThread
    275     public void onChannelScanStarted() {
    276         if (mScanStarted || !TvFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) {
    277             return;
    278         }
    279         mScanStarted = true;
    280         stopFetchingJob();
    281         synchronized (mFetchDuringScanHandlerLock) {
    282             if (mFetchDuringScanHandler == null) {
    283                 HandlerThread thread = new HandlerThread("EpgFetchDuringScan");
    284                 thread.start();
    285                 mFetchDuringScanHandler = new FetchDuringScanHandler(thread.getLooper());
    286             }
    287             mFetchDuringScanHandler.sendEmptyMessage(MSG_PREPARE_FETCH_DURING_SCAN);
    288         }
    289         Log.i(TAG, "EPG fetching on channel scanning started.");
    290     }
    291 
    292     @Override
    293     @MainThread
    294     public void onChannelScanFinished() {
    295         if (!mScanStarted) {
    296             return;
    297         }
    298         mScanStarted = false;
    299         mFetchDuringScanHandler.sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN);
    300     }
    301 
    302     @MainThread
    303     @Override
    304     public void stopFetchingJob() {
    305         if (DEBUG) Log.d(TAG, "Try to stop routinely fetching job...");
    306         if (mFetchTask != null) {
    307             mFetchTask.cancel(true);
    308             mFetchTask = null;
    309             Log.i(TAG, "EPG routinely fetching job stopped.");
    310         }
    311     }
    312 
    313     @MainThread
    314     @Override
    315     public boolean executeFetchTaskIfPossible(JobService service, JobParameters params) {
    316         if (DEBUG) Log.d(TAG, "executeFetchTaskIfPossible");
    317         SoftPreconditions.checkState(mChannelDataManager.isDbLoadFinished());
    318         if (!CommonUtils.isRunningInTest() && checkFetchPrerequisite()) {
    319             mFetchTask = createFetchTask(service, params);
    320             mFetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    321             return true;
    322         }
    323         return false;
    324     }
    325 
    326     @VisibleForTesting
    327     FetchAsyncTask createFetchTask(JobService service, JobParameters params) {
    328         return new FetchAsyncTask(service, params);
    329     }
    330 
    331     @MainThread
    332     private boolean checkFetchPrerequisite() {
    333         if (DEBUG) Log.d(TAG, "Check prerequisite of routinely fetching job.");
    334         if (!TvFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) {
    335             Log.i(
    336                     TAG,
    337                     "Cannot start routine service: country not supported: "
    338                             + LocationUtils.getCurrentCountry(mContext));
    339             return false;
    340         }
    341         if (mFetchTask != null) {
    342             // Fetching job is already running or ready to run, no need to start again.
    343             return false;
    344         }
    345         if (mFetchDuringScanHandler != null) {
    346             if (DEBUG) Log.d(TAG, "Cannot start routine service: scanning channels.");
    347             return false;
    348         }
    349         return true;
    350     }
    351 
    352     @MainThread
    353     private int getTunerChannelCount() {
    354         for (TvInputInfo input :
    355                 TvSingletons.getSingletons(mContext)
    356                         .getTvInputManagerHelper()
    357                         .getTvInputInfos(true, true)) {
    358             String inputId = input.getId();
    359             if (Utils.isInternalTvInput(mContext, inputId)) {
    360                 return mChannelDataManager.getChannelCountForInput(inputId);
    361             }
    362         }
    363         return 0;
    364     }
    365 
    366     @AnyThread
    367     private void clearUnusedLineups(@Nullable String lineupId) {
    368         synchronized (mPossibleLineupsLock) {
    369             if (mPossibleLineups == null) {
    370                 return;
    371             }
    372             for (Lineup lineup : mPossibleLineups) {
    373                 if (!TextUtils.equals(lineupId, lineup.getId())) {
    374                     mEpgReader.clearCachedChannels(lineup.getId());
    375                 }
    376             }
    377             mPossibleLineups = null;
    378         }
    379     }
    380 
    381     @WorkerThread
    382     private Integer prepareFetchEpg(boolean forceUpdatePossibleLineups) {
    383         if (!mEpgReader.isAvailable()) {
    384             Log.i(TAG, "EPG reader is temporarily unavailable.");
    385             return REASON_EPG_READER_NOT_READY;
    386         }
    387         // Checks the EPG Timestamp.
    388         mEpgTimeStamp = mEpgReader.getEpgTimestamp();
    389         if (mEpgTimeStamp <= EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)) {
    390             if (DEBUG) Log.d(TAG, "No new EPG.");
    391             return REASON_NO_NEW_EPG;
    392         }
    393         // Updates postal code.
    394         boolean postalCodeChanged = false;
    395         try {
    396             postalCodeChanged = PostalCodeUtils.updatePostalCode(mContext);
    397         } catch (IOException e) {
    398             if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e);
    399             if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
    400                 return REASON_LOCATION_INFO_UNAVAILABLE;
    401             }
    402         } catch (SecurityException e) {
    403             Log.w(TAG, "No permission to get the current location.");
    404             if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
    405                 return REASON_LOCATION_PERMISSION_NOT_GRANTED;
    406             }
    407         } catch (PostalCodeUtils.NoPostalCodeException e) {
    408             Log.i(TAG, "Cannot get address or postal code.");
    409             return REASON_LOCATION_INFO_UNAVAILABLE;
    410         }
    411         // Updates possible lineups if necessary.
    412         SoftPreconditions.checkState(mPossibleLineups == null, TAG, "Possible lineups not reset.");
    413         if (postalCodeChanged
    414                 || forceUpdatePossibleLineups
    415                 || EpgFetchHelper.getLastLineupId(mContext) == null) {
    416             // To prevent main thread being blocked, though theoretically it should not happen.
    417             String lastPostalCode = PostalCodeUtils.getLastPostalCode(mContext);
    418             List<Lineup> possibleLineups = mEpgReader.getLineups(lastPostalCode);
    419             if (possibleLineups.isEmpty()) {
    420                 Log.i(TAG, "No lineups found for " + lastPostalCode);
    421                 return REASON_NO_EPG_DATA_RETURNED;
    422             }
    423             for (Lineup lineup : possibleLineups) {
    424                 mEpgReader.preloadChannels(lineup.getId());
    425             }
    426             synchronized (mPossibleLineupsLock) {
    427                 mPossibleLineups = possibleLineups;
    428             }
    429             EpgFetchHelper.setLastLineupId(mContext, null);
    430         }
    431         return null;
    432     }
    433 
    434     @WorkerThread
    435     private void batchFetchEpg(Set<EpgReader.EpgChannel> epgChannels, long durationSec) {
    436         Log.i(TAG, "Start batch fetching (" + durationSec + ")...." + epgChannels.size());
    437         if (epgChannels.size() == 0) {
    438             return;
    439         }
    440         Set<EpgReader.EpgChannel> batch = new HashSet<>(QUERY_CHANNEL_COUNT);
    441         for (EpgReader.EpgChannel epgChannel : epgChannels) {
    442             batch.add(epgChannel);
    443             if (batch.size() >= QUERY_CHANNEL_COUNT) {
    444                 batchUpdateEpg(mEpgReader.getPrograms(batch, durationSec));
    445                 batch.clear();
    446             }
    447         }
    448         if (!batch.isEmpty()) {
    449             batchUpdateEpg(mEpgReader.getPrograms(batch, durationSec));
    450         }
    451     }
    452 
    453     @WorkerThread
    454     private void batchUpdateEpg(Map<EpgReader.EpgChannel, Collection<Program>> allPrograms) {
    455         for (Map.Entry<EpgReader.EpgChannel, Collection<Program>> entry : allPrograms.entrySet()) {
    456             List<Program> programs = new ArrayList(entry.getValue());
    457             if (programs == null) {
    458                 continue;
    459             }
    460             Collections.sort(programs);
    461             Log.i(
    462                     TAG,
    463                     "Batch fetched " + programs.size() + " programs for channel " + entry.getKey());
    464             EpgFetchHelper.updateEpgData(
    465                     mContext, mClock, entry.getKey().getChannel().getId(), programs);
    466         }
    467     }
    468 
    469     @Nullable
    470     @WorkerThread
    471     private String pickBestLineupId(Set<Channel> currentChannels) {
    472         String maxLineupId = null;
    473         synchronized (mPossibleLineupsLock) {
    474             if (mPossibleLineups == null) {
    475                 return null;
    476             }
    477             int maxCount = 0;
    478             for (Lineup lineup : mPossibleLineups) {
    479                 int count = getMatchedChannelCount(lineup.getId(), currentChannels);
    480                 Log.i(TAG, lineup.getName() + " (" + lineup.getId() + ") - " + count + " matches");
    481                 if (count > maxCount) {
    482                     maxCount = count;
    483                     maxLineupId = lineup.getId();
    484                 }
    485             }
    486         }
    487         return maxLineupId;
    488     }
    489 
    490     @WorkerThread
    491     private int getMatchedChannelCount(String lineupId, Set<Channel> currentChannels) {
    492         // Construct a list of display numbers for existing channels.
    493         if (currentChannels.isEmpty()) {
    494             if (DEBUG) Log.d(TAG, "No existing channel to compare");
    495             return 0;
    496         }
    497         List<String> numbers = new ArrayList<>(currentChannels.size());
    498         for (Channel channel : currentChannels) {
    499             // We only support channels from internal tuner inputs.
    500             if (Utils.isInternalTvInput(mContext, channel.getInputId())) {
    501                 numbers.add(channel.getDisplayNumber());
    502             }
    503         }
    504         numbers.retainAll(mEpgReader.getChannelNumbers(lineupId));
    505         return numbers.size();
    506     }
    507 
    508     @VisibleForTesting
    509     class FetchAsyncTask extends AsyncTask<Void, Void, Integer> {
    510         private final JobService mService;
    511         private final JobParameters mParams;
    512         private Set<Channel> mCurrentChannels;
    513         private TimerEvent mTimerEvent;
    514 
    515         private FetchAsyncTask(JobService service, JobParameters params) {
    516             mService = service;
    517             mParams = params;
    518         }
    519 
    520         @Override
    521         protected void onPreExecute() {
    522             mTimerEvent = mPerformanceMonitor.startTimer();
    523             mCurrentChannels = new HashSet<>(mChannelDataManager.getChannelList());
    524         }
    525 
    526         @Override
    527         protected Integer doInBackground(Void... args) {
    528             final int oldTag = TrafficStats.getThreadStatsTag();
    529             TrafficStats.setThreadStatsTag(NetworkTrafficTags.EPG_FETCH);
    530             try {
    531                 if (DEBUG) Log.d(TAG, "Start EPG routinely fetching.");
    532                 Integer builtInResult = fetchEpgForBuiltInTuner();
    533                 boolean anyCloudEpgFailure = false;
    534                 boolean anyCloudEpgSuccess = false;
    535                 return builtInResult;
    536             } finally {
    537                 TrafficStats.setThreadStatsTag(oldTag);
    538             }
    539         }
    540 
    541         private Set<Channel> getExistingChannelsFor(String inputId) {
    542             Set<Channel> result = new HashSet<>();
    543             try (Cursor cursor =
    544                     mContext.getContentResolver()
    545                             .query(
    546                                     TvContract.buildChannelsUriForInput(inputId),
    547                                     ChannelImpl.PROJECTION,
    548                                     null,
    549                                     null,
    550                                     null)) {
    551                 while (cursor.moveToNext()) {
    552                     result.add(ChannelImpl.fromCursor(cursor));
    553                 }
    554                 return result;
    555             }
    556         }
    557 
    558         private Integer fetchEpgForBuiltInTuner() {
    559             try {
    560                 Integer failureReason = prepareFetchEpg(false);
    561                 // InterruptedException might be caught by RPC, we should check it here.
    562                 if (failureReason != null || this.isCancelled()) {
    563                     return failureReason;
    564                 }
    565                 String lineupId = EpgFetchHelper.getLastLineupId(mContext);
    566                 lineupId = lineupId == null ? pickBestLineupId(mCurrentChannels) : lineupId;
    567                 if (lineupId != null) {
    568                     Log.i(TAG, "Selecting the lineup " + lineupId);
    569                     // During normal fetching process, the lineup ID should be confirmed since all
    570                     // channels are known, clear up possible lineups to save resources.
    571                     EpgFetchHelper.setLastLineupId(mContext, lineupId);
    572                     clearUnusedLineups(lineupId);
    573                 } else {
    574                     Log.i(TAG, "Failed to get lineup id");
    575                     return REASON_NO_EPG_DATA_RETURNED;
    576                 }
    577                 Set<Channel> existingChannelsForMyPackage =
    578                         getExistingChannelsForMyPackage(mContext);
    579                 if (existingChannelsForMyPackage.isEmpty()) {
    580                     return REASON_NO_BUILT_IN_CHANNELS;
    581                 }
    582                 return fetchEpgFor(lineupId, existingChannelsForMyPackage);
    583             } catch (Exception e) {
    584                 Log.w(TAG, "Failed to update EPG for builtin tuner", e);
    585                 return REASON_ERROR;
    586             }
    587         }
    588 
    589         @Nullable
    590         private Integer fetchEpgFor(String lineupId, Set<Channel> existingChannels) {
    591             if (DEBUG) {
    592                 Log.d(
    593                         TAG,
    594                         "Starting Fetching EPG is for "
    595                                 + lineupId
    596                                 + " with  channelCount "
    597                                 + existingChannels.size());
    598             }
    599             final Set<EpgReader.EpgChannel> channels =
    600                     mEpgReader.getChannels(existingChannels, lineupId);
    601             // InterruptedException might be caught by RPC, we should check it here.
    602             if (this.isCancelled()) {
    603                 return null;
    604             }
    605             if (channels.isEmpty()) {
    606                 Log.i(TAG, "Failed to get EPG channels for " + lineupId);
    607                 return REASON_NO_EPG_DATA_RETURNED;
    608             }
    609             if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
    610                     > mEpgDataExpiredTimeLimitMs) {
    611                 batchFetchEpg(channels, mFastFetchDurationSec);
    612             }
    613             new Handler(mContext.getMainLooper())
    614                     .post(
    615                             new Runnable() {
    616                                 @Override
    617                                 public void run() {
    618                                     ChannelLogoFetcher.startFetchingChannelLogos(
    619                                             mContext, asChannelList(channels));
    620                                 }
    621                             });
    622             for (EpgReader.EpgChannel epgChannel : channels) {
    623                 if (this.isCancelled()) {
    624                     return null;
    625                 }
    626                 List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(epgChannel));
    627                 // InterruptedException might be caught by RPC, we should check it here.
    628                 Collections.sort(programs);
    629                 Log.i(
    630                         TAG,
    631                         "Fetched "
    632                                 + programs.size()
    633                                 + " programs for channel "
    634                                 + epgChannel.getChannel());
    635                 EpgFetchHelper.updateEpgData(
    636                         mContext, mClock, epgChannel.getChannel().getId(), programs);
    637             }
    638             EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, mEpgTimeStamp);
    639             if (DEBUG) Log.d(TAG, "Fetching EPG is for " + lineupId);
    640             return null;
    641         }
    642 
    643         @Override
    644         protected void onPostExecute(Integer failureReason) {
    645             mFetchTask = null;
    646             if (failureReason == null
    647                     || failureReason == REASON_LOCATION_PERMISSION_NOT_GRANTED
    648                     || failureReason == REASON_NO_NEW_EPG) {
    649                 jobFinished(false);
    650             } else {
    651                 // Applies back-off policy
    652                 jobFinished(true);
    653             }
    654             mPerformanceMonitor.stopTimer(mTimerEvent, EventNames.FETCH_EPG_TASK);
    655             mPerformanceMonitor.recordMemory(EventNames.FETCH_EPG_TASK);
    656         }
    657 
    658         @Override
    659         protected void onCancelled(Integer failureReason) {
    660             clearUnusedLineups(null);
    661             jobFinished(false);
    662         }
    663 
    664         private void jobFinished(boolean reschedule) {
    665             if (mService != null && mParams != null) {
    666                 // Task is executed from JobService, need to report jobFinished.
    667                 mService.jobFinished(mParams, reschedule);
    668             }
    669         }
    670     }
    671 
    672     private List<Channel> asChannelList(Set<EpgReader.EpgChannel> epgChannels) {
    673         List<Channel> result = new ArrayList<>(epgChannels.size());
    674         for (EpgReader.EpgChannel epgChannel : epgChannels) {
    675             result.add(epgChannel.getChannel());
    676         }
    677         return result;
    678     }
    679 
    680     @WorkerThread
    681     private class FetchDuringScanHandler extends Handler {
    682         private final Set<Long> mFetchedChannelIdsDuringScan = new HashSet<>();
    683         private String mPossibleLineupId;
    684 
    685         private final ChannelDataManager.Listener mDuringScanChannelListener =
    686                 new ChannelDataManager.Listener() {
    687                     @Override
    688                     public void onLoadFinished() {
    689                         if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()");
    690                         if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP
    691                                 && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
    692                             Message.obtain(
    693                                             FetchDuringScanHandler.this,
    694                                             MSG_CHANNEL_UPDATED_DURING_SCAN,
    695                                             getExistingChannelsForMyPackage(mContext))
    696                                     .sendToTarget();
    697                         }
    698                     }
    699 
    700                     @Override
    701                     public void onChannelListUpdated() {
    702                         if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()");
    703                         if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP
    704                                 && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
    705                             Message.obtain(
    706                                             FetchDuringScanHandler.this,
    707                                             MSG_CHANNEL_UPDATED_DURING_SCAN,
    708                                             getExistingChannelsForMyPackage(mContext))
    709                                     .sendToTarget();
    710                         }
    711                     }
    712 
    713                     @Override
    714                     public void onChannelBrowsableChanged() {
    715                         // Do nothing
    716                     }
    717                 };
    718 
    719         @AnyThread
    720         private FetchDuringScanHandler(Looper looper) {
    721             super(looper);
    722         }
    723 
    724         @Override
    725         public void handleMessage(Message msg) {
    726             switch (msg.what) {
    727                 case MSG_PREPARE_FETCH_DURING_SCAN:
    728                 case MSG_RETRY_PREPARE_FETCH_DURING_SCAN:
    729                     onPrepareFetchDuringScan();
    730                     break;
    731                 case MSG_CHANNEL_UPDATED_DURING_SCAN:
    732                     if (!hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
    733                         onChannelUpdatedDuringScan((Set<Channel>) msg.obj);
    734                     }
    735                     break;
    736                 case MSG_FINISH_FETCH_DURING_SCAN:
    737                     removeMessages(MSG_RETRY_PREPARE_FETCH_DURING_SCAN);
    738                     if (hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
    739                         sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN);
    740                     } else {
    741                         onFinishFetchDuringScan();
    742                     }
    743                     break;
    744                 default:
    745                     // do nothing
    746             }
    747         }
    748 
    749         private void onPrepareFetchDuringScan() {
    750             Integer failureReason = prepareFetchEpg(true);
    751             if (failureReason != null) {
    752                 sendEmptyMessageDelayed(
    753                         MSG_RETRY_PREPARE_FETCH_DURING_SCAN, FETCH_DURING_SCAN_WAIT_TIME_MS);
    754                 return;
    755             }
    756             mChannelDataManager.addListener(mDuringScanChannelListener);
    757         }
    758 
    759         private void onChannelUpdatedDuringScan(Set<Channel> currentChannels) {
    760             String lineupId = pickBestLineupId(currentChannels);
    761             Log.i(TAG, "Fast fetch channels for lineup ID: " + lineupId);
    762             if (TextUtils.isEmpty(lineupId)) {
    763                 if (TextUtils.isEmpty(mPossibleLineupId)) {
    764                     return;
    765                 }
    766             } else if (!TextUtils.equals(lineupId, mPossibleLineupId)) {
    767                 mFetchedChannelIdsDuringScan.clear();
    768                 mPossibleLineupId = lineupId;
    769             }
    770             List<Long> currentChannelIds = new ArrayList<>();
    771             for (Channel channel : currentChannels) {
    772                 currentChannelIds.add(channel.getId());
    773             }
    774             mFetchedChannelIdsDuringScan.retainAll(currentChannelIds);
    775             Set<EpgReader.EpgChannel> newChannels = new HashSet<>();
    776             for (EpgReader.EpgChannel epgChannel :
    777                     mEpgReader.getChannels(currentChannels, mPossibleLineupId)) {
    778                 if (!mFetchedChannelIdsDuringScan.contains(epgChannel.getChannel().getId())) {
    779                     newChannels.add(epgChannel);
    780                     mFetchedChannelIdsDuringScan.add(epgChannel.getChannel().getId());
    781                 }
    782             }
    783             batchFetchEpg(newChannels, FETCH_DURING_SCAN_DURATION_SEC);
    784         }
    785 
    786         private void onFinishFetchDuringScan() {
    787             mChannelDataManager.removeListener(mDuringScanChannelListener);
    788             EpgFetchHelper.setLastLineupId(mContext, mPossibleLineupId);
    789             clearUnusedLineups(null);
    790             mFetchedChannelIdsDuringScan.clear();
    791             synchronized (mFetchDuringScanHandlerLock) {
    792                 if (!hasMessages(MSG_PREPARE_FETCH_DURING_SCAN)) {
    793                     removeCallbacksAndMessages(null);
    794                     getLooper().quit();
    795                     mFetchDuringScanHandler = null;
    796                 }
    797             }
    798             // Clear timestamp to make routine service start right away.
    799             EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, 0);
    800             Log.i(TAG, "EPG Fetching during channel scanning finished.");
    801             new Handler(Looper.getMainLooper())
    802                     .post(
    803                             new Runnable() {
    804                                 @Override
    805                                 public void run() {
    806                                     fetchImmediately();
    807                                 }
    808                             });
    809         }
    810     }
    811 }
    812