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