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