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.common.MemoryManageable; 38 import com.android.tv.common.SoftPreconditions; 39 import com.android.tv.util.AsyncDbTask; 40 import com.android.tv.util.Clock; 41 import com.android.tv.util.MultiLongSparseArray; 42 import com.android.tv.util.Utils; 43 44 import java.util.ArrayList; 45 import java.util.Collections; 46 import java.util.HashMap; 47 import java.util.HashSet; 48 import java.util.List; 49 import java.util.ListIterator; 50 import java.util.Map; 51 import java.util.Objects; 52 import java.util.Set; 53 import java.util.concurrent.ConcurrentHashMap; 54 import java.util.concurrent.TimeUnit; 55 56 @MainThread 57 public class ProgramDataManager implements MemoryManageable { 58 private static final String TAG = "ProgramDataManager"; 59 private static final boolean DEBUG = false; 60 61 // To prevent from too many program update operations at the same time, we give random interval 62 // between PERIODIC_PROGRAM_UPDATE_MIN_MS and PERIODIC_PROGRAM_UPDATE_MAX_MS. 63 private static final long PERIODIC_PROGRAM_UPDATE_MIN_MS = TimeUnit.MINUTES.toMillis(5); 64 private static final long PERIODIC_PROGRAM_UPDATE_MAX_MS = TimeUnit.MINUTES.toMillis(10); 65 private static final long PROGRAM_PREFETCH_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5); 66 // TODO: need to optimize consecutive DB updates. 67 private static final long CURRENT_PROGRAM_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5); 68 @VisibleForTesting 69 static final long PROGRAM_GUIDE_SNAP_TIME_MS = TimeUnit.MINUTES.toMillis(30); 70 @VisibleForTesting 71 static final long PROGRAM_GUIDE_MAX_TIME_RANGE = TimeUnit.DAYS.toMillis(2); 72 73 // TODO: Use TvContract constants, once they become public. 74 private static final String PARAM_START_TIME = "start_time"; 75 private static final String PARAM_END_TIME = "end_time"; 76 // COLUMN_CHANNEL_ID, COLUMN_END_TIME_UTC_MILLIS are added to detect duplicated programs. 77 // Duplicated programs are always consecutive by the sorting order. 78 private static final String SORT_BY_TIME = Programs.COLUMN_START_TIME_UTC_MILLIS + ", " 79 + Programs.COLUMN_CHANNEL_ID + ", " + Programs.COLUMN_END_TIME_UTC_MILLIS; 80 81 private static final int MSG_UPDATE_CURRENT_PROGRAMS = 1000; 82 private static final int MSG_UPDATE_ONE_CURRENT_PROGRAM = 1001; 83 private static final int MSG_UPDATE_PREFETCH_PROGRAM = 1002; 84 85 private final Clock mClock; 86 private final ContentResolver mContentResolver; 87 private boolean mStarted; 88 // Updated only on the main thread. 89 private volatile boolean mCurrentProgramsLoadFinished; 90 private ProgramsUpdateTask mProgramsUpdateTask; 91 private final LongSparseArray<UpdateCurrentProgramForChannelTask> mProgramUpdateTaskMap = 92 new LongSparseArray<>(); 93 private final Map<Long, Program> mChannelIdCurrentProgramMap = new ConcurrentHashMap<>(); 94 private final MultiLongSparseArray<OnCurrentProgramUpdatedListener> 95 mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>(); 96 private final Handler mHandler; 97 private final Set<Listener> mListeners = new ArraySet<>(); 98 99 private final ContentObserver mProgramObserver; 100 101 private boolean mPrefetchEnabled; 102 private long mProgramPrefetchUpdateWaitMs; 103 private long mLastPrefetchTaskRunMs; 104 private ProgramsPrefetchTask mProgramsPrefetchTask; 105 private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new HashMap<>(); 106 107 // Any program that ends prior to this time will be removed from the cache 108 // when a channel's current program is updated. 109 // Note that there's no limit for end time. 110 private long mPrefetchTimeRangeStartMs; 111 112 private boolean mPauseProgramUpdate = false; 113 private final LruCache<Long, Program> mZeroLengthProgramCache = new LruCache<>(10); 114 115 @MainThread ProgramDataManager(Context context)116 public ProgramDataManager(Context context) { 117 this(context.getContentResolver(), Clock.SYSTEM, Looper.myLooper()); 118 } 119 120 @VisibleForTesting ProgramDataManager(ContentResolver contentResolver, Clock time, Looper looper)121 ProgramDataManager(ContentResolver contentResolver, Clock time, Looper looper) { 122 mClock = time; 123 mContentResolver = contentResolver; 124 mHandler = new MyHandler(looper); 125 mProgramObserver = new ContentObserver(mHandler) { 126 @Override 127 public void onChange(boolean selfChange) { 128 if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) { 129 mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS); 130 } 131 if (isProgramUpdatePaused()) { 132 return; 133 } 134 if (mPrefetchEnabled) { 135 // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be quite long 136 // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message 137 // and send MSG_UPDATE_PREFETCH_PROGRAM again. 138 mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM); 139 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); 140 } 141 } 142 }; 143 mProgramPrefetchUpdateWaitMs = PROGRAM_PREFETCH_UPDATE_WAIT_MS; 144 } 145 146 @VisibleForTesting getContentObserver()147 ContentObserver getContentObserver() { 148 return mProgramObserver; 149 } 150 151 /** 152 * Set the program prefetch update wait which gives the delay to query all programs from DB 153 * to prevent from too frequent DB queries. 154 * Default value is {@link #PROGRAM_PREFETCH_UPDATE_WAIT_MS} 155 */ 156 @VisibleForTesting setProgramPrefetchUpdateWait(long programPrefetchUpdateWaitMs)157 void setProgramPrefetchUpdateWait(long programPrefetchUpdateWaitMs) { 158 mProgramPrefetchUpdateWaitMs = programPrefetchUpdateWaitMs; 159 } 160 161 /** 162 * Starts the manager. 163 */ start()164 public void start() { 165 if (mStarted) { 166 return; 167 } 168 mStarted = true; 169 // Should be called directly instead of posting MSG_UPDATE_CURRENT_PROGRAMS message 170 // to the handler. If not, another DB task can be executed before loading current programs. 171 handleUpdateCurrentPrograms(); 172 if (mPrefetchEnabled) { 173 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); 174 } 175 mContentResolver.registerContentObserver(Programs.CONTENT_URI, 176 true, mProgramObserver); 177 } 178 179 /** 180 * Stops the manager. It clears manager states and runs pending DB operations. Added listeners 181 * aren't automatically removed by this method. 182 */ 183 @VisibleForTesting stop()184 public void stop() { 185 if (!mStarted) { 186 return; 187 } 188 mStarted = false; 189 mContentResolver.unregisterContentObserver(mProgramObserver); 190 mHandler.removeCallbacksAndMessages(null); 191 192 clearTask(mProgramUpdateTaskMap); 193 cancelPrefetchTask(); 194 if (mProgramsUpdateTask != null) { 195 mProgramsUpdateTask.cancel(true); 196 mProgramsUpdateTask = null; 197 } 198 } 199 200 @AnyThread isCurrentProgramsLoadFinished()201 public boolean isCurrentProgramsLoadFinished() { 202 return mCurrentProgramsLoadFinished; 203 } 204 205 /** Returns the current program at the specified channel. */ 206 @AnyThread getCurrentProgram(long channelId)207 public Program getCurrentProgram(long channelId) { 208 return mChannelIdCurrentProgramMap.get(channelId); 209 } 210 211 /** Returns all the current programs. */ 212 @AnyThread getCurrentPrograms()213 public List<Program> getCurrentPrograms() { 214 return new ArrayList<>(mChannelIdCurrentProgramMap.values()); 215 } 216 217 /** 218 * Reloads program data. 219 */ reload()220 public void reload() { 221 if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) { 222 mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS); 223 } 224 if (mPrefetchEnabled && !mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) { 225 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); 226 } 227 } 228 229 /** 230 * A listener interface to receive notification on program data retrieval from DB. 231 */ 232 public interface Listener { 233 /** 234 * Called when a Program data is now available through getProgram() 235 * after the DB operation is done which wasn't before. 236 * This would be called only if fetched data is around the selected program. 237 **/ onProgramUpdated()238 void onProgramUpdated(); 239 } 240 241 /** 242 * Adds the {@link Listener}. 243 */ addListener(Listener listener)244 public void addListener(Listener listener) { 245 mListeners.add(listener); 246 } 247 248 /** 249 * Removes the {@link Listener}. 250 */ removeListener(Listener listener)251 public void removeListener(Listener listener) { 252 mListeners.remove(listener); 253 } 254 255 /** 256 * Enables or Disables program prefetch. 257 */ setPrefetchEnabled(boolean enable)258 public void setPrefetchEnabled(boolean enable) { 259 if (mPrefetchEnabled == enable) { 260 return; 261 } 262 if (enable) { 263 mPrefetchEnabled = true; 264 mLastPrefetchTaskRunMs = 0; 265 if (mStarted) { 266 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); 267 } 268 } else { 269 mPrefetchEnabled = false; 270 cancelPrefetchTask(); 271 mChannelIdProgramCache.clear(); 272 mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM); 273 } 274 } 275 276 /** 277 * Returns the programs for the given channel which ends after the given start time. 278 * 279 * <p> Prefetch should be enabled to call it. 280 * 281 * @return {@link List} with Programs. It may includes dummy program if the entry needs DB 282 * operations to get. 283 */ getPrograms(long channelId, long startTime)284 public List<Program> getPrograms(long channelId, long startTime) { 285 SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); 286 ArrayList<Program> cachedPrograms = mChannelIdProgramCache.get(channelId); 287 if (cachedPrograms == null) { 288 return Collections.emptyList(); 289 } 290 int startIndex = getProgramIndexAt(cachedPrograms, startTime); 291 return Collections.unmodifiableList( 292 cachedPrograms.subList(startIndex, cachedPrograms.size())); 293 } 294 295 // Returns the index of program that is played at the specified time. 296 // If there isn't, return the first program among programs that starts after the given time 297 // if returnNextProgram is {@code true}. getProgramIndexAt(List<Program> programs, long time)298 private int getProgramIndexAt(List<Program> programs, long time) { 299 Program key = mZeroLengthProgramCache.get(time); 300 if (key == null) { 301 key = createDummyProgram(time, time); 302 mZeroLengthProgramCache.put(time, key); 303 } 304 int index = Collections.binarySearch(programs, key); 305 if (index < 0) { 306 index = -(index + 1); // change it to index to be added. 307 if (index > 0 && isProgramPlayedAt(programs.get(index - 1), time)) { 308 // A program is played at that time. 309 return index - 1; 310 } 311 return index; 312 } 313 return index; 314 } 315 isProgramPlayedAt(Program program, long time)316 private boolean isProgramPlayedAt(Program program, long time) { 317 return program.getStartTimeUtcMillis() <= time && time <= program.getEndTimeUtcMillis(); 318 } 319 320 /** 321 * Adds the listener to be notified if current program is updated for a channel. 322 * 323 * @param channelId A channel ID to get notified. If it's {@link Channel#INVALID_ID}, the 324 * listener would be called whenever a current program is updated. 325 */ addOnCurrentProgramUpdatedListener( long channelId, OnCurrentProgramUpdatedListener listener)326 public void addOnCurrentProgramUpdatedListener( 327 long channelId, OnCurrentProgramUpdatedListener listener) { 328 mChannelId2ProgramUpdatedListeners 329 .put(channelId, listener); 330 } 331 332 /** 333 * Removes the listener previously added by 334 * {@link #addOnCurrentProgramUpdatedListener(long, OnCurrentProgramUpdatedListener)}. 335 */ removeOnCurrentProgramUpdatedListener( long channelId, OnCurrentProgramUpdatedListener listener)336 public void removeOnCurrentProgramUpdatedListener( 337 long channelId, OnCurrentProgramUpdatedListener listener) { 338 mChannelId2ProgramUpdatedListeners 339 .remove(channelId, listener); 340 } 341 notifyCurrentProgramUpdate(long channelId, Program program)342 private void notifyCurrentProgramUpdate(long channelId, Program program) { 343 for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners 344 .get(channelId)) { 345 listener.onCurrentProgramUpdated(channelId, program); 346 } 347 for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners 348 .get(Channel.INVALID_ID)) { 349 listener.onCurrentProgramUpdated(channelId, program); 350 } 351 } 352 updateCurrentProgram(long channelId, Program program)353 private void updateCurrentProgram(long channelId, Program program) { 354 Program previousProgram = program == null ? mChannelIdCurrentProgramMap.remove(channelId) 355 : mChannelIdCurrentProgramMap.put(channelId, program); 356 if (!Objects.equals(program, previousProgram)) { 357 if (mPrefetchEnabled) { 358 removePreviousProgramsAndUpdateCurrentProgramInCache(channelId, program); 359 } 360 notifyCurrentProgramUpdate(channelId, program); 361 } 362 363 long delayedTime; 364 if (program == null) { 365 delayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS 366 + (long) (Math.random() * (PERIODIC_PROGRAM_UPDATE_MAX_MS 367 - PERIODIC_PROGRAM_UPDATE_MIN_MS)); 368 } else { 369 delayedTime = program.getEndTimeUtcMillis() - mClock.currentTimeMillis(); 370 } 371 mHandler.sendMessageDelayed(mHandler.obtainMessage( 372 MSG_UPDATE_ONE_CURRENT_PROGRAM, channelId), delayedTime); 373 } 374 removePreviousProgramsAndUpdateCurrentProgramInCache( long channelId, Program currentProgram)375 private void removePreviousProgramsAndUpdateCurrentProgramInCache( 376 long channelId, Program currentProgram) { 377 SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); 378 if (!Program.isValid(currentProgram)) { 379 return; 380 } 381 ArrayList<Program> cachedPrograms = mChannelIdProgramCache.remove(channelId); 382 if (cachedPrograms == null) { 383 return; 384 } 385 ListIterator<Program> i = cachedPrograms.listIterator(); 386 while (i.hasNext()) { 387 Program cachedProgram = i.next(); 388 if (cachedProgram.getEndTimeUtcMillis() <= mPrefetchTimeRangeStartMs) { 389 // Remove previous programs which will not be shown in program guide. 390 i.remove(); 391 continue; 392 } 393 394 if (cachedProgram.getEndTimeUtcMillis() <= currentProgram 395 .getStartTimeUtcMillis()) { 396 // Keep the programs that ends earlier than current program 397 // but later than mPrefetchTimeRangeStartMs. 398 continue; 399 } 400 401 // Update dummy program around current program if any. 402 if (cachedProgram.getStartTimeUtcMillis() < currentProgram 403 .getStartTimeUtcMillis()) { 404 // The dummy program starts earlier than the current program. Adjust its end time. 405 i.set(createDummyProgram(cachedProgram.getStartTimeUtcMillis(), 406 currentProgram.getStartTimeUtcMillis())); 407 i.add(currentProgram); 408 } else { 409 i.set(currentProgram); 410 } 411 if (currentProgram.getEndTimeUtcMillis() < cachedProgram.getEndTimeUtcMillis()) { 412 // The dummy program ends later than the current program. Adjust its start time. 413 i.add(createDummyProgram(currentProgram.getEndTimeUtcMillis(), 414 cachedProgram.getEndTimeUtcMillis())); 415 } 416 break; 417 } 418 if (cachedPrograms.isEmpty()) { 419 // If all the cached programs finish before mPrefetchTimeRangeStartMs, the 420 // currentProgram would not have a chance to be inserted to the cache. 421 cachedPrograms.add(currentProgram); 422 } 423 mChannelIdProgramCache.put(channelId, cachedPrograms); 424 } 425 handleUpdateCurrentPrograms()426 private void handleUpdateCurrentPrograms() { 427 if (mProgramsUpdateTask != null) { 428 mHandler.sendEmptyMessageDelayed(MSG_UPDATE_CURRENT_PROGRAMS, 429 CURRENT_PROGRAM_UPDATE_WAIT_MS); 430 return; 431 } 432 clearTask(mProgramUpdateTaskMap); 433 mHandler.removeMessages(MSG_UPDATE_ONE_CURRENT_PROGRAM); 434 mProgramsUpdateTask = new ProgramsUpdateTask(mContentResolver, mClock.currentTimeMillis()); 435 mProgramsUpdateTask.executeOnDbThread(); 436 } 437 438 private class ProgramsPrefetchTask 439 extends AsyncDbTask<Void, Void, Map<Long, ArrayList<Program>>> { 440 private final long mStartTimeMs; 441 private final long mEndTimeMs; 442 443 private boolean mSuccess; 444 ProgramsPrefetchTask()445 public ProgramsPrefetchTask() { 446 long time = mClock.currentTimeMillis(); 447 mStartTimeMs = Utils 448 .floorTime(time - PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS); 449 mEndTimeMs = mStartTimeMs + PROGRAM_GUIDE_MAX_TIME_RANGE; 450 mSuccess = false; 451 } 452 453 @Override doInBackground(Void... params)454 protected Map<Long, ArrayList<Program>> doInBackground(Void... params) { 455 Map<Long, ArrayList<Program>> programMap = new HashMap<>(); 456 if (DEBUG) { 457 Log.d(TAG, "Starts programs prefetch. " + Utils.toTimeString(mStartTimeMs) + "-" 458 + Utils.toTimeString(mEndTimeMs)); 459 } 460 Uri uri = Programs.CONTENT_URI.buildUpon() 461 .appendQueryParameter(PARAM_START_TIME, String.valueOf(mStartTimeMs)) 462 .appendQueryParameter(PARAM_END_TIME, String.valueOf(mEndTimeMs)).build(); 463 final int RETRY_COUNT = 3; 464 Program lastReadProgram = null; 465 for (int retryCount = RETRY_COUNT; retryCount > 0; retryCount--) { 466 if (isProgramUpdatePaused()) { 467 return null; 468 } 469 programMap.clear(); 470 try (Cursor c = mContentResolver.query(uri, Program.PROJECTION, null, null, 471 SORT_BY_TIME)) { 472 if (c == null) { 473 continue; 474 } 475 while (c.moveToNext()) { 476 int duplicateCount = 0; 477 if (isCancelled()) { 478 if (DEBUG) { 479 Log.d(TAG, "ProgramsPrefetchTask canceled."); 480 } 481 return null; 482 } 483 Program program = Program.fromCursor(c); 484 if (Program.isDuplicate(program, lastReadProgram)) { 485 duplicateCount++; 486 continue; 487 } else { 488 lastReadProgram = program; 489 } 490 ArrayList<Program> programs = programMap.get(program.getChannelId()); 491 if (programs == null) { 492 programs = new ArrayList<>(); 493 programMap.put(program.getChannelId(), programs); 494 } 495 programs.add(program); 496 if (duplicateCount > 0) { 497 Log.w(TAG, "Found " + duplicateCount + " duplicate programs"); 498 } 499 } 500 mSuccess = true; 501 break; 502 } catch (IllegalStateException e) { 503 if (DEBUG) { 504 Log.d(TAG, "Database is changed while querying. Will retry."); 505 } 506 } catch (SecurityException e) { 507 Log.d(TAG, "Security exception during program data query", e); 508 } 509 } 510 if (DEBUG) { 511 Log.d(TAG, "Ends programs prefetch for " + programMap.size() + " channels"); 512 } 513 return programMap; 514 } 515 516 @Override onPostExecute(Map<Long, ArrayList<Program>> programs)517 protected void onPostExecute(Map<Long, ArrayList<Program>> programs) { 518 mProgramsPrefetchTask = null; 519 if (isProgramUpdatePaused()) { 520 // ProgramsPrefetchTask will run again once setPauseProgramUpdate(false) is called. 521 return; 522 } 523 long nextMessageDelayedTime; 524 if (mSuccess) { 525 mChannelIdProgramCache = programs; 526 notifyProgramUpdated(); 527 long currentTime = mClock.currentTimeMillis(); 528 mLastPrefetchTaskRunMs = currentTime; 529 nextMessageDelayedTime = 530 Utils.floorTime(mLastPrefetchTaskRunMs + PROGRAM_GUIDE_SNAP_TIME_MS, 531 PROGRAM_GUIDE_SNAP_TIME_MS) - currentTime; 532 } else { 533 nextMessageDelayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS; 534 } 535 if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) { 536 mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PREFETCH_PROGRAM, 537 nextMessageDelayedTime); 538 } 539 } 540 } 541 notifyProgramUpdated()542 private void notifyProgramUpdated() { 543 for (Listener listener : mListeners) { 544 listener.onProgramUpdated(); 545 } 546 } 547 548 private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> { ProgramsUpdateTask(ContentResolver contentResolver, long time)549 public ProgramsUpdateTask(ContentResolver contentResolver, long time) { 550 super(contentResolver, Programs.CONTENT_URI.buildUpon() 551 .appendQueryParameter(PARAM_START_TIME, String.valueOf(time)) 552 .appendQueryParameter(PARAM_END_TIME, String.valueOf(time)).build(), 553 Program.PROJECTION, null, null, SORT_BY_TIME); 554 } 555 556 @Override onQuery(Cursor c)557 public List<Program> onQuery(Cursor c) { 558 final List<Program> programs = new ArrayList<>(); 559 if (c != null) { 560 int duplicateCount = 0; 561 Program lastReadProgram = null; 562 while (c.moveToNext()) { 563 if (isCancelled()) { 564 return programs; 565 } 566 Program program = Program.fromCursor(c); 567 if (Program.isDuplicate(program, lastReadProgram)) { 568 duplicateCount++; 569 continue; 570 } else { 571 lastReadProgram = program; 572 } 573 programs.add(program); 574 } 575 if (duplicateCount > 0) { 576 Log.w(TAG, "Found " + duplicateCount + " duplicate programs"); 577 } 578 } 579 return programs; 580 } 581 582 @Override onPostExecute(List<Program> programs)583 protected void onPostExecute(List<Program> programs) { 584 if (DEBUG) Log.d(TAG, "ProgramsUpdateTask done"); 585 mProgramsUpdateTask = null; 586 if (programs != null) { 587 Set<Long> removedChannelIds = new HashSet<>(mChannelIdCurrentProgramMap.keySet()); 588 for (Program program : programs) { 589 long channelId = program.getChannelId(); 590 updateCurrentProgram(channelId, program); 591 removedChannelIds.remove(channelId); 592 } 593 for (Long channelId : removedChannelIds) { 594 if (mPrefetchEnabled) { 595 mChannelIdProgramCache.remove(channelId); 596 } 597 mChannelIdCurrentProgramMap.remove(channelId); 598 notifyCurrentProgramUpdate(channelId, null); 599 } 600 } 601 mCurrentProgramsLoadFinished = true; 602 } 603 } 604 605 private class UpdateCurrentProgramForChannelTask extends AsyncDbTask.AsyncQueryTask<Program> { 606 private final long mChannelId; UpdateCurrentProgramForChannelTask(ContentResolver contentResolver, long channelId, long time)607 private UpdateCurrentProgramForChannelTask(ContentResolver contentResolver, long channelId, 608 long time) { 609 super(contentResolver, TvContract.buildProgramsUriForChannel(channelId, time, time), 610 Program.PROJECTION, null, null, SORT_BY_TIME); 611 mChannelId = channelId; 612 } 613 614 @Override onQuery(Cursor c)615 public Program onQuery(Cursor c) { 616 Program program = null; 617 if (c != null && c.moveToNext()) { 618 program = Program.fromCursor(c); 619 } 620 return program; 621 } 622 623 @Override onPostExecute(Program program)624 protected void onPostExecute(Program program) { 625 mProgramUpdateTaskMap.remove(mChannelId); 626 updateCurrentProgram(mChannelId, program); 627 } 628 } 629 630 private class MyHandler extends Handler { MyHandler(Looper looper)631 public MyHandler(Looper looper) { 632 super(looper); 633 } 634 635 @Override handleMessage(Message msg)636 public void handleMessage(Message msg) { 637 switch (msg.what) { 638 case MSG_UPDATE_CURRENT_PROGRAMS: 639 handleUpdateCurrentPrograms(); 640 break; 641 case MSG_UPDATE_ONE_CURRENT_PROGRAM: { 642 long channelId = (Long) msg.obj; 643 UpdateCurrentProgramForChannelTask oldTask = mProgramUpdateTaskMap 644 .get(channelId); 645 if (oldTask != null) { 646 oldTask.cancel(true); 647 } 648 UpdateCurrentProgramForChannelTask 649 task = new UpdateCurrentProgramForChannelTask( 650 mContentResolver, channelId, mClock.currentTimeMillis()); 651 mProgramUpdateTaskMap.put(channelId, task); 652 task.executeOnDbThread(); 653 break; 654 } 655 case MSG_UPDATE_PREFETCH_PROGRAM: { 656 if (isProgramUpdatePaused()) { 657 return; 658 } 659 if (mProgramsPrefetchTask != null) { 660 mHandler.sendEmptyMessageDelayed(msg.what, mProgramPrefetchUpdateWaitMs); 661 return; 662 } 663 long delayMillis = mLastPrefetchTaskRunMs + mProgramPrefetchUpdateWaitMs 664 - mClock.currentTimeMillis(); 665 if (delayMillis > 0) { 666 mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PREFETCH_PROGRAM, delayMillis); 667 } else { 668 mProgramsPrefetchTask = new ProgramsPrefetchTask(); 669 mProgramsPrefetchTask.executeOnDbThread(); 670 } 671 break; 672 } 673 } 674 } 675 } 676 677 /** 678 * Pause program update. 679 * Updating program data will result in UI refresh, 680 * but UI is fragile to handle it so we'd better disable it for a while. 681 * 682 * <p> Prefetch should be enabled to call it. 683 */ setPauseProgramUpdate(boolean pauseProgramUpdate)684 public void setPauseProgramUpdate(boolean pauseProgramUpdate) { 685 SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); 686 if (mPauseProgramUpdate && !pauseProgramUpdate) { 687 if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) { 688 // MSG_UPDATE_PRFETCH_PROGRAM can be empty 689 // if prefetch task is launched while program update is paused. 690 // Update immediately in that case. 691 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); 692 } 693 } 694 mPauseProgramUpdate = pauseProgramUpdate; 695 } 696 isProgramUpdatePaused()697 private boolean isProgramUpdatePaused() { 698 // Although pause is requested, we need to keep updating if cache is empty. 699 return mPauseProgramUpdate && !mChannelIdProgramCache.isEmpty(); 700 } 701 702 /** 703 * Sets program data prefetch time range. 704 * Any program data that ends before the start time will be removed from the cache later. 705 * Note that there's no limit for end time. 706 * 707 * <p> Prefetch should be enabled to call it. 708 */ setPrefetchTimeRange(long startTimeMs)709 public void setPrefetchTimeRange(long startTimeMs) { 710 SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); 711 if (mPrefetchTimeRangeStartMs > startTimeMs) { 712 // Fetch the programs immediately to re-create the cache. 713 if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) { 714 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); 715 } 716 } 717 mPrefetchTimeRangeStartMs = startTimeMs; 718 } 719 clearTask(LongSparseArray<UpdateCurrentProgramForChannelTask> tasks)720 private void clearTask(LongSparseArray<UpdateCurrentProgramForChannelTask> tasks) { 721 for (int i = 0; i < tasks.size(); i++) { 722 tasks.valueAt(i).cancel(true); 723 } 724 tasks.clear(); 725 } 726 cancelPrefetchTask()727 private void cancelPrefetchTask() { 728 if (mProgramsPrefetchTask != null) { 729 mProgramsPrefetchTask.cancel(true); 730 mProgramsPrefetchTask = null; 731 } 732 } 733 734 // Create dummy program which indicates data isn't loaded yet so DB query is required. createDummyProgram(long startTimeMs, long endTimeMs)735 private Program createDummyProgram(long startTimeMs, long endTimeMs) { 736 return new Program.Builder() 737 .setChannelId(Channel.INVALID_ID) 738 .setStartTimeUtcMillis(startTimeMs) 739 .setEndTimeUtcMillis(endTimeMs).build(); 740 } 741 742 @Override performTrimMemory(int level)743 public void performTrimMemory(int level) { 744 mChannelId2ProgramUpdatedListeners.clearEmptyCache(); 745 } 746 } 747