aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/data/ProgramDataManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/data/ProgramDataManager.java')
-rw-r--r--src/com/android/tv/data/ProgramDataManager.java695
1 files changed, 695 insertions, 0 deletions
diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java
new file mode 100644
index 00000000..80733bc5
--- /dev/null
+++ b/src/com/android/tv/data/ProgramDataManager.java
@@ -0,0 +1,695 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.data;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.LruCache;
+
+import com.android.tv.MainActivity.MemoryManageable;
+import com.android.tv.util.AsyncDbTask;
+import com.android.tv.util.Clock;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+public class ProgramDataManager implements MemoryManageable {
+ private static final String TAG = "ProgramDataManager";
+ private static final boolean DEBUG = false;
+
+ // To prevent from too many program update operations at the same time, we give random interval
+ // between PERIODIC_PROGRAM_UPDATE_MIN_MS and PERIODIC_PROGRAM_UPDATE_MAX_MS.
+ private static final long PERIODIC_PROGRAM_UPDATE_MIN_MS = TimeUnit.MINUTES.toMillis(5);
+ private static final long PERIODIC_PROGRAM_UPDATE_MAX_MS = TimeUnit.MINUTES.toMillis(10);
+ private static final long PROGRAM_PREFETCH_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5);
+ // TODO: need to optimize consecutive DB updates.
+ private static final long CURRENT_PROGRAM_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5);
+ @VisibleForTesting
+ static final long PROGRAM_GUIDE_SNAP_TIME_MS = TimeUnit.MINUTES.toMillis(30);
+ @VisibleForTesting
+ static final long PROGRAM_GUIDE_MAX_TIME_RANGE = TimeUnit.DAYS.toMillis(2);
+
+ // TODO: Use TvContract constants, once they become public.
+ private static final String PARAM_START_TIME = "start_time";
+ private static final String PARAM_END_TIME = "end_time";
+ // COLUMN_CHANNEL_ID, COLUMN_END_TIME_UTC_MILLIS are added to detect duplicated programs.
+ // Duplicated programs are always consecutive by the sorting order.
+ private static final String SORT_BY_TIME = Programs.COLUMN_START_TIME_UTC_MILLIS + ", "
+ + Programs.COLUMN_CHANNEL_ID + ", " + Programs.COLUMN_END_TIME_UTC_MILLIS;
+
+ private static final int MSG_UPDATE_CURRENT_PROGRAMS = 1000;
+ private static final int MSG_UPDATE_ONE_CURRENT_PROGRAM = 1001;
+ private static final int MSG_UPDATE_PREFETCH_PROGRAM = 1002;
+
+ private final Clock mClock;
+ private final ContentResolver mContentResolver;
+ private boolean mStarted;
+ private long mProgramPrefetchUpdateWaitMs;
+ private ProgramsUpdateTask mProgramsUpdateTask;
+ private final LongSparseArray<UpdateCurrentProgramForChannelTask> mProgramUpdateTaskMap =
+ new LongSparseArray<>();
+ private long mLastPrefetchTaskRunMs;
+ private ProgramsPrefetchTask mProgramsPrefetchTask;
+ private final Map<Long, Program> mChannelIdCurrentProgramMap = new HashMap<>();
+ private final Map<Long, List<OnCurrentProgramUpdatedListener>>
+ mOnCurrentProgramUpdatedListenersMap = new HashMap<>();
+ private final Handler mHandler;
+ private final List<Listener> mListeners = new ArrayList<>();
+
+ private final ContentObserver mProgramObserver;
+
+ private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new HashMap<>();
+
+ // Any program that ends prior to this time will be removed from the cache
+ // when a channel's current program is updated.
+ // Note that there's no limit for end time.
+ private long mPrefetchTimeRangeStartMs;
+
+ private boolean mPauseProgramUpdate = false;
+ private final LruCache<Long, Program> mZeroLengthProgramCache = new LruCache<>(10);
+
+ public ProgramDataManager(Context context) {
+ this(context.getContentResolver(), Clock.SYSTEM, Looper.myLooper());
+ }
+
+ @VisibleForTesting
+ ProgramDataManager(ContentResolver contentResolver, Clock time, Looper looper) {
+ mClock = time;
+ mContentResolver = contentResolver;
+ mHandler = new MyHandler(looper);
+ mProgramObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) {
+ mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS);
+ }
+ if (isProgramUpdatePaused()) {
+ return;
+ }
+ // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be quite long
+ // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message and
+ // send MSG_UPDATE_PREFETCH_PROGRAM again.
+ mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM);
+ mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
+ }
+ };
+ mProgramPrefetchUpdateWaitMs = PROGRAM_PREFETCH_UPDATE_WAIT_MS;
+ }
+
+ @VisibleForTesting
+ ContentObserver getContentObserver() {
+ return mProgramObserver;
+ }
+
+ /**
+ * Set the program prefetch update wait which gives the delay to query all programs from DB
+ * to prevent from too frequent DB queries.
+ * Default value is {@link #PROGRAM_PREFETCH_UPDATE_WAIT_MS}
+ */
+ @VisibleForTesting
+ void setProgramPrefetchUpdateWait(long programPrefetchUpdateWaitMs) {
+ mProgramPrefetchUpdateWaitMs = programPrefetchUpdateWaitMs;
+ }
+
+ /**
+ * Starts the manager.
+ */
+ public void start() {
+ if (mStarted) {
+ return;
+ }
+ mStarted = true;
+ // Should be called directly instead of posting MSG_UPDATE_CURRENT_PROGRAMS message
+ // to the handler. If not, another DB task can be executed before loading current programs.
+ handleUpdateCurrentPrograms();
+ mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
+ mContentResolver.registerContentObserver(Programs.CONTENT_URI,
+ true, mProgramObserver);
+ }
+
+ /**
+ * Stops the manager. It clears manager states and runs pending DB operations. Added listeners
+ * aren't automatically removed by this method.
+ */
+ public void stop() {
+ if (!mStarted) {
+ return;
+ }
+ mStarted = false;
+
+ mContentResolver.unregisterContentObserver(mProgramObserver);
+ mHandler.removeCallbacksAndMessages(null);
+
+ clearTask(mProgramUpdateTaskMap);
+ cancelPrefetchTask();
+ if (mProgramsUpdateTask != null) {
+ mProgramsUpdateTask.cancel(true);
+ mProgramsUpdateTask = null;
+ }
+ }
+
+ /**
+ * Returns the current program at the specified channel.
+ */
+ public Program getCurrentProgram(long channelId) {
+ return mChannelIdCurrentProgramMap.get(channelId);
+ }
+
+ /**
+ * A listener interface to receive notification on program data retrieval from DB.
+ */
+ public interface Listener {
+ /**
+ * Called when a Program data is now available through getProgram()
+ * after the DB operation is done which wasn't before.
+ * This would be called only if fetched data is around the selected program.
+ **/
+ void onProgramUpdated();
+ }
+
+ /**
+ * Adds the {@link Listener}.
+ */
+ public void addListener(Listener listener) {
+ mListeners.add(listener);
+ }
+
+ /**
+ * Removes the {@link Listener}.
+ */
+ public void removeListener(Listener listener) {
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Returns the programs for the given channel which ends after the given start time.
+ *
+ * @return {@link List} with Programs. It may includes dummy program if the entry needs DB
+ * operations to get.
+ */
+ public List<Program> getPrograms(long channelId, long startTime) {
+ ArrayList<Program> cachedPrograms = mChannelIdProgramCache.get(channelId);
+ if (cachedPrograms == null) {
+ return Collections.emptyList();
+ }
+ int startIndex = getProgramIndexAt(cachedPrograms, startTime);
+ return Collections.unmodifiableList(
+ cachedPrograms.subList(startIndex, cachedPrograms.size()));
+ }
+
+ // Returns the index of program that is played at the specified time.
+ // If there isn't, return the first program among programs that starts after the given time
+ // if returnNextProgram is {@code true}.
+ private int getProgramIndexAt(List<Program> programs, long time) {
+ Program key = mZeroLengthProgramCache.get(time);
+ if (key == null) {
+ key = createDummyProgram(time, time);
+ mZeroLengthProgramCache.put(time, key);
+ }
+ int index = Collections.binarySearch(programs, key);
+ if (index < 0) {
+ index = -(index + 1); // change it to index to be added.
+ if (index > 0 && isProgramPlayedAt(programs.get(index - 1), time)) {
+ // A program is played at that time.
+ return index - 1;
+ }
+ return index;
+ }
+ return index;
+ }
+
+ private boolean isProgramPlayedAt(Program program, long time) {
+ return program.getStartTimeUtcMillis() <= time && time <= program.getEndTimeUtcMillis();
+ }
+
+ /**
+ * Adds the listener to be notified if current program is updated for a channel.
+ *
+ * @param channelId A channel ID to get notified. If it's {@link Channel#INVALID_ID}, the
+ * listener would be called whenever a current program is updated.
+ */
+ public void addOnCurrentProgramUpdatedListener(
+ long channelId, OnCurrentProgramUpdatedListener listener) {
+ List<OnCurrentProgramUpdatedListener> listeners =
+ mOnCurrentProgramUpdatedListenersMap.get(channelId);
+ if (listeners == null) {
+ listeners = new ArrayList<>();
+ mOnCurrentProgramUpdatedListenersMap.put(channelId, listeners);
+ }
+ listeners.add(listener);
+ }
+
+ /**
+ * Removes the listener previously added by
+ * {@link #addOnCurrentProgramUpdatedListener(long, OnCurrentProgramUpdatedListener)}.
+ */
+ public void removeOnCurrentProgramUpdatedListener(
+ long channelId, OnCurrentProgramUpdatedListener listener) {
+ List<OnCurrentProgramUpdatedListener> listeners =
+ mOnCurrentProgramUpdatedListenersMap.get(channelId);
+ if (listeners != null) {
+ listeners.remove(listener);
+ // Do not remove list from map although it's empty to reduce GC.
+ // The unused list is removed in {@link #performTrimMemory} which is called when
+ // the memory usage is high.
+ }
+ }
+
+ private void notifyCurrentProgramUpdate(long channelId, Program program) {
+ List<OnCurrentProgramUpdatedListener> listeners =
+ mOnCurrentProgramUpdatedListenersMap.get(channelId);
+ if (listeners != null) {
+ for (OnCurrentProgramUpdatedListener listener : listeners) {
+ listener.onCurrentProgramUpdated(channelId, program);
+ }
+ }
+ listeners = mOnCurrentProgramUpdatedListenersMap.get(Channel.INVALID_ID);
+ if (listeners != null) {
+ for (OnCurrentProgramUpdatedListener listener : listeners) {
+ listener.onCurrentProgramUpdated(channelId, program);
+ }
+ }
+ }
+
+ private void updateCurrentProgram(long channelId, Program program) {
+ Program previousProgram = mChannelIdCurrentProgramMap.put(channelId, program);
+ if (!Objects.equals(program, previousProgram)) {
+ removePreviousProgramsAndUpdateCurrentProgramInCache(channelId, program);
+ notifyCurrentProgramUpdate(channelId, program);
+ }
+
+ long delayedTime;
+ if (program == null) {
+ delayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS
+ + (long) (Math.random() * (PERIODIC_PROGRAM_UPDATE_MAX_MS
+ - PERIODIC_PROGRAM_UPDATE_MIN_MS));
+ } else {
+ delayedTime = program.getEndTimeUtcMillis() - mClock.currentTimeMillis();
+ }
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(
+ MSG_UPDATE_ONE_CURRENT_PROGRAM, channelId), delayedTime);
+ }
+
+ private void removePreviousProgramsAndUpdateCurrentProgramInCache(
+ long channelId, Program currentProgram) {
+ if (!Program.isValid(currentProgram)) {
+ return;
+ }
+ ArrayList<Program> cachedPrograms = mChannelIdProgramCache.remove(channelId);
+ if (cachedPrograms == null) {
+ return;
+ }
+ ListIterator<Program> i = cachedPrograms.listIterator();
+ while (i.hasNext()) {
+ Program cachedProgram = i.next();
+ if (cachedProgram.getEndTimeUtcMillis() <= mPrefetchTimeRangeStartMs) {
+ // Remove previous programs which will not be shown in program guide.
+ i.remove();
+ continue;
+ }
+
+ if (cachedProgram.getEndTimeUtcMillis() <= currentProgram
+ .getStartTimeUtcMillis()) {
+ // Keep the programs that ends earlier than current program
+ // but later than mPrefetchTimeRangeStartMs.
+ continue;
+ }
+
+ // Update dummy program around current program if any.
+ if (cachedProgram.getStartTimeUtcMillis() < currentProgram
+ .getStartTimeUtcMillis()) {
+ // The dummy program starts earlier than the current program. Adjust its end time.
+ i.set(createDummyProgram(cachedProgram.getStartTimeUtcMillis(),
+ currentProgram.getStartTimeUtcMillis()));
+ i.add(currentProgram);
+ } else {
+ i.set(currentProgram);
+ }
+ if (currentProgram.getEndTimeUtcMillis() < cachedProgram.getEndTimeUtcMillis()) {
+ // The dummy program ends later than the current program. Adjust its start time.
+ i.add(createDummyProgram(currentProgram.getEndTimeUtcMillis(),
+ cachedProgram.getEndTimeUtcMillis()));
+ }
+ break;
+ }
+ mChannelIdProgramCache.put(channelId, cachedPrograms);
+ }
+
+ private void handleUpdateCurrentPrograms() {
+ if (mProgramsUpdateTask != null) {
+ mHandler.sendEmptyMessageDelayed(MSG_UPDATE_CURRENT_PROGRAMS,
+ CURRENT_PROGRAM_UPDATE_WAIT_MS);
+ return;
+ }
+ clearTask(mProgramUpdateTaskMap);
+ mHandler.removeMessages(MSG_UPDATE_ONE_CURRENT_PROGRAM);
+ mProgramsUpdateTask = new ProgramsUpdateTask(mContentResolver, mClock.currentTimeMillis());
+ mProgramsUpdateTask.executeOnDbThread();
+ }
+
+ private class ProgramsPrefetchTask
+ extends AsyncDbTask<Void, Void, Map<Long, ArrayList<Program>>> {
+ private final long mStartTimeMs;
+ private final long mEndTimeMs;
+
+ private boolean mSuccess;
+
+ public ProgramsPrefetchTask() {
+ long time = mClock.currentTimeMillis();
+ mStartTimeMs = Utils
+ .floorTime(time - PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS);
+ mEndTimeMs = mStartTimeMs + PROGRAM_GUIDE_MAX_TIME_RANGE;
+ mSuccess = false;
+ }
+
+ @Override
+ protected Map<Long, ArrayList<Program>> doInBackground(Void... params) {
+ Map<Long, ArrayList<Program>> programMap = new HashMap<>();
+ if (DEBUG) {
+ Log.d(TAG, "Starts programs prefetch. " + Utils.toTimeString(mStartTimeMs) + "-"
+ + Utils.toTimeString(mEndTimeMs));
+ }
+ Uri uri = Programs.CONTENT_URI.buildUpon()
+ .appendQueryParameter(PARAM_START_TIME, String.valueOf(mStartTimeMs))
+ .appendQueryParameter(PARAM_END_TIME, String.valueOf(mEndTimeMs)).build();
+ final int RETRY_COUNT = 3;
+ Program lastReadProgram = null;
+ for (int retryCount = RETRY_COUNT; retryCount > 0; retryCount--) {
+ if (isProgramUpdatePaused()) {
+ return null;
+ }
+ programMap.clear();
+ try (Cursor c = mContentResolver.query(uri, Program.PROJECTION, null, null,
+ SORT_BY_TIME)) {
+ if (c == null) {
+ continue;
+ }
+ while (c.moveToNext()) {
+ if (isCancelled()) {
+ if (DEBUG) {
+ Log.d(TAG, "ProgramsPrefetchTask canceled.");
+ }
+ return null;
+ }
+ Program program = Program.fromCursor(c);
+ if (isDuplicateProgram(program, lastReadProgram)) {
+ continue;
+ } else {
+ lastReadProgram = program;
+ }
+ ArrayList<Program> programs = programMap.get(program.getChannelId());
+ if (programs == null) {
+ programs = new ArrayList<>();
+ programMap.put(program.getChannelId(), programs);
+ }
+ programs.add(program);
+ }
+ mSuccess = true;
+ break;
+ } catch (IllegalStateException e) {
+ if (DEBUG) {
+ Log.d(TAG, "Database is changed while querying. Will retry.");
+ }
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Ends programs prefetch for " + programMap.size() + " channels");
+ }
+ return programMap;
+ }
+
+ @Override
+ protected void onPostExecute(Map<Long, ArrayList<Program>> programs) {
+ mProgramsPrefetchTask = null;
+ if (isProgramUpdatePaused()) {
+ // ProgramsPrefetchTask will run again once setPauseProgramUpdate(false) is called.
+ return;
+ }
+ long nextMessageDelayedTime;
+ if (mSuccess) {
+ mChannelIdProgramCache = programs;
+ notifyProgramUpdated();
+ long currentTime = mClock.currentTimeMillis();
+ mLastPrefetchTaskRunMs = currentTime;
+ nextMessageDelayedTime =
+ Utils.floorTime(mLastPrefetchTaskRunMs + PROGRAM_GUIDE_SNAP_TIME_MS,
+ PROGRAM_GUIDE_SNAP_TIME_MS) - currentTime;
+ } else {
+ nextMessageDelayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS;
+ }
+ if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
+ mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PREFETCH_PROGRAM,
+ nextMessageDelayedTime);
+ }
+ }
+ }
+
+ private void notifyProgramUpdated() {
+ for (Listener listener : mListeners) {
+ listener.onProgramUpdated();
+ }
+ }
+
+ private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> {
+ public ProgramsUpdateTask(ContentResolver contentResolver, long time) {
+ super(contentResolver, Programs.CONTENT_URI.buildUpon()
+ .appendQueryParameter(PARAM_START_TIME, String.valueOf(time))
+ .appendQueryParameter(PARAM_END_TIME, String.valueOf(time)).build(),
+ Program.PROJECTION, null, null, SORT_BY_TIME);
+ }
+
+ @Override
+ public List<Program> onQuery(Cursor c) {
+ final List<Program> programs = new ArrayList<>();
+ if (c != null) {
+ Program lastReadProgram = null;
+ while (c.moveToNext()) {
+ if (isCancelled()) {
+ return programs;
+ }
+ Program program = Program.fromCursor(c);
+ if (isDuplicateProgram(program, lastReadProgram)) {
+ continue;
+ } else {
+ lastReadProgram = program;
+ }
+ programs.add(program);
+ }
+ }
+ return programs;
+ }
+
+ @Override
+ protected void onPostExecute(List<Program> programs) {
+ if (DEBUG) {
+ Log.d(TAG, "ProgramsUpdateTask done");
+ }
+ mProgramsUpdateTask = null;
+ if (programs == null) {
+ return;
+ }
+ Set<Long> removedChannelIds = new HashSet<>(mChannelIdCurrentProgramMap.keySet());
+ for (Program program : programs) {
+ long channelId = program.getChannelId();
+ updateCurrentProgram(channelId, program);
+ removedChannelIds.remove(channelId);
+ }
+ for (Long channelId : removedChannelIds) {
+ mChannelIdProgramCache.remove(channelId);
+ mChannelIdCurrentProgramMap.remove(channelId);
+ notifyCurrentProgramUpdate(channelId, null);
+ }
+ }
+ }
+
+ private class UpdateCurrentProgramForChannelTask extends AsyncDbTask.AsyncQueryTask<Program> {
+ private final long mChannelId;
+ private UpdateCurrentProgramForChannelTask(ContentResolver contentResolver, long channelId,
+ long time) {
+ super(contentResolver, TvContract.buildProgramsUriForChannel(channelId, time, time),
+ Program.PROJECTION, null, null, SORT_BY_TIME);
+ mChannelId = channelId;
+ }
+
+ @Override
+ public Program onQuery(Cursor c) {
+ Program program = null;
+ if (c != null && c.moveToNext()) {
+ program = Program.fromCursor(c);
+ }
+ return program;
+ }
+
+ @Override
+ protected void onPostExecute(Program program) {
+ mProgramUpdateTaskMap.remove(mChannelId);
+ updateCurrentProgram(mChannelId, program);
+ }
+ }
+
+ private class MyHandler extends Handler {
+ public MyHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_CURRENT_PROGRAMS:
+ handleUpdateCurrentPrograms();
+ break;
+ case MSG_UPDATE_ONE_CURRENT_PROGRAM: {
+ long channelId = (Long) msg.obj;
+ UpdateCurrentProgramForChannelTask oldTask = mProgramUpdateTaskMap
+ .get(channelId);
+ if (oldTask != null) {
+ oldTask.cancel(true);
+ }
+ UpdateCurrentProgramForChannelTask
+ task = new UpdateCurrentProgramForChannelTask(
+ mContentResolver, channelId, mClock.currentTimeMillis());
+ mProgramUpdateTaskMap.put(channelId, task);
+ task.executeOnDbThread();
+ break;
+ }
+ case MSG_UPDATE_PREFETCH_PROGRAM: {
+ if (isProgramUpdatePaused()) {
+ return;
+ }
+ if (mProgramsPrefetchTask != null) {
+ mHandler.sendEmptyMessageDelayed(msg.what, mProgramPrefetchUpdateWaitMs);
+ return;
+ }
+ long delayMillis = mLastPrefetchTaskRunMs + mProgramPrefetchUpdateWaitMs
+ - mClock.currentTimeMillis();
+ if (delayMillis > 0) {
+ mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PREFETCH_PROGRAM, delayMillis);
+ } else {
+ mProgramsPrefetchTask = new ProgramsPrefetchTask();
+ mProgramsPrefetchTask.executeOnDbThread();
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Pause program update.
+ * Updating program data will result in UI refresh,
+ * but UI is fragile to handle it so we'd better disable it for a while.
+ */
+ public void setPauseProgramUpdate(boolean pauseProgramUpdate) {
+ if (mPauseProgramUpdate && !pauseProgramUpdate) {
+ if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
+ // MSG_UPDATE_PRFETCH_PROGRAM can be empty
+ // if prefetch task is launched while program update is paused.
+ // Update immediately in that case.
+ mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
+ }
+ }
+ mPauseProgramUpdate = pauseProgramUpdate;
+ }
+
+ private boolean isProgramUpdatePaused() {
+ // Although pause is requested, we need to keep updating if cache is empty.
+ return mPauseProgramUpdate && !mChannelIdProgramCache.isEmpty();
+ }
+
+ /**
+ * Sets program data prefetch time range.
+ * Any program data that ends before the start time will be removed from the cache later.
+ * Note that there's no limit for end time.
+ */
+ public void setPrefetchTimeRange(long startTimeMs) {
+ if (mPrefetchTimeRangeStartMs > startTimeMs) {
+ // TODO: Do not throw exception and simply refresh the cache.
+ throw new IllegalArgumentException("");
+ }
+ mPrefetchTimeRangeStartMs = startTimeMs;
+ }
+
+ private void clearTask(LongSparseArray<UpdateCurrentProgramForChannelTask> tasks) {
+ for (int i = 0; i < tasks.size(); i++) {
+ tasks.valueAt(i).cancel(true);
+ }
+ tasks.clear();
+ }
+
+ private void cancelPrefetchTask() {
+ if (mProgramsPrefetchTask != null) {
+ mProgramsPrefetchTask.cancel(true);
+ mProgramsPrefetchTask = null;
+ }
+ }
+
+ // Create dummy program which indicates data isn't loaded yet so DB query is required.
+ private Program createDummyProgram(long startTimeMs, long endTimeMs) {
+ return new Program.Builder()
+ .setChannelId(Channel.INVALID_ID)
+ .setStartTimeUtcMillis(startTimeMs)
+ .setEndTimeUtcMillis(endTimeMs).build();
+ }
+
+ private static boolean isDuplicateProgram(Program p1, Program p2) {
+ if (p1 == null || p2 == null) {
+ return false;
+ }
+ boolean isDuplicate = p1.getChannelId() == p2.getChannelId()
+ && p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis()
+ && p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis();
+ if (isDuplicate) {
+ Log.w(TAG, "Duplicate programs detected! - \"" + p1.getTitle() + "\" and \""
+ + p2.getTitle() + "\"");
+ }
+ return isDuplicate;
+ }
+
+ @Override
+ public void performTrimMemory(int level) {
+ // Removes unused listeners.
+ Iterator<Entry<Long, List<OnCurrentProgramUpdatedListener>>> it =
+ mOnCurrentProgramUpdatedListenersMap.entrySet().iterator();
+ while (it.hasNext()) {
+ if (it.next().getValue().isEmpty()) {
+ it.remove();
+ }
+ }
+ }
+}