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