• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
getInstance(Context context)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}. */
createEpgReader(Context context, String region)131     public static EpgReader createEpgReader(Context context, String region) {
132         return new StubEpgReader(context);
133     }
134 
EpgFetcher(Context context)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
fetchImmediately()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
onChannelScanStarted()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
onChannelScanFinished()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
stopFetchingJob()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
executeFetchTaskIfPossible(JobService service, JobParameters params)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
checkFetchPrerequisite()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
getTunerChannelCount()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
clearUnusedLineups(@ullable String lineupId)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
prepareFetchEpg(boolean forceUpdatePossibleLineups)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
batchFetchEpg(List<Channel> channels, long durationSec)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
batchUpdateEpg(Map<Long, List<Program>> allPrograms)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
pickBestLineupId(List<Channel> currentChannelList)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
getMatchedChannelCount(String lineupId, List<Channel> currentChannelList)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
onCreate()467         public void onCreate() {
468             super.onCreate();
469             TvApplication.setCurrentRunningProcess(this, true);
470             mEpgFetcher = EpgFetcher.getInstance(this);
471         }
472 
473         @Override
onStartJob(JobParameters params)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
onStopJob(JobParameters params)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 
FetchAsyncTask(JobService service, JobParameters params)510         private FetchAsyncTask(JobService service, JobParameters params) {
511             mService = service;
512             mParams = params;
513         }
514 
515         @Override
onPreExecute()516         protected void onPreExecute() {
517             mTimerEvent = mPerformanceMonitor.startTimer();
518             mCurrentChannelList = mChannelDataManager.getChannelList();
519         }
520 
521         @Override
doInBackground(Void... args)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
onPostExecute(Integer failureReason)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
onCancelled(Integer failureReason)600         protected void onCancelled(Integer failureReason) {
601             clearUnusedLineups(null);
602             jobFinished(false);
603         }
604 
jobFinished(boolean reschedule)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
FetchDuringScanHandler(Looper looper)649         private FetchDuringScanHandler(Looper looper) {
650             super(looper);
651         }
652 
653         @Override
handleMessage(Message msg)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 
onPrepareFetchDuringScan()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 
onChannelUpdatedDuringScan(List<Channel> currentChannelList)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 
onFinishFetchDuringScan()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