• 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.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 
create(Context context)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
EpgFetcherImpl( Context context, ChannelDataManager channelDataManager, EpgReader epgReader, PerformanceMonitor performanceMonitor, Clock clock, long routineIntervalMs)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 
getExistingChannelsForMyPackage(Context context)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
startRoutineService()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
fetchImmediatelyIfNeeded()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
fetchImmediately()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
onChannelScanStarted()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
onChannelScanFinished()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
stopFetchingJob()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
executeFetchTaskIfPossible(JobService service, JobParameters params)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
createFetchTask(JobService service, JobParameters params)327     FetchAsyncTask createFetchTask(JobService service, JobParameters params) {
328         return new FetchAsyncTask(service, params);
329     }
330 
331     @MainThread
checkFetchPrerequisite()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
getTunerChannelCount()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
clearUnusedLineups(@ullable String lineupId)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
prepareFetchEpg(boolean forceUpdatePossibleLineups)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
batchFetchEpg(Set<EpgReader.EpgChannel> epgChannels, long durationSec)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
batchUpdateEpg(Map<EpgReader.EpgChannel, Collection<Program>> allPrograms)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
pickBestLineupId(Set<Channel> currentChannels)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
getMatchedChannelCount(String lineupId, Set<Channel> currentChannels)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 
FetchAsyncTask(JobService service, JobParameters params)515         private FetchAsyncTask(JobService service, JobParameters params) {
516             mService = service;
517             mParams = params;
518         }
519 
520         @Override
onPreExecute()521         protected void onPreExecute() {
522             mTimerEvent = mPerformanceMonitor.startTimer();
523             mCurrentChannels = new HashSet<>(mChannelDataManager.getChannelList());
524         }
525 
526         @Override
doInBackground(Void... args)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 
getExistingChannelsFor(String inputId)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 
fetchEpgForBuiltInTuner()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
fetchEpgFor(String lineupId, Set<Channel> existingChannels)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
onPostExecute(Integer failureReason)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
onCancelled(Integer failureReason)659         protected void onCancelled(Integer failureReason) {
660             clearUnusedLineups(null);
661             jobFinished(false);
662         }
663 
jobFinished(boolean reschedule)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 
asChannelList(Set<EpgReader.EpgChannel> epgChannels)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
FetchDuringScanHandler(Looper looper)720         private FetchDuringScanHandler(Looper looper) {
721             super(looper);
722         }
723 
724         @Override
handleMessage(Message msg)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 
onPrepareFetchDuringScan()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 
onChannelUpdatedDuringScan(Set<Channel> currentChannels)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 
onFinishFetchDuringScan()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