1 /* 2 * Copyright (C) 2015 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; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.database.ContentObserver; 22 import android.database.Cursor; 23 import android.media.tv.TvContract; 24 import android.media.tv.TvContract.Programs; 25 import android.net.Uri; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.os.Message; 29 import android.support.annotation.AnyThread; 30 import android.support.annotation.MainThread; 31 import android.support.annotation.VisibleForTesting; 32 import android.util.ArraySet; 33 import android.util.Log; 34 import android.util.LongSparseArray; 35 import android.util.LruCache; 36 37 import com.android.tv.TvSingletons; 38 import com.android.tv.common.SoftPreconditions; 39 import com.android.tv.common.memory.MemoryManageable; 40 import com.android.tv.common.util.Clock; 41 import com.android.tv.data.api.Channel; 42 import com.android.tv.data.api.Program; 43 import com.android.tv.perf.EventNames; 44 import com.android.tv.perf.PerformanceMonitor; 45 import com.android.tv.perf.TimerEvent; 46 import com.android.tv.util.AsyncDbTask; 47 import com.android.tv.util.MultiLongSparseArray; 48 import com.android.tv.util.TvInputManagerHelper; 49 import com.android.tv.util.TvProviderUtils; 50 import com.android.tv.util.Utils; 51 52 import com.android.tv.common.flags.BackendKnobsFlags; 53 54 import java.util.ArrayList; 55 import java.util.Collections; 56 import java.util.HashMap; 57 import java.util.HashSet; 58 import java.util.List; 59 import java.util.ListIterator; 60 import java.util.Map; 61 import java.util.Objects; 62 import java.util.Set; 63 import java.util.concurrent.ConcurrentHashMap; 64 import java.util.concurrent.Executor; 65 import java.util.concurrent.TimeUnit; 66 67 @MainThread 68 public class ProgramDataManager implements MemoryManageable { 69 private static final String TAG = "ProgramDataManager"; 70 private static final boolean DEBUG = false; 71 72 // To prevent from too many program update operations at the same time, we give random interval 73 // between PERIODIC_PROGRAM_UPDATE_MIN_MS and PERIODIC_PROGRAM_UPDATE_MAX_MS. 74 @VisibleForTesting 75 static final long PERIODIC_PROGRAM_UPDATE_MIN_MS = TimeUnit.MINUTES.toMillis(5); 76 77 private static final long PERIODIC_PROGRAM_UPDATE_MAX_MS = TimeUnit.MINUTES.toMillis(10); 78 private static final long PROGRAM_PREFETCH_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5); 79 // TODO: need to optimize consecutive DB updates. 80 private static final long CURRENT_PROGRAM_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5); 81 @VisibleForTesting static final long PROGRAM_GUIDE_SNAP_TIME_MS = TimeUnit.MINUTES.toMillis(30); 82 83 // Default fetch hours 84 private static final long FETCH_HOURS_MS = TimeUnit.HOURS.toMillis(24); 85 // Load data earlier for smooth scrolling. 86 private static final long BUFFER_HOURS_MS = TimeUnit.HOURS.toMillis(6); 87 88 // TODO: Use TvContract constants, once they become public. 89 private static final String PARAM_START_TIME = "start_time"; 90 private static final String PARAM_END_TIME = "end_time"; 91 // COLUMN_CHANNEL_ID, COLUMN_END_TIME_UTC_MILLIS are added to detect duplicated programs. 92 // Duplicated programs are always consecutive by the sorting order. 93 private static final String SORT_BY_TIME = 94 Programs.COLUMN_START_TIME_UTC_MILLIS 95 + ", " 96 + Programs.COLUMN_CHANNEL_ID 97 + ", " 98 + Programs.COLUMN_END_TIME_UTC_MILLIS; 99 private static final String SORT_BY_CHANNEL_ID = 100 Programs.COLUMN_CHANNEL_ID 101 + ", " 102 + Programs.COLUMN_START_TIME_UTC_MILLIS 103 + " DESC, " 104 + Programs.COLUMN_END_TIME_UTC_MILLIS 105 + " ASC, " 106 + Programs._ID 107 + " DESC"; 108 109 private static final int MSG_UPDATE_CURRENT_PROGRAMS = 1000; 110 private static final int MSG_UPDATE_ONE_CURRENT_PROGRAM = 1001; 111 private static final int MSG_UPDATE_PREFETCH_PROGRAM = 1002; 112 private static final int MSG_UPDATE_CONTENT_RATINGS = 1003; 113 114 private final Context mContext; 115 private final Clock mClock; 116 private final ContentResolver mContentResolver; 117 private final Executor mDbExecutor; 118 private final BackendKnobsFlags mBackendKnobsFlags; 119 private final PerformanceMonitor mPerformanceMonitor; 120 private final ChannelDataManager mChannelDataManager; 121 private final TvInputManagerHelper mTvInputManagerHelper; 122 private boolean mStarted; 123 // Updated only on the main thread. 124 private volatile boolean mCurrentProgramsLoadFinished; 125 private ProgramsUpdateTask mProgramsUpdateTask; 126 private final LongSparseArray<UpdateCurrentProgramForChannelTask> mProgramUpdateTaskMap = 127 new LongSparseArray<>(); 128 private final Map<Long, Program> mChannelIdCurrentProgramMap = new ConcurrentHashMap<>(); 129 private final MultiLongSparseArray<OnCurrentProgramUpdatedListener> 130 mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>(); 131 private final Handler mHandler; 132 private final Set<Callback> mCallbacks = new ArraySet<>(); 133 private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new ConcurrentHashMap<>(); 134 private final Set<Long> mCompleteInfoChannelIds = new HashSet<>(); 135 private final ContentObserver mProgramObserver; 136 137 private boolean mPrefetchEnabled; 138 private long mProgramPrefetchUpdateWaitMs; 139 private long mLastPrefetchTaskRunMs; 140 private ProgramsPrefetchTask mProgramsPrefetchTask; 141 142 // Any program that ends prior to this time will be removed from the cache 143 // when a channel's current program is updated. 144 // Note that there's no limit for end time. 145 private long mPrefetchTimeRangeStartMs; 146 147 private boolean mPauseProgramUpdate = false; 148 private final LruCache<Long, Program> mZeroLengthProgramCache = new LruCache<>(10); 149 // Current tuned channel. 150 private long mTunedChannelId; 151 // Hours of data to be fetched, it is updated during horizontal scroll. 152 // Note that it should never exceed programGuideMaxHours. 153 private long mMaxFetchHoursMs = FETCH_HOURS_MS; 154 155 @MainThread ProgramDataManager(Context context)156 public ProgramDataManager(Context context) { 157 this( 158 context, 159 TvSingletons.getSingletons(context).getDbExecutor(), 160 context.getContentResolver(), 161 Clock.SYSTEM, 162 Looper.myLooper(), 163 TvSingletons.getSingletons(context).getBackendKnobs(), 164 TvSingletons.getSingletons(context).getPerformanceMonitor(), 165 TvSingletons.getSingletons(context).getChannelDataManager(), 166 TvSingletons.getSingletons(context).getTvInputManagerHelper()); 167 } 168 169 @VisibleForTesting ProgramDataManager( Context context, Executor executor, ContentResolver contentResolver, Clock time, Looper looper, BackendKnobsFlags backendKnobsFlags, PerformanceMonitor performanceMonitor, ChannelDataManager channelDataManager, TvInputManagerHelper tvInputManagerHelper)170 ProgramDataManager( 171 Context context, 172 Executor executor, 173 ContentResolver contentResolver, 174 Clock time, 175 Looper looper, 176 BackendKnobsFlags backendKnobsFlags, 177 PerformanceMonitor performanceMonitor, 178 ChannelDataManager channelDataManager, 179 TvInputManagerHelper tvInputManagerHelper) { 180 mContext = context; 181 mDbExecutor = executor; 182 mClock = time; 183 mContentResolver = contentResolver; 184 mHandler = new MyHandler(looper); 185 mBackendKnobsFlags = backendKnobsFlags; 186 mPerformanceMonitor = performanceMonitor; 187 mChannelDataManager = channelDataManager; 188 mTvInputManagerHelper = tvInputManagerHelper; 189 mProgramObserver = 190 new ContentObserver(mHandler) { 191 @Override 192 public void onChange(boolean selfChange) { 193 if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) { 194 mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS); 195 } 196 if (isProgramUpdatePaused()) { 197 return; 198 } 199 if (mPrefetchEnabled) { 200 // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be 201 // quite long 202 // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing 203 // message 204 // and send MSG_UPDATE_PREFETCH_PROGRAM again. 205 mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM); 206 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); 207 } 208 } 209 }; 210 mProgramPrefetchUpdateWaitMs = PROGRAM_PREFETCH_UPDATE_WAIT_MS; 211 } 212 213 @VisibleForTesting getContentObserver()214 ContentObserver getContentObserver() { 215 return mProgramObserver; 216 } 217 218 /** 219 * Set the program prefetch update wait which gives the delay to query all programs from DB to 220 * prevent from too frequent DB queries. Default value is {@link 221 * #PROGRAM_PREFETCH_UPDATE_WAIT_MS} 222 */ 223 @VisibleForTesting setProgramPrefetchUpdateWait(long programPrefetchUpdateWaitMs)224 void setProgramPrefetchUpdateWait(long programPrefetchUpdateWaitMs) { 225 mProgramPrefetchUpdateWaitMs = programPrefetchUpdateWaitMs; 226 } 227 228 /** Starts the manager. */ start()229 public void start() { 230 if (mStarted) { 231 return; 232 } 233 mStarted = true; 234 // Should be called directly instead of posting MSG_UPDATE_CURRENT_PROGRAMS message 235 // to the handler. If not, another DB task can be executed before loading current programs. 236 handleUpdateCurrentPrograms(); 237 mHandler.sendEmptyMessage(MSG_UPDATE_CONTENT_RATINGS); 238 if (mPrefetchEnabled) { 239 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); 240 } 241 mContentResolver.registerContentObserver(Programs.CONTENT_URI, true, mProgramObserver); 242 } 243 244 /** 245 * Stops the manager. It clears manager states and runs pending DB operations. Added listeners 246 * aren't automatically removed by this method. 247 */ 248 @VisibleForTesting stop()249 public void stop() { 250 if (!mStarted) { 251 return; 252 } 253 mStarted = false; 254 mContentResolver.unregisterContentObserver(mProgramObserver); 255 mHandler.removeCallbacksAndMessages(null); 256 257 clearTask(mProgramUpdateTaskMap); 258 cancelPrefetchTask(); 259 if (mProgramsUpdateTask != null) { 260 mProgramsUpdateTask.cancel(true); 261 mProgramsUpdateTask = null; 262 } 263 } 264 265 @AnyThread isCurrentProgramsLoadFinished()266 public boolean isCurrentProgramsLoadFinished() { 267 return mCurrentProgramsLoadFinished; 268 } 269 270 /** Returns the current program at the specified channel. */ 271 @AnyThread getCurrentProgram(long channelId)272 public Program getCurrentProgram(long channelId) { 273 return mChannelIdCurrentProgramMap.get(channelId); 274 } 275 276 /** Returns all the current programs. */ 277 @AnyThread getCurrentPrograms()278 public List<Program> getCurrentPrograms() { 279 return new ArrayList<>(mChannelIdCurrentProgramMap.values()); 280 } 281 282 /** Reloads program data. */ reload()283 public void reload() { 284 if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) { 285 mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS); 286 } 287 if (mPrefetchEnabled && !mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) { 288 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); 289 } 290 } 291 292 /** 293 * Prefetch program data if needed. 294 * 295 * @param channelId ID of the channel to prefetch 296 * @param selectedProgramIndex index of selected program. 297 */ prefetchChannel(long channelId, int selectedProgramIndex)298 public void prefetchChannel(long channelId, int selectedProgramIndex) { 299 long startTimeMs = 300 Utils.floorTime( 301 mClock.currentTimeMillis() - PROGRAM_GUIDE_SNAP_TIME_MS, 302 PROGRAM_GUIDE_SNAP_TIME_MS); 303 long programGuideMaxHoursMs = 304 TimeUnit.HOURS.toMillis(mBackendKnobsFlags.programGuideMaxHours()); 305 long endTimeMs = 0; 306 if (mMaxFetchHoursMs < programGuideMaxHoursMs 307 && isHorizontalLoadNeeded(startTimeMs, channelId, selectedProgramIndex)) { 308 // Horizontal scrolling needs to load data of further days. 309 mMaxFetchHoursMs = Math.min(programGuideMaxHoursMs, mMaxFetchHoursMs + FETCH_HOURS_MS); 310 mCompleteInfoChannelIds.clear(); 311 } 312 // Load max hours complete data for first channel. 313 if (mCompleteInfoChannelIds.isEmpty()) { 314 endTimeMs = startTimeMs + programGuideMaxHoursMs; 315 } else if (!mCompleteInfoChannelIds.contains(channelId)) { 316 endTimeMs = startTimeMs + mMaxFetchHoursMs; 317 } 318 if (endTimeMs > 0) { 319 mCompleteInfoChannelIds.add(channelId); 320 new SingleChannelPrefetchTask(channelId, startTimeMs, endTimeMs).executeOnDbThread(); 321 } 322 } 323 prefetchChannel(long channelId)324 public void prefetchChannel(long channelId) { 325 prefetchChannel(channelId, 0); 326 } 327 328 /** 329 * Check if enough data is present for horizontal scroll, otherwise prefetch programs. 330 * 331 * <p>If end time of current program is past {@code BUFFER_HOURS_MS} less than the fetched time 332 * we need to prefetch proceeding programs. 333 * 334 * @param startTimeMs Fetch start time, it is used to get fetch end time. 335 * @param channelId 336 * @param selectedProgramIndex 337 * @return {@code true} If data load is needed, else {@code false}. 338 */ isHorizontalLoadNeeded( long startTimeMs, long channelId, int selectedProgramIndex)339 private boolean isHorizontalLoadNeeded( 340 long startTimeMs, long channelId, int selectedProgramIndex) { 341 if (mChannelIdProgramCache.containsKey(channelId)) { 342 ArrayList<Program> programs = mChannelIdProgramCache.get(channelId); 343 long marginEndTime = startTimeMs + mMaxFetchHoursMs - BUFFER_HOURS_MS; 344 return programs.size() > selectedProgramIndex && 345 programs.get(selectedProgramIndex).getEndTimeUtcMillis() > marginEndTime; 346 } 347 return false; 348 } 349 onChannelTuned(long channelId)350 public void onChannelTuned(long channelId) { 351 mTunedChannelId = channelId; 352 prefetchChannel(channelId); 353 } 354 355 /** A Callback interface to receive notification on program data retrieval from DB. */ 356 public interface Callback { 357 /** 358 * Called when a Program data is now available through getProgram() after the DB operation 359 * is done which wasn't before. This would be called only if fetched data is around the 360 * selected program. 361 */ onProgramUpdated()362 void onProgramUpdated(); 363 364 /** 365 * Called when we update program data during scrolling. Data is loaded from DB on request 366 * basis. It loads data based on horizontal scrolling as well. 367 */ onChannelUpdated()368 void onChannelUpdated(); 369 } 370 371 /** Adds the {@link Callback}. */ addCallback(Callback callback)372 public void addCallback(Callback callback) { 373 mCallbacks.add(callback); 374 } 375 376 /** Removes the {@link Callback}. */ removeCallback(Callback callback)377 public void removeCallback(Callback callback) { 378 mCallbacks.remove(callback); 379 } 380 381 /** Enables or Disables program prefetch. */ setPrefetchEnabled(boolean enable)382 public void setPrefetchEnabled(boolean enable) { 383 if (mPrefetchEnabled == enable) { 384 return; 385 } 386 if (enable) { 387 mPrefetchEnabled = true; 388 mLastPrefetchTaskRunMs = 0; 389 if (mStarted) { 390 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); 391 } 392 } else { 393 mPrefetchEnabled = false; 394 cancelPrefetchTask(); 395 clearChannelInfoMap(); 396 mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM); 397 } 398 } 399 400 /** 401 * Returns the programs for the given channel which ends after the given start time. 402 * 403 * <p>Prefetch should be enabled to call it. 404 * 405 * @return {@link List} with Programs. It may includes dummy program if the entry needs DB 406 * operations to get. 407 */ getPrograms(long channelId, long startTime)408 public List<Program> getPrograms(long channelId, long startTime) { 409 SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); 410 ArrayList<Program> cachedPrograms = mChannelIdProgramCache.get(channelId); 411 if (cachedPrograms == null) { 412 return Collections.emptyList(); 413 } 414 int startIndex = getProgramIndexAt(cachedPrograms, startTime); 415 return Collections.unmodifiableList( 416 cachedPrograms.subList(startIndex, cachedPrograms.size())); 417 } 418 419 /** 420 * Returns the index of program that is played at the specified time. 421 * 422 * <p>If there isn't, return the first program among programs that starts after the given time 423 * if returnNextProgram is {@code true}. 424 */ getProgramIndexAt(List<Program> programs, long time)425 private int getProgramIndexAt(List<Program> programs, long time) { 426 Program key = mZeroLengthProgramCache.get(time); 427 if (key == null) { 428 key = createDummyProgram(time, time); 429 mZeroLengthProgramCache.put(time, key); 430 } 431 int index = Collections.binarySearch(programs, key); 432 if (index < 0) { 433 index = -(index + 1); // change it to index to be added. 434 if (index > 0 && isProgramPlayedAt(programs.get(index - 1), time)) { 435 // A program is played at that time. 436 return index - 1; 437 } 438 return index; 439 } 440 return index; 441 } 442 isProgramPlayedAt(Program program, long time)443 private boolean isProgramPlayedAt(Program program, long time) { 444 return program.getStartTimeUtcMillis() <= time && time <= program.getEndTimeUtcMillis(); 445 } 446 447 /** 448 * Adds the listener to be notified if current program is updated for a channel. 449 * 450 * @param channelId A channel ID to get notified. If it's {@link Channel#INVALID_ID}, the 451 * listener would be called whenever a current program is updated. 452 */ addOnCurrentProgramUpdatedListener( long channelId, OnCurrentProgramUpdatedListener listener)453 public void addOnCurrentProgramUpdatedListener( 454 long channelId, OnCurrentProgramUpdatedListener listener) { 455 mChannelId2ProgramUpdatedListeners.put(channelId, listener); 456 } 457 458 /** 459 * Removes the listener previously added by {@link #addOnCurrentProgramUpdatedListener(long, 460 * OnCurrentProgramUpdatedListener)}. 461 */ removeOnCurrentProgramUpdatedListener( long channelId, OnCurrentProgramUpdatedListener listener)462 public void removeOnCurrentProgramUpdatedListener( 463 long channelId, OnCurrentProgramUpdatedListener listener) { 464 mChannelId2ProgramUpdatedListeners.remove(channelId, listener); 465 } 466 notifyCurrentProgramUpdate(long channelId, Program program)467 private void notifyCurrentProgramUpdate(long channelId, Program program) { 468 for (OnCurrentProgramUpdatedListener listener : 469 mChannelId2ProgramUpdatedListeners.get(channelId)) { 470 listener.onCurrentProgramUpdated(channelId, program); 471 } 472 for (OnCurrentProgramUpdatedListener listener : 473 mChannelId2ProgramUpdatedListeners.get(Channel.INVALID_ID)) { 474 listener.onCurrentProgramUpdated(channelId, program); 475 } 476 } 477 updateCurrentProgram(long channelId, Program program)478 private void updateCurrentProgram(long channelId, Program program) { 479 Program previousProgram = 480 program == null 481 ? mChannelIdCurrentProgramMap.remove(channelId) 482 : mChannelIdCurrentProgramMap.put(channelId, program); 483 if (!Objects.equals(program, previousProgram)) { 484 if (mPrefetchEnabled) { 485 removePreviousProgramsAndUpdateCurrentProgramInCache(channelId, program); 486 } 487 notifyCurrentProgramUpdate(channelId, program); 488 } 489 490 long delayedTime; 491 if (program == null) { 492 delayedTime = 493 PERIODIC_PROGRAM_UPDATE_MIN_MS 494 + (long) 495 (Math.random() 496 * (PERIODIC_PROGRAM_UPDATE_MAX_MS 497 - PERIODIC_PROGRAM_UPDATE_MIN_MS)); 498 } else { 499 delayedTime = program.getEndTimeUtcMillis() - mClock.currentTimeMillis(); 500 } 501 mHandler.sendMessageDelayed( 502 mHandler.obtainMessage(MSG_UPDATE_ONE_CURRENT_PROGRAM, channelId), delayedTime); 503 } 504 removePreviousProgramsAndUpdateCurrentProgramInCache( long channelId, Program currentProgram)505 private void removePreviousProgramsAndUpdateCurrentProgramInCache( 506 long channelId, Program currentProgram) { 507 SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); 508 if (!Program.isProgramValid(currentProgram)) { 509 return; 510 } 511 ArrayList<Program> cachedPrograms = mChannelIdProgramCache.remove(channelId); 512 if (cachedPrograms == null) { 513 return; 514 } 515 ListIterator<Program> i = cachedPrograms.listIterator(); 516 while (i.hasNext()) { 517 Program cachedProgram = i.next(); 518 if (cachedProgram.getEndTimeUtcMillis() <= mPrefetchTimeRangeStartMs) { 519 // Remove previous programs which will not be shown in program guide. 520 i.remove(); 521 continue; 522 } 523 524 if (cachedProgram.getEndTimeUtcMillis() <= currentProgram.getStartTimeUtcMillis()) { 525 // Keep the programs that ends earlier than current program 526 // but later than mPrefetchTimeRangeStartMs. 527 continue; 528 } 529 530 // Update dummy program around current program if any. 531 if (cachedProgram.getStartTimeUtcMillis() < currentProgram.getStartTimeUtcMillis()) { 532 // The dummy program starts earlier than the current program. Adjust its end time. 533 i.set( 534 createDummyProgram( 535 cachedProgram.getStartTimeUtcMillis(), 536 currentProgram.getStartTimeUtcMillis())); 537 i.add(currentProgram); 538 } else { 539 i.set(currentProgram); 540 } 541 if (currentProgram.getEndTimeUtcMillis() < cachedProgram.getEndTimeUtcMillis()) { 542 // The dummy program ends later than the current program. Adjust its start time. 543 i.add( 544 createDummyProgram( 545 currentProgram.getEndTimeUtcMillis(), 546 cachedProgram.getEndTimeUtcMillis())); 547 } 548 break; 549 } 550 if (cachedPrograms.isEmpty()) { 551 // If all the cached programs finish before mPrefetchTimeRangeStartMs, the 552 // currentProgram would not have a chance to be inserted to the cache. 553 cachedPrograms.add(currentProgram); 554 } 555 mChannelIdProgramCache.put(channelId, cachedPrograms); 556 } 557 handleUpdateCurrentPrograms()558 private void handleUpdateCurrentPrograms() { 559 if (mProgramsUpdateTask != null) { 560 mHandler.sendEmptyMessageDelayed( 561 MSG_UPDATE_CURRENT_PROGRAMS, CURRENT_PROGRAM_UPDATE_WAIT_MS); 562 return; 563 } 564 clearTask(mProgramUpdateTaskMap); 565 mHandler.removeMessages(MSG_UPDATE_ONE_CURRENT_PROGRAM); 566 mProgramsUpdateTask = new ProgramsUpdateTask(mClock.currentTimeMillis()); 567 mProgramsUpdateTask.executeOnDbThread(); 568 } 569 570 private class ProgramsPrefetchTask 571 extends AsyncDbTask<Void, Void, Map<Long, ArrayList<Program>>> { 572 private final long mStartTimeMs; 573 private final long mEndTimeMs; 574 575 private boolean mSuccess; 576 private TimerEvent mFromEmptyCacheTimeEvent; 577 ProgramsPrefetchTask()578 public ProgramsPrefetchTask() { 579 super(mDbExecutor); 580 long time = mClock.currentTimeMillis(); 581 mStartTimeMs = 582 Utils.floorTime(time - PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS); 583 mEndTimeMs = mStartTimeMs + TimeUnit.HOURS.toMillis(getFetchDuration()); 584 mSuccess = false; 585 } 586 587 @Override onPreExecute()588 protected void onPreExecute() { 589 if (mChannelIdCurrentProgramMap.isEmpty()) { 590 // No current program guide is shown. 591 // Measure the delay before users can see program guides. 592 mFromEmptyCacheTimeEvent = mPerformanceMonitor.startTimer(); 593 } 594 } 595 596 @Override doInBackground(Void... params)597 protected Map<Long, ArrayList<Program>> doInBackground(Void... params) { 598 TimerEvent asyncTimeEvent = mPerformanceMonitor.startTimer(); 599 Map<Long, ArrayList<Program>> programMap = new HashMap<>(); 600 if (DEBUG) { 601 Log.d( 602 TAG, 603 "Starts programs prefetch. " 604 + Utils.toTimeString(mStartTimeMs) 605 + "-" 606 + Utils.toTimeString(mEndTimeMs)); 607 } 608 Uri uri = 609 Programs.CONTENT_URI 610 .buildUpon() 611 .appendQueryParameter(PARAM_START_TIME, String.valueOf(mStartTimeMs)) 612 .appendQueryParameter(PARAM_END_TIME, String.valueOf(mEndTimeMs)) 613 .build(); 614 final int RETRY_COUNT = 3; 615 Program lastReadProgram = null; 616 for (int retryCount = RETRY_COUNT; retryCount > 0; retryCount--) { 617 if (isProgramUpdatePaused()) { 618 return null; 619 } 620 programMap.clear(); 621 622 String[] projection = ProgramImpl.PARTIAL_PROJECTION; 623 if (TvProviderUtils.checkSeriesIdColumn(mContext, Programs.CONTENT_URI)) { 624 if (Utils.isProgramsUri(uri)) { 625 projection = 626 TvProviderUtils.addExtraColumnsToProjection( 627 projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID); 628 } 629 } 630 try (Cursor c = mContentResolver.query(uri, projection, null, null, SORT_BY_TIME)) { 631 if (c == null) { 632 continue; 633 } 634 while (c.moveToNext()) { 635 int duplicateCount = 0; 636 if (isCancelled()) { 637 if (DEBUG) { 638 Log.d(TAG, "ProgramsPrefetchTask canceled."); 639 } 640 return null; 641 } 642 Program program = ProgramImpl.fromCursorPartialProjection(c); 643 if (Program.isDuplicate(program, lastReadProgram)) { 644 duplicateCount++; 645 continue; 646 } else { 647 lastReadProgram = program; 648 } 649 ArrayList<Program> programs = programMap.get(program.getChannelId()); 650 if (programs == null) { 651 programs = new ArrayList<>(); 652 // To skip already loaded complete data. 653 Program currentProgramInfo = 654 mChannelIdCurrentProgramMap.get(program.getChannelId()); 655 if (currentProgramInfo != null 656 && Program.isDuplicate(program, currentProgramInfo)) { 657 program = currentProgramInfo; 658 } 659 660 programMap.put(program.getChannelId(), programs); 661 } 662 programs.add(program); 663 if (duplicateCount > 0) { 664 Log.w(TAG, "Found " + duplicateCount + " duplicate programs"); 665 } 666 } 667 mSuccess = true; 668 break; 669 } catch (IllegalStateException e) { 670 if (DEBUG) { 671 Log.d(TAG, "Database is changed while querying. Will retry."); 672 } 673 } catch (SecurityException e) { 674 Log.w(TAG, "Security exception during program data query", e); 675 } catch (Exception e) { 676 Log.w(TAG, "Error during program data query", e); 677 } 678 } 679 if (DEBUG) { 680 Log.d(TAG, "Ends programs prefetch for " + programMap.size() + " channels"); 681 } 682 mPerformanceMonitor.stopTimer( 683 asyncTimeEvent, 684 EventNames.PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND); 685 return programMap; 686 } 687 688 @Override onPostExecute(Map<Long, ArrayList<Program>> programs)689 protected void onPostExecute(Map<Long, ArrayList<Program>> programs) { 690 mProgramsPrefetchTask = null; 691 if (isProgramUpdatePaused()) { 692 // ProgramsPrefetchTask will run again once setPauseProgramUpdate(false) is called. 693 return; 694 } 695 long nextMessageDelayedTime; 696 if (mSuccess) { 697 long currentTime = mClock.currentTimeMillis(); 698 mLastPrefetchTaskRunMs = currentTime; 699 nextMessageDelayedTime = 700 Utils.floorTime( 701 mLastPrefetchTaskRunMs + PROGRAM_GUIDE_SNAP_TIME_MS, 702 PROGRAM_GUIDE_SNAP_TIME_MS) 703 - currentTime; 704 mChannelIdProgramCache = programs; 705 // Since cache has partial data we need to reset the map of complete data. 706 clearChannelInfoMap(); 707 // Get complete projection of tuned channel. 708 prefetchChannel(mTunedChannelId); 709 710 notifyProgramUpdated(); 711 if (mFromEmptyCacheTimeEvent != null) { 712 mPerformanceMonitor.stopTimer( 713 mFromEmptyCacheTimeEvent, 714 EventNames.PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE); 715 mFromEmptyCacheTimeEvent = null; 716 } 717 } else { 718 nextMessageDelayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS; 719 } 720 if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) { 721 mHandler.sendEmptyMessageDelayed( 722 MSG_UPDATE_PREFETCH_PROGRAM, nextMessageDelayedTime); 723 } 724 } 725 } 726 clearChannelInfoMap()727 private void clearChannelInfoMap() { 728 mCompleteInfoChannelIds.clear(); 729 mMaxFetchHoursMs = FETCH_HOURS_MS; 730 } 731 getFetchDuration()732 private long getFetchDuration() { 733 if (mChannelIdProgramCache.isEmpty()) { 734 return Math.max(1L, mBackendKnobsFlags.programGuideInitialFetchHours()); 735 } else { 736 long durationHours; 737 int channelCount = mChannelDataManager.getChannelCount(); 738 long knobsMaxHours = mBackendKnobsFlags.programGuideMaxHours(); 739 long targetChannelCount = mBackendKnobsFlags.epgTargetChannelCount(); 740 if (channelCount <= targetChannelCount) { 741 durationHours = Math.max(48L, knobsMaxHours); 742 } else { 743 // 2 days <= duration <= 14 days (336 hours) 744 durationHours = knobsMaxHours * targetChannelCount / channelCount; 745 if (durationHours < 48L) { 746 durationHours = 48L; 747 } else if (durationHours > 336L) { 748 durationHours = 336L; 749 } 750 } 751 return durationHours; 752 } 753 } 754 755 private class SingleChannelPrefetchTask extends AsyncDbTask.AsyncQueryTask<ArrayList<Program>> { 756 long mChannelId; 757 SingleChannelPrefetchTask(long channelId, long startTimeMs, long endTimeMs)758 public SingleChannelPrefetchTask(long channelId, long startTimeMs, long endTimeMs) { 759 super( 760 mDbExecutor, 761 mContext, 762 TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs), 763 ProgramImpl.PROJECTION, 764 null, 765 null, 766 SORT_BY_TIME); 767 mChannelId = channelId; 768 } 769 770 @Override onQuery(Cursor c)771 protected ArrayList<Program> onQuery(Cursor c) { 772 ArrayList<Program> programMap = new ArrayList<>(); 773 while (c.moveToNext()) { 774 Program program = ProgramImpl.fromCursor(c); 775 programMap.add(program); 776 } 777 return programMap; 778 } 779 780 @Override onPostExecute(ArrayList<Program> programs)781 protected void onPostExecute(ArrayList<Program> programs) { 782 mChannelIdProgramCache.put(mChannelId, programs); 783 notifyChannelUpdated(); 784 } 785 } 786 notifyProgramUpdated()787 private void notifyProgramUpdated() { 788 for (Callback callback : mCallbacks) { 789 callback.onProgramUpdated(); 790 } 791 } 792 notifyChannelUpdated()793 private void notifyChannelUpdated() { 794 for (Callback callback : mCallbacks) { 795 callback.onChannelUpdated(); 796 } 797 } 798 799 private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> { ProgramsUpdateTask(long time)800 public ProgramsUpdateTask(long time) { 801 super( 802 mDbExecutor, 803 mContext, 804 Programs.CONTENT_URI 805 .buildUpon() 806 .appendQueryParameter(PARAM_START_TIME, String.valueOf(time)) 807 .appendQueryParameter(PARAM_END_TIME, String.valueOf(time)) 808 .build(), 809 ProgramImpl.PROJECTION, 810 null, 811 null, 812 SORT_BY_CHANNEL_ID); 813 } 814 815 @Override onQuery(Cursor c)816 public List<Program> onQuery(Cursor c) { 817 final List<Program> programs = new ArrayList<>(); 818 if (c != null) { 819 int duplicateCount = 0; 820 Program lastReadProgram = null; 821 while (c.moveToNext()) { 822 if (isCancelled()) { 823 return programs; 824 } 825 Program program = ProgramImpl.fromCursor(c); 826 // Only one program is expected per channel for this query 827 // However, skip overlapping programs from same channel 828 if (Program.sameChannel(program, lastReadProgram) 829 && Program.isOverlapping(program, lastReadProgram)) { 830 duplicateCount++; 831 continue; 832 } else { 833 lastReadProgram = program; 834 } 835 836 programs.add(program); 837 } 838 if (duplicateCount > 0) { 839 Log.w(TAG, "Found " + duplicateCount + " overlapping programs"); 840 } 841 } 842 return programs; 843 } 844 845 @Override onPostExecute(List<Program> programs)846 protected void onPostExecute(List<Program> programs) { 847 if (DEBUG) Log.d(TAG, "ProgramsUpdateTask done"); 848 mProgramsUpdateTask = null; 849 if (programs != null) { 850 Set<Long> removedChannelIds = new HashSet<>(mChannelIdCurrentProgramMap.keySet()); 851 for (Program program : programs) { 852 long channelId = program.getChannelId(); 853 updateCurrentProgram(channelId, program); 854 removedChannelIds.remove(channelId); 855 } 856 for (Long channelId : removedChannelIds) { 857 if (mPrefetchEnabled) { 858 mChannelIdProgramCache.remove(channelId); 859 mCompleteInfoChannelIds.remove(channelId); 860 } 861 mChannelIdCurrentProgramMap.remove(channelId); 862 notifyCurrentProgramUpdate(channelId, null); 863 } 864 } 865 mCurrentProgramsLoadFinished = true; 866 } 867 } 868 869 private class UpdateCurrentProgramForChannelTask extends AsyncDbTask.AsyncQueryTask<Program> { 870 private final long mChannelId; 871 UpdateCurrentProgramForChannelTask(long channelId, long time)872 private UpdateCurrentProgramForChannelTask(long channelId, long time) { 873 super( 874 mDbExecutor, 875 mContext, 876 TvContract.buildProgramsUriForChannel(channelId, time, time), 877 ProgramImpl.PROJECTION, 878 null, 879 null, 880 SORT_BY_TIME); 881 mChannelId = channelId; 882 } 883 884 @Override onQuery(Cursor c)885 public Program onQuery(Cursor c) { 886 Program program = null; 887 if (c != null && c.moveToNext()) { 888 program = ProgramImpl.fromCursor(c); 889 } 890 return program; 891 } 892 893 @Override onPostExecute(Program program)894 protected void onPostExecute(Program program) { 895 mProgramUpdateTaskMap.remove(mChannelId); 896 updateCurrentProgram(mChannelId, program); 897 } 898 } 899 900 private class MyHandler extends Handler { MyHandler(Looper looper)901 public MyHandler(Looper looper) { 902 super(looper); 903 } 904 905 @Override handleMessage(Message msg)906 public void handleMessage(Message msg) { 907 switch (msg.what) { 908 case MSG_UPDATE_CURRENT_PROGRAMS: 909 handleUpdateCurrentPrograms(); 910 break; 911 case MSG_UPDATE_ONE_CURRENT_PROGRAM: 912 { 913 long channelId = (Long) msg.obj; 914 UpdateCurrentProgramForChannelTask oldTask = 915 mProgramUpdateTaskMap.get(channelId); 916 if (oldTask != null) { 917 oldTask.cancel(true); 918 } 919 UpdateCurrentProgramForChannelTask task = 920 new UpdateCurrentProgramForChannelTask( 921 channelId, mClock.currentTimeMillis()); 922 mProgramUpdateTaskMap.put(channelId, task); 923 task.executeOnDbThread(); 924 break; 925 } 926 case MSG_UPDATE_PREFETCH_PROGRAM: 927 { 928 if (isProgramUpdatePaused()) { 929 return; 930 } 931 if (mProgramsPrefetchTask != null) { 932 mHandler.sendEmptyMessageDelayed( 933 msg.what, mProgramPrefetchUpdateWaitMs); 934 return; 935 } 936 long delayMillis = 937 mLastPrefetchTaskRunMs 938 + mProgramPrefetchUpdateWaitMs 939 - mClock.currentTimeMillis(); 940 if (delayMillis > 0) { 941 mHandler.sendEmptyMessageDelayed( 942 MSG_UPDATE_PREFETCH_PROGRAM, delayMillis); 943 } else { 944 mProgramsPrefetchTask = new ProgramsPrefetchTask(); 945 mProgramsPrefetchTask.executeOnDbThread(); 946 } 947 break; 948 } 949 case MSG_UPDATE_CONTENT_RATINGS: 950 mTvInputManagerHelper.getContentRatingsManager().update(); 951 break; 952 default: 953 // Do nothing 954 } 955 } 956 } 957 958 /** 959 * Pause program update. Updating program data will result in UI refresh, but UI is fragile to 960 * handle it so we'd better disable it for a while. 961 * 962 * <p>Prefetch should be enabled to call it. 963 */ setPauseProgramUpdate(boolean pauseProgramUpdate)964 public void setPauseProgramUpdate(boolean pauseProgramUpdate) { 965 SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); 966 if (mPauseProgramUpdate && !pauseProgramUpdate) { 967 if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) { 968 // MSG_UPDATE_PRFETCH_PROGRAM can be empty 969 // if prefetch task is launched while program update is paused. 970 // Update immediately in that case. 971 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); 972 } 973 } 974 mPauseProgramUpdate = pauseProgramUpdate; 975 } 976 isProgramUpdatePaused()977 private boolean isProgramUpdatePaused() { 978 // Although pause is requested, we need to keep updating if cache is empty. 979 return mPauseProgramUpdate && !mChannelIdProgramCache.isEmpty(); 980 } 981 982 /** 983 * Sets program data prefetch time range. Any program data that ends before the start time will 984 * be removed from the cache later. Note that there's no limit for end time. 985 * 986 * <p>Prefetch should be enabled to call it. 987 */ setPrefetchTimeRange(long startTimeMs)988 public void setPrefetchTimeRange(long startTimeMs) { 989 SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); 990 if (mPrefetchTimeRangeStartMs > startTimeMs) { 991 // Fetch the programs immediately to re-create the cache. 992 if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) { 993 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); 994 } 995 } 996 mPrefetchTimeRangeStartMs = startTimeMs; 997 } 998 clearTask(LongSparseArray<UpdateCurrentProgramForChannelTask> tasks)999 private void clearTask(LongSparseArray<UpdateCurrentProgramForChannelTask> tasks) { 1000 for (int i = 0; i < tasks.size(); i++) { 1001 tasks.valueAt(i).cancel(true); 1002 } 1003 tasks.clear(); 1004 } 1005 cancelPrefetchTask()1006 private void cancelPrefetchTask() { 1007 if (mProgramsPrefetchTask != null) { 1008 mProgramsPrefetchTask.cancel(true); 1009 mProgramsPrefetchTask = null; 1010 } 1011 } 1012 1013 // Create dummy program which indicates data isn't loaded yet so DB query is required. createDummyProgram(long startTimeMs, long endTimeMs)1014 private Program createDummyProgram(long startTimeMs, long endTimeMs) { 1015 return new ProgramImpl.Builder() 1016 .setChannelId(Channel.INVALID_ID) 1017 .setStartTimeUtcMillis(startTimeMs) 1018 .setEndTimeUtcMillis(endTimeMs) 1019 .build(); 1020 } 1021 1022 @Override performTrimMemory(int level)1023 public void performTrimMemory(int level) { 1024 mChannelId2ProgramUpdatedListeners.clearEmptyCache(); 1025 } 1026 } 1027