blob: dcfa69e26a572068d076eb316d921d1e0b8e85b5 [file] [log] [blame]
/*
* 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.AnyThread;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
import android.util.ArraySet;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.LruCache;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.memory.MemoryManageable;
import com.android.tv.common.util.Clock;
import com.android.tv.data.api.Channel;
import com.android.tv.data.api.Program;
import com.android.tv.perf.EventNames;
import com.android.tv.perf.PerformanceMonitor;
import com.android.tv.perf.TimerEvent;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.MultiLongSparseArray;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.TvProviderUtils;
import com.android.tv.util.Utils;
import com.android.tv.common.flags.BackendKnobsFlags;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@MainThread
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.
@VisibleForTesting
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);
// Default fetch hours
private static final long FETCH_HOURS_MS = TimeUnit.HOURS.toMillis(24);
// Load data earlier for smooth scrolling.
private static final long BUFFER_HOURS_MS = TimeUnit.HOURS.toMillis(6);
// 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 String SORT_BY_CHANNEL_ID =
Programs.COLUMN_CHANNEL_ID
+ ", "
+ Programs.COLUMN_START_TIME_UTC_MILLIS
+ " DESC, "
+ Programs.COLUMN_END_TIME_UTC_MILLIS
+ " ASC, "
+ Programs._ID
+ " DESC";
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 static final int MSG_UPDATE_CONTENT_RATINGS = 1003;
private final Context mContext;
private final Clock mClock;
private final ContentResolver mContentResolver;
private final Executor mDbExecutor;
private final BackendKnobsFlags mBackendKnobsFlags;
private final PerformanceMonitor mPerformanceMonitor;
private final ChannelDataManager mChannelDataManager;
private final TvInputManagerHelper mTvInputManagerHelper;
private boolean mStarted;
// Updated only on the main thread.
private volatile boolean mCurrentProgramsLoadFinished;
private ProgramsUpdateTask mProgramsUpdateTask;
private final LongSparseArray<UpdateCurrentProgramForChannelTask> mProgramUpdateTaskMap =
new LongSparseArray<>();
private final Map<Long, Program> mChannelIdCurrentProgramMap = new ConcurrentHashMap<>();
private final MultiLongSparseArray<OnCurrentProgramUpdatedListener>
mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>();
private final Handler mHandler;
private final Set<Callback> mCallbacks = new ArraySet<>();
private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new ConcurrentHashMap<>();
private final Set<Long> mCompleteInfoChannelIds = new HashSet<>();
private final ContentObserver mProgramObserver;
private boolean mPrefetchEnabled;
private long mProgramPrefetchUpdateWaitMs;
private long mLastPrefetchTaskRunMs;
private ProgramsPrefetchTask mProgramsPrefetchTask;
// 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);
// Current tuned channel.
private long mTunedChannelId;
// Hours of data to be fetched, it is updated during horizontal scroll.
// Note that it should never exceed programGuideMaxHours.
private long mMaxFetchHoursMs = FETCH_HOURS_MS;
@MainThread
public ProgramDataManager(Context context) {
this(
context,
TvSingletons.getSingletons(context).getDbExecutor(),
context.getContentResolver(),
Clock.SYSTEM,
Looper.myLooper(),
TvSingletons.getSingletons(context).getBackendKnobs(),
TvSingletons.getSingletons(context).getPerformanceMonitor(),
TvSingletons.getSingletons(context).getChannelDataManager(),
TvSingletons.getSingletons(context).getTvInputManagerHelper());
}
@VisibleForTesting
ProgramDataManager(
Context context,
Executor executor,
ContentResolver contentResolver,
Clock time,
Looper looper,
BackendKnobsFlags backendKnobsFlags,
PerformanceMonitor performanceMonitor,
ChannelDataManager channelDataManager,
TvInputManagerHelper tvInputManagerHelper) {
mContext = context;
mDbExecutor = executor;
mClock = time;
mContentResolver = contentResolver;
mHandler = new MyHandler(looper);
mBackendKnobsFlags = backendKnobsFlags;
mPerformanceMonitor = performanceMonitor;
mChannelDataManager = channelDataManager;
mTvInputManagerHelper = tvInputManagerHelper;
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;
}
if (mPrefetchEnabled) {
// 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_CONTENT_RATINGS);
if (mPrefetchEnabled) {
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.
*/
@VisibleForTesting
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;
}
}
@AnyThread
public boolean isCurrentProgramsLoadFinished() {
return mCurrentProgramsLoadFinished;
}
/** Returns the current program at the specified channel. */
@AnyThread
public Program getCurrentProgram(long channelId) {
return mChannelIdCurrentProgramMap.get(channelId);
}
/** Returns all the current programs. */
@AnyThread
public List<Program> getCurrentPrograms() {
return new ArrayList<>(mChannelIdCurrentProgramMap.values());
}
/** Reloads program data. */
public void reload() {
if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) {
mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS);
}
if (mPrefetchEnabled && !mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
}
}
/**
* Prefetch program data if needed.
*
* @param channelId ID of the channel to prefetch
* @param selectedProgramIndex index of selected program.
*/
public void prefetchChannel(long channelId, int selectedProgramIndex) {
long startTimeMs =
Utils.floorTime(
mClock.currentTimeMillis() - PROGRAM_GUIDE_SNAP_TIME_MS,
PROGRAM_GUIDE_SNAP_TIME_MS);
long programGuideMaxHoursMs =
TimeUnit.HOURS.toMillis(mBackendKnobsFlags.programGuideMaxHours());
long endTimeMs = 0;
if (mMaxFetchHoursMs < programGuideMaxHoursMs
&& isHorizontalLoadNeeded(startTimeMs, channelId, selectedProgramIndex)) {
// Horizontal scrolling needs to load data of further days.
mMaxFetchHoursMs = Math.min(programGuideMaxHoursMs, mMaxFetchHoursMs + FETCH_HOURS_MS);
mCompleteInfoChannelIds.clear();
}
// Load max hours complete data for first channel.
if (mCompleteInfoChannelIds.isEmpty()) {
endTimeMs = startTimeMs + programGuideMaxHoursMs;
} else if (!mCompleteInfoChannelIds.contains(channelId)) {
endTimeMs = startTimeMs + mMaxFetchHoursMs;
}
if (endTimeMs > 0) {
mCompleteInfoChannelIds.add(channelId);
new SingleChannelPrefetchTask(channelId, startTimeMs, endTimeMs).executeOnDbThread();
}
}
public void prefetchChannel(long channelId) {
prefetchChannel(channelId, 0);
}
/**
* Check if enough data is present for horizontal scroll, otherwise prefetch programs.
*
* <p>If end time of current program is past {@code BUFFER_HOURS_MS} less than the fetched time
* we need to prefetch proceeding programs.
*
* @param startTimeMs Fetch start time, it is used to get fetch end time.
* @param channelId
* @param selectedProgramIndex
* @return {@code true} If data load is needed, else {@code false}.
*/
private boolean isHorizontalLoadNeeded(
long startTimeMs, long channelId, int selectedProgramIndex) {
if (mChannelIdProgramCache.containsKey(channelId)) {
ArrayList<Program> programs = mChannelIdProgramCache.get(channelId);
long marginEndTime = startTimeMs + mMaxFetchHoursMs - BUFFER_HOURS_MS;
return programs.size() > selectedProgramIndex &&
programs.get(selectedProgramIndex).getEndTimeUtcMillis() > marginEndTime;
}
return false;
}
public void onChannelTuned(long channelId) {
mTunedChannelId = channelId;
prefetchChannel(channelId);
}
/** A Callback interface to receive notification on program data retrieval from DB. */
public interface Callback {
/**
* 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();
/**
* Called when we update program data during scrolling. Data is loaded from DB on request
* basis. It loads data based on horizontal scrolling as well.
*/
void onChannelUpdated();
}
/** Adds the {@link Callback}. */
public void addCallback(Callback callback) {
mCallbacks.add(callback);
}
/** Removes the {@link Callback}. */
public void removeCallback(Callback callback) {
mCallbacks.remove(callback);
}
/** Enables or Disables program prefetch. */
public void setPrefetchEnabled(boolean enable) {
if (mPrefetchEnabled == enable) {
return;
}
if (enable) {
mPrefetchEnabled = true;
mLastPrefetchTaskRunMs = 0;
if (mStarted) {
mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
}
} else {
mPrefetchEnabled = false;
cancelPrefetchTask();
clearChannelInfoMap();
mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM);
}
}
/**
* Returns the programs for the given channel which ends after the given start time.
*
* <p>Prefetch should be enabled to call it.
*
* @return {@link List} with Programs. It may includes stub program if the entry needs DB
* operations to get.
*/
public List<Program> getPrograms(long channelId, long startTime) {
SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
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.
*
* <p>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 = createStubProgram(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) {
mChannelId2ProgramUpdatedListeners.put(channelId, listener);
}
/**
* Removes the listener previously added by {@link #addOnCurrentProgramUpdatedListener(long,
* OnCurrentProgramUpdatedListener)}.
*/
public void removeOnCurrentProgramUpdatedListener(
long channelId, OnCurrentProgramUpdatedListener listener) {
mChannelId2ProgramUpdatedListeners.remove(channelId, listener);
}
private void notifyCurrentProgramUpdate(long channelId, Program program) {
for (OnCurrentProgramUpdatedListener listener :
mChannelId2ProgramUpdatedListeners.get(channelId)) {
listener.onCurrentProgramUpdated(channelId, program);
}
for (OnCurrentProgramUpdatedListener listener :
mChannelId2ProgramUpdatedListeners.get(Channel.INVALID_ID)) {
listener.onCurrentProgramUpdated(channelId, program);
}
}
private void updateCurrentProgram(long channelId, Program program) {
Program previousProgram =
program == null
? mChannelIdCurrentProgramMap.remove(channelId)
: mChannelIdCurrentProgramMap.put(channelId, program);
if (!Objects.equals(program, previousProgram)) {
if (mPrefetchEnabled) {
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) {
SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
if (!Program.isProgramValid(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 stub program around current program if any.
if (cachedProgram.getStartTimeUtcMillis() < currentProgram.getStartTimeUtcMillis()) {
// The stub program starts earlier than the current program. Adjust its end time.
i.set(
createStubProgram(
cachedProgram.getStartTimeUtcMillis(),
currentProgram.getStartTimeUtcMillis()));
i.add(currentProgram);
} else {
i.set(currentProgram);
}
if (currentProgram.getEndTimeUtcMillis() < cachedProgram.getEndTimeUtcMillis()) {
// The stub program ends later than the current program. Adjust its start time.
i.add(
createStubProgram(
currentProgram.getEndTimeUtcMillis(),
cachedProgram.getEndTimeUtcMillis()));
}
break;
}
if (cachedPrograms.isEmpty()) {
// If all the cached programs finish before mPrefetchTimeRangeStartMs, the
// currentProgram would not have a chance to be inserted to the cache.
cachedPrograms.add(currentProgram);
}
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(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;
private TimerEvent mFromEmptyCacheTimeEvent;
public ProgramsPrefetchTask() {
super(mDbExecutor);
long time = mClock.currentTimeMillis();
mStartTimeMs =
Utils.floorTime(time - PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS);
mEndTimeMs = mStartTimeMs + TimeUnit.HOURS.toMillis(getFetchDuration());
mSuccess = false;
}
@Override
protected void onPreExecute() {
if (mChannelIdCurrentProgramMap.isEmpty()) {
// No current program guide is shown.
// Measure the delay before users can see program guides.
mFromEmptyCacheTimeEvent = mPerformanceMonitor.startTimer();
}
}
@Override
protected Map<Long, ArrayList<Program>> doInBackground(Void... params) {
TimerEvent asyncTimeEvent = mPerformanceMonitor.startTimer();
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();
String[] projection = ProgramImpl.PARTIAL_PROJECTION;
if (TvProviderUtils.checkSeriesIdColumn(mContext, Programs.CONTENT_URI)) {
if (Utils.isProgramsUri(uri)) {
projection =
TvProviderUtils.addExtraColumnsToProjection(
projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
}
}
try (Cursor c = mContentResolver.query(uri, projection, null, null, SORT_BY_TIME)) {
if (c == null) {
continue;
}
while (c.moveToNext()) {
int duplicateCount = 0;
if (isCancelled()) {
if (DEBUG) {
Log.d(TAG, "ProgramsPrefetchTask canceled.");
}
return null;
}
Program program = ProgramImpl.fromCursorPartialProjection(c);
if (Program.isDuplicate(program, lastReadProgram)) {
duplicateCount++;
continue;
} else {
lastReadProgram = program;
}
ArrayList<Program> programs = programMap.get(program.getChannelId());
if (programs == null) {
programs = new ArrayList<>();
// To skip already loaded complete data.
Program currentProgramInfo =
mChannelIdCurrentProgramMap.get(program.getChannelId());
if (currentProgramInfo != null
&& Program.isDuplicate(program, currentProgramInfo)) {
program = currentProgramInfo;
}
programMap.put(program.getChannelId(), programs);
}
programs.add(program);
if (duplicateCount > 0) {
Log.w(TAG, "Found " + duplicateCount + " duplicate programs");
}
}
mSuccess = true;
break;
} catch (IllegalStateException e) {
if (DEBUG) {
Log.d(TAG, "Database is changed while querying. Will retry.");
}
} catch (SecurityException e) {
Log.w(TAG, "Security exception during program data query", e);
} catch (Exception e) {
Log.w(TAG, "Error during program data query", e);
}
}
if (DEBUG) {
Log.d(TAG, "Ends programs prefetch for " + programMap.size() + " channels");
}
mPerformanceMonitor.stopTimer(
asyncTimeEvent,
EventNames.PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND);
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) {
long currentTime = mClock.currentTimeMillis();
mLastPrefetchTaskRunMs = currentTime;
nextMessageDelayedTime =
Utils.floorTime(
mLastPrefetchTaskRunMs + PROGRAM_GUIDE_SNAP_TIME_MS,
PROGRAM_GUIDE_SNAP_TIME_MS)
- currentTime;
mChannelIdProgramCache = programs;
// Since cache has partial data we need to reset the map of complete data.
clearChannelInfoMap();
// Get complete projection of tuned channel.
prefetchChannel(mTunedChannelId);
notifyProgramUpdated();
if (mFromEmptyCacheTimeEvent != null) {
mPerformanceMonitor.stopTimer(
mFromEmptyCacheTimeEvent,
EventNames.PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE);
mFromEmptyCacheTimeEvent = null;
}
} else {
nextMessageDelayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS;
}
if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
mHandler.sendEmptyMessageDelayed(
MSG_UPDATE_PREFETCH_PROGRAM, nextMessageDelayedTime);
}
}
}
private void clearChannelInfoMap() {
mCompleteInfoChannelIds.clear();
mMaxFetchHoursMs = FETCH_HOURS_MS;
}
private long getFetchDuration() {
if (mChannelIdProgramCache.isEmpty()) {
return Math.max(1L, mBackendKnobsFlags.programGuideInitialFetchHours());
} else {
long durationHours;
int channelCount = mChannelDataManager.getChannelCount();
long knobsMaxHours = mBackendKnobsFlags.programGuideMaxHours();
long targetChannelCount = mBackendKnobsFlags.epgTargetChannelCount();
if (channelCount <= targetChannelCount) {
durationHours = Math.max(48L, knobsMaxHours);
} else {
// 2 days <= duration <= 14 days (336 hours)
durationHours = knobsMaxHours * targetChannelCount / channelCount;
if (durationHours < 48L) {
durationHours = 48L;
} else if (durationHours > 336L) {
durationHours = 336L;
}
}
return durationHours;
}
}
private class SingleChannelPrefetchTask extends AsyncDbTask.AsyncQueryTask<ArrayList<Program>> {
long mChannelId;
public SingleChannelPrefetchTask(long channelId, long startTimeMs, long endTimeMs) {
super(
mDbExecutor,
mContext,
TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
ProgramImpl.PROJECTION,
null,
null,
SORT_BY_TIME);
mChannelId = channelId;
}
@Override
protected ArrayList<Program> onQuery(Cursor c) {
ArrayList<Program> programMap = new ArrayList<>();
while (c.moveToNext()) {
Program program = ProgramImpl.fromCursor(c);
programMap.add(program);
}
return programMap;
}
@Override
protected void onPostExecute(ArrayList<Program> programs) {
mChannelIdProgramCache.put(mChannelId, programs);
notifyChannelUpdated();
}
}
private void notifyProgramUpdated() {
for (Callback callback : mCallbacks) {
callback.onProgramUpdated();
}
}
private void notifyChannelUpdated() {
for (Callback callback : mCallbacks) {
callback.onChannelUpdated();
}
}
private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> {
public ProgramsUpdateTask(long time) {
super(
mDbExecutor,
mContext,
Programs.CONTENT_URI
.buildUpon()
.appendQueryParameter(PARAM_START_TIME, String.valueOf(time))
.appendQueryParameter(PARAM_END_TIME, String.valueOf(time))
.build(),
ProgramImpl.PROJECTION,
null,
null,
SORT_BY_CHANNEL_ID);
}
@Override
public List<Program> onQuery(Cursor c) {
final List<Program> programs = new ArrayList<>();
if (c != null) {
int duplicateCount = 0;
Program lastReadProgram = null;
while (c.moveToNext()) {
if (isCancelled()) {
return programs;
}
Program program = ProgramImpl.fromCursor(c);
// Only one program is expected per channel for this query
// However, skip overlapping programs from same channel
if (Program.sameChannel(program, lastReadProgram)
&& Program.isOverlapping(program, lastReadProgram)) {
duplicateCount++;
continue;
} else {
lastReadProgram = program;
}
programs.add(program);
}
if (duplicateCount > 0) {
Log.w(TAG, "Found " + duplicateCount + " overlapping programs");
}
}
return programs;
}
@Override
protected void onPostExecute(List<Program> programs) {
if (DEBUG) Log.d(TAG, "ProgramsUpdateTask done");
mProgramsUpdateTask = null;
if (programs != null) {
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) {
if (mPrefetchEnabled) {
mChannelIdProgramCache.remove(channelId);
mCompleteInfoChannelIds.remove(channelId);
}
mChannelIdCurrentProgramMap.remove(channelId);
notifyCurrentProgramUpdate(channelId, null);
}
}
mCurrentProgramsLoadFinished = true;
}
}
private class UpdateCurrentProgramForChannelTask extends AsyncDbTask.AsyncQueryTask<Program> {
private final long mChannelId;
private UpdateCurrentProgramForChannelTask(long channelId, long time) {
super(
mDbExecutor,
mContext,
TvContract.buildProgramsUriForChannel(channelId, time, time),
ProgramImpl.PROJECTION,
null,
null,
SORT_BY_TIME);
mChannelId = channelId;
}
@Override
public Program onQuery(Cursor c) {
Program program = null;
if (c != null && c.moveToNext()) {
program = ProgramImpl.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(
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;
}
case MSG_UPDATE_CONTENT_RATINGS:
mTvInputManagerHelper.getContentRatingsManager().update();
break;
default:
// Do nothing
}
}
}
/**
* 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.
*
* <p>Prefetch should be enabled to call it.
*/
public void setPauseProgramUpdate(boolean pauseProgramUpdate) {
SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
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.
*
* <p>Prefetch should be enabled to call it.
*/
public void setPrefetchTimeRange(long startTimeMs) {
SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
if (mPrefetchTimeRangeStartMs > startTimeMs) {
// Fetch the programs immediately to re-create the cache.
if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
}
}
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 stub program which indicates data isn't loaded yet so DB query is required.
private Program createStubProgram(long startTimeMs, long endTimeMs) {
return new ProgramImpl.Builder()
.setChannelId(Channel.INVALID_ID)
.setStartTimeUtcMillis(startTimeMs)
.setEndTimeUtcMillis(endTimeMs)
.build();
}
@Override
public void performTrimMemory(int level) {
mChannelId2ProgramUpdatedListeners.clearEmptyCache();
}
}