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