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