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