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 EpgInputAllowList mEpgInputAllowList; 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, EpgInputAllowList epgInputAllowList, ChannelDataManager channelDataManager, EpgReader epgReader, PerformanceMonitor performanceMonitor, Clock clock, BackendKnobsFlags backendKnobsFlags, HasBuildType.BuildType buildType)142 public EpgFetcherImpl( 143 @ApplicationContext Context context, 144 EpgInputAllowList epgInputAllowList, 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 mEpgInputAllowList = epgInputAllowList; 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 isInputAllowed(EpgInput epgInput)519 private boolean isInputAllowed(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 || mEpgInputAllowList.isInputAllowed(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 (isInputAllowed(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