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 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 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 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 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 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 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 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 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 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 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 327 FetchAsyncTask createFetchTask(JobService service, JobParameters params) { 328 return new FetchAsyncTask(service, params); 329 } 330 331 @MainThread 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 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 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 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 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 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 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 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 515 private FetchAsyncTask(JobService service, JobParameters params) { 516 mService = service; 517 mParams = params; 518 } 519 520 @Override 521 protected void onPreExecute() { 522 mTimerEvent = mPerformanceMonitor.startTimer(); 523 mCurrentChannels = new HashSet<>(mChannelDataManager.getChannelList()); 524 } 525 526 @Override 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 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 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 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 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 659 protected void onCancelled(Integer failureReason) { 660 clearUnusedLineups(null); 661 jobFinished(false); 662 } 663 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 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 720 private FetchDuringScanHandler(Looper looper) { 721 super(looper); 722 } 723 724 @Override 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 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 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 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