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