blob: 9dfc05c0b8cfa471ca0ffb8a9e4e6afa0ef3f9f7 [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.guide;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.ArraySet;
import android.util.Log;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.GenreItems;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.data.ProgramImpl;
import com.android.tv.data.api.Channel;
import com.android.tv.data.api.Program;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/** Manages the channels and programs for the program guide. */
@MainThread
public class ProgramManager {
private static final String TAG = "ProgramManager";
private static final boolean DEBUG = false;
/**
* If the first entry's visible duration is shorter than this value, we clip the entry out.
* Note: If this value is larger than 1 min, it could cause mismatches between the entry's
* position and detailed view's time range.
*/
static final long FIRST_ENTRY_MIN_DURATION = TimeUnit.MINUTES.toMillis(1);
private static final long INVALID_ID = -1;
private final TvInputManagerHelper mTvInputManagerHelper;
private final ChannelDataManager mChannelDataManager;
private final ProgramDataManager mProgramDataManager;
private final DvrDataManager mDvrDataManager; // Only set if DVR is enabled
private final DvrScheduleManager mDvrScheduleManager;
private long mStartUtcMillis;
private long mEndUtcMillis;
private long mFromUtcMillis;
private long mToUtcMillis;
private List<Channel> mChannels = new ArrayList<>();
private final Map<Long, List<TableEntry>> mChannelIdEntriesMap = new HashMap<>();
private final List<List<Channel>> mGenreChannelList = new ArrayList<>();
private final List<Integer> mFilteredGenreIds = new ArrayList<>();
// Position of selected genre to filter channel list.
private int mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
// Channel list after applying genre filter.
// Should be matched with mSelectedGenreId always.
private List<Channel> mFilteredChannels = mChannels;
private boolean mChannelDataLoaded;
private final Set<Listener> mListeners = new ArraySet<>();
private final Set<TableEntriesUpdatedListener> mTableEntriesUpdatedListeners = new ArraySet<>();
private final Set<TableEntryChangedListener> mTableEntryChangedListeners = new ArraySet<>();
private final DvrDataManager.OnDvrScheduleLoadFinishedListener mDvrLoadedListener =
new DvrDataManager.OnDvrScheduleLoadFinishedListener() {
@Override
public void onDvrScheduleLoadFinished() {
if (mChannelDataLoaded) {
for (ScheduledRecording r : mDvrDataManager.getAllScheduledRecordings()) {
mScheduledRecordingListener.onScheduledRecordingAdded(r);
}
}
mDvrDataManager.removeDvrScheduleLoadFinishedListener(this);
}
};
private final ChannelDataManager.Listener mChannelDataManagerListener =
new ChannelDataManager.Listener() {
@Override
public void onLoadFinished() {
mChannelDataLoaded = true;
updateChannels(false);
}
@Override
public void onChannelListUpdated() {
updateChannels(false);
}
@Override
public void onChannelBrowsableChanged() {
updateChannels(false);
}
};
private final ProgramDataManager.Callback mProgramDataManagerCallback =
new ProgramDataManager.Callback() {
@Override
public void onProgramUpdated() {
updateTableEntries(true);
}
@Override
public void onChannelUpdated() {
updateTableEntriesWithoutNotification(false);
notifyTableEntriesUpdated();
}
};
private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener =
new DvrDataManager.ScheduledRecordingListener() {
@Override
public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
for (ScheduledRecording schedule : scheduledRecordings) {
TableEntry oldEntry = getTableEntry(schedule);
if (oldEntry != null) {
TableEntry newEntry =
new TableEntry(
oldEntry.channelId,
oldEntry.program,
schedule,
oldEntry.entryStartUtcMillis,
oldEntry.entryEndUtcMillis,
oldEntry.isBlocked());
updateEntry(oldEntry, newEntry);
}
}
}
@Override
public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
for (ScheduledRecording schedule : scheduledRecordings) {
TableEntry oldEntry = getTableEntry(schedule);
if (oldEntry != null) {
TableEntry newEntry =
new TableEntry(
oldEntry.channelId,
oldEntry.program,
null,
oldEntry.entryStartUtcMillis,
oldEntry.entryEndUtcMillis,
oldEntry.isBlocked());
updateEntry(oldEntry, newEntry);
}
}
}
@Override
public void onScheduledRecordingStatusChanged(
ScheduledRecording... scheduledRecordings) {
for (ScheduledRecording schedule : scheduledRecordings) {
TableEntry oldEntry = getTableEntry(schedule);
if (oldEntry != null) {
TableEntry newEntry =
new TableEntry(
oldEntry.channelId,
oldEntry.program,
schedule,
oldEntry.entryStartUtcMillis,
oldEntry.entryEndUtcMillis,
oldEntry.isBlocked());
updateEntry(oldEntry, newEntry);
}
}
}
};
private final OnConflictStateChangeListener mOnConflictStateChangeListener =
new OnConflictStateChangeListener() {
@Override
public void onConflictStateChange(
boolean conflict, ScheduledRecording... schedules) {
for (ScheduledRecording schedule : schedules) {
TableEntry entry = getTableEntry(schedule);
if (entry != null) {
notifyTableEntryUpdated(entry);
}
}
}
};
public ProgramManager(
TvInputManagerHelper tvInputManagerHelper,
ChannelDataManager channelDataManager,
ProgramDataManager programDataManager,
@Nullable DvrDataManager dvrDataManager,
@Nullable DvrScheduleManager dvrScheduleManager) {
mTvInputManagerHelper = tvInputManagerHelper;
mChannelDataManager = channelDataManager;
mProgramDataManager = programDataManager;
mDvrDataManager = dvrDataManager;
mDvrScheduleManager = dvrScheduleManager;
}
void programGuideVisibilityChanged(boolean visible) {
mProgramDataManager.setPauseProgramUpdate(visible);
if (visible) {
mChannelDataManager.addListener(mChannelDataManagerListener);
mProgramDataManager.addCallback(mProgramDataManagerCallback);
if (mDvrDataManager != null) {
if (!mDvrDataManager.isDvrScheduleLoadFinished()) {
mDvrDataManager.addDvrScheduleLoadFinishedListener(mDvrLoadedListener);
}
mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener);
}
if (mDvrScheduleManager != null) {
mDvrScheduleManager.addOnConflictStateChangeListener(
mOnConflictStateChangeListener);
}
} else {
mChannelDataManager.removeListener(mChannelDataManagerListener);
mProgramDataManager.removeCallback(mProgramDataManagerCallback);
if (mDvrDataManager != null) {
mDvrDataManager.removeDvrScheduleLoadFinishedListener(mDvrLoadedListener);
mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
}
if (mDvrScheduleManager != null) {
mDvrScheduleManager.removeOnConflictStateChangeListener(
mOnConflictStateChangeListener);
}
}
}
/** Adds a {@link Listener}. */
void addListener(Listener listener) {
mListeners.add(listener);
}
/** Registers a listener to be invoked when table entries are updated. */
void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
mTableEntriesUpdatedListeners.add(listener);
}
/** Registers a listener to be invoked when a table entry is changed. */
void addTableEntryChangedListener(TableEntryChangedListener listener) {
mTableEntryChangedListeners.add(listener);
}
/** Removes a {@link Listener}. */
void removeListener(Listener listener) {
mListeners.remove(listener);
}
/** Removes a previously installed table entries update listener. */
void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
mTableEntriesUpdatedListeners.remove(listener);
}
/** Removes a previously installed table entry changed listener. */
void removeTableEntryChangedListener(TableEntryChangedListener listener) {
mTableEntryChangedListeners.remove(listener);
}
/**
* Resets channel list with given genre. Caller should call {@link #buildGenreFilters()} prior
* to call this API to make This notifies channel updates to listeners.
*/
void resetChannelListWithGenre(int genreId) {
if (genreId == mSelectedGenreId) {
return;
}
mFilteredChannels = mGenreChannelList.get(genreId);
mSelectedGenreId = genreId;
if (DEBUG) {
Log.d(
TAG,
"resetChannelListWithGenre: "
+ GenreItems.getCanonicalGenre(genreId)
+ " has "
+ mFilteredChannels.size()
+ " channels out of "
+ mChannels.size());
}
if (mGenreChannelList.get(mSelectedGenreId) == null) {
throw new IllegalStateException("Genre filter isn't ready.");
}
notifyChannelsUpdated();
}
/** Update the initial time range to manage. It updates program entries and genre as well. */
void updateInitialTimeRange(long startUtcMillis, long endUtcMillis) {
mStartUtcMillis = startUtcMillis;
if (endUtcMillis > mEndUtcMillis) {
mEndUtcMillis = endUtcMillis;
}
mProgramDataManager.setPrefetchTimeRange(mStartUtcMillis);
updateChannels(true);
setTimeRange(startUtcMillis, endUtcMillis);
}
/** Shifts the time range by the given time. Also makes ProgramGuide scroll the views. */
void shiftTime(long timeMillisToScroll) {
long fromUtcMillis = mFromUtcMillis + timeMillisToScroll;
long toUtcMillis = mToUtcMillis + timeMillisToScroll;
if (fromUtcMillis < mStartUtcMillis) {
toUtcMillis += mStartUtcMillis - fromUtcMillis;
fromUtcMillis = mStartUtcMillis;
}
if (toUtcMillis > mEndUtcMillis) {
fromUtcMillis -= toUtcMillis - mEndUtcMillis;
toUtcMillis = mEndUtcMillis;
}
setTimeRange(fromUtcMillis, toUtcMillis);
}
/** Returned the scrolled(shifted) time in milliseconds. */
long getShiftedTime() {
return mFromUtcMillis - mStartUtcMillis;
}
/** Returns the start time set by {@link #updateInitialTimeRange}. */
long getStartTime() {
return mStartUtcMillis;
}
/** Returns the program index of the program with {@code entryId} or -1 if not found. */
int getProgramIdIndex(long channelId, long entryId) {
List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
if (entries != null) {
for (int i = 0; i < entries.size(); i++) {
if (entries.get(i).getId() == entryId) {
return i;
}
}
}
return -1;
}
/** Returns the program index of the program at {@code time} or -1 if not found. */
int getProgramIndexAtTime(long channelId, long time) {
List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
if (entries != null) {
for (int i = 0; i < entries.size(); ++i) {
TableEntry entry = entries.get(i);
if (entry.entryStartUtcMillis <= time && time < entry.entryEndUtcMillis) {
return i;
}
}
}
return -1;
}
/** Returns the start time of currently managed time range, in UTC millisecond. */
long getFromUtcMillis() {
return mFromUtcMillis;
}
/** Returns the end time of currently managed time range, in UTC millisecond. */
long getToUtcMillis() {
return mToUtcMillis;
}
/** Returns the number of the currently managed channels. */
int getChannelCount() {
return mFilteredChannels.size();
}
/**
* Returns a {@link Channel} at a given {@code channelIndex} of the currently managed channels.
* Returns {@code null} if such a channel is not found.
*/
Channel getChannel(int channelIndex) {
if (channelIndex < 0 || channelIndex >= getChannelCount()) {
return null;
}
return mFilteredChannels.get(channelIndex);
}
/**
* Returns the index of provided {@link Channel} within the currently managed channels. Returns
* -1 if such a channel is not found.
*/
int getChannelIndex(Channel channel) {
return mFilteredChannels.indexOf(channel);
}
/**
* Returns the index of channel with {@code channelId} within the currently managed channels.
* Returns -1 if such a channel is not found.
*/
int getChannelIndex(long channelId) {
return getChannelIndex(mChannelDataManager.getChannel(channelId));
}
/**
* Returns the number of "entries", which lies within the currently managed time range, for a
* given {@code channelId}.
*/
int getTableEntryCount(long channelId) {
return mChannelIdEntriesMap.isEmpty() ? 0 : mChannelIdEntriesMap.get(channelId).size();
}
/**
* Returns an entry as {@link ProgramImpl} for a given {@code channelId} and {@code index} of
* entries within the currently managed time range. Returned {@link ProgramImpl} can be a
* placeholder (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between
* programs.
*/
TableEntry getTableEntry(long channelId, int index) {
mProgramDataManager.prefetchChannel(channelId, index);
return mChannelIdEntriesMap.get(channelId).get(index);
}
/** Returns list genre ID's which has a channel. */
List<Integer> getFilteredGenreIds() {
return mFilteredGenreIds;
}
int getSelectedGenreId() {
return mSelectedGenreId;
}
// Note that This can be happens only if program guide isn't shown
// because an user has to select channels as browsable through UI.
private void updateChannels(boolean clearPreviousTableEntries) {
if (DEBUG) Log.d(TAG, "updateChannels");
mChannels = mChannelDataManager.getBrowsableChannelList();
mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
mFilteredChannels = mChannels;
updateTableEntriesWithoutNotification(clearPreviousTableEntries);
// Channel update notification should be called after updating table entries, so that
// the listener can get the entries.
notifyChannelsUpdated();
notifyTableEntriesUpdated();
buildGenreFilters();
}
/** Sets the channel list for testing */
void setChannels(List<Channel> channels) {
mChannels = new ArrayList<>(channels);
mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
mFilteredChannels = mChannels;
buildGenreFilters();
}
private void updateTableEntries(boolean clear) {
updateTableEntriesWithoutNotification(clear);
notifyTableEntriesUpdated();
buildGenreFilters();
}
/** Updates the table entries without notifying the change. */
private void updateTableEntriesWithoutNotification(boolean clear) {
if (clear) {
mChannelIdEntriesMap.clear();
}
boolean parentalControlsEnabled =
mTvInputManagerHelper.getParentalControlSettings().isParentalControlsEnabled();
for (Channel channel : mChannels) {
long channelId = channel.getId();
// Inline the updating of the mChannelIdEntriesMap here so we can only call
// getParentalControlSettings once.
List<TableEntry> entries = createProgramEntries(channelId, parentalControlsEnabled);
mChannelIdEntriesMap.put(channelId, entries);
int size = entries.size();
if (DEBUG) {
Log.d(
TAG,
"Programs are loaded for channel "
+ channel.getId()
+ ", loaded size = "
+ size);
}
if (size == 0) {
continue;
}
TableEntry lastEntry = entries.get(size - 1);
if (mEndUtcMillis < lastEntry.entryEndUtcMillis
&& lastEntry.entryEndUtcMillis != Long.MAX_VALUE) {
mEndUtcMillis = lastEntry.entryEndUtcMillis;
}
}
if (mEndUtcMillis > mStartUtcMillis) {
for (Channel channel : mChannels) {
long channelId = channel.getId();
List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
if (entries.isEmpty()) {
entries.add(new TableEntry(channelId, mStartUtcMillis, mEndUtcMillis));
} else {
TableEntry lastEntry = entries.get(entries.size() - 1);
if (mEndUtcMillis > lastEntry.entryEndUtcMillis) {
entries.add(
new TableEntry(
channelId, lastEntry.entryEndUtcMillis, mEndUtcMillis));
} else if (lastEntry.entryEndUtcMillis == Long.MAX_VALUE) {
entries.remove(entries.size() - 1);
entries.add(
new TableEntry(
lastEntry.channelId,
lastEntry.program,
lastEntry.scheduledRecording,
lastEntry.entryStartUtcMillis,
mEndUtcMillis,
lastEntry.mIsBlocked));
}
}
}
}
}
/**
* Build genre filters based on the current programs. This categories channels by its current
* program's canonical genres and subsequent @{link resetChannelListWithGenre(int)} calls will
* reset channel list with built channel list. This is expected to be called whenever program
* guide is shown.
*/
private void buildGenreFilters() {
if (DEBUG) Log.d(TAG, "buildGenreFilters");
mGenreChannelList.clear();
for (int i = 0; i < GenreItems.getGenreCount(); i++) {
mGenreChannelList.add(new ArrayList<>());
}
for (Channel channel : mChannels) {
Program currentProgram = mProgramDataManager.getCurrentProgram(channel.getId());
if (currentProgram != null && currentProgram.getCanonicalGenres() != null) {
for (String genre : currentProgram.getCanonicalGenres()) {
mGenreChannelList.get(GenreItems.getId(genre)).add(channel);
}
}
}
mGenreChannelList.set(GenreItems.ID_ALL_CHANNELS, mChannels);
mFilteredGenreIds.clear();
mFilteredGenreIds.add(0);
for (int i = 1; i < GenreItems.getGenreCount(); i++) {
if (mGenreChannelList.get(i).size() > 0) {
mFilteredGenreIds.add(i);
}
}
mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
mFilteredChannels = mChannels;
notifyGenresUpdated();
}
@Nullable
private TableEntry getTableEntry(ScheduledRecording scheduledRecording) {
return getTableEntry(scheduledRecording.getChannelId(), scheduledRecording.getProgramId());
}
@Nullable
private TableEntry getTableEntry(long channelId, long entryId) {
if (mChannelIdEntriesMap.isEmpty()) {
return null;
}
List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
if (entries != null) {
for (TableEntry entry : entries) {
if (entry.getId() == entryId) {
return entry;
}
}
}
return null;
}
private void updateEntry(TableEntry old, TableEntry newEntry) {
List<TableEntry> entries = mChannelIdEntriesMap.get(old.channelId);
int index = entries.indexOf(old);
entries.set(index, newEntry);
notifyTableEntryUpdated(newEntry);
}
private void setTimeRange(long fromUtcMillis, long toUtcMillis) {
if (DEBUG) {
Log.d(
TAG,
"setTimeRange. {FromTime="
+ Utils.toTimeString(fromUtcMillis)
+ ", ToTime="
+ Utils.toTimeString(toUtcMillis)
+ "}");
}
if (mFromUtcMillis != fromUtcMillis || mToUtcMillis != toUtcMillis) {
mFromUtcMillis = fromUtcMillis;
mToUtcMillis = toUtcMillis;
notifyTimeRangeUpdated();
}
}
private List<TableEntry> createProgramEntries(long channelId, boolean parentalControlsEnabled) {
List<TableEntry> entries = new ArrayList<>();
boolean channelLocked =
parentalControlsEnabled && mChannelDataManager.getChannel(channelId).isLocked();
if (channelLocked) {
entries.add(new TableEntry(channelId, mStartUtcMillis, Long.MAX_VALUE, true));
} else {
long lastProgramEndTime = mStartUtcMillis;
List<Program> programs = mProgramDataManager.getPrograms(channelId, mStartUtcMillis);
for (Program program : programs) {
if (program.getChannelId() == INVALID_ID) {
// Placeholder program.
continue;
}
long programStartTime = Math.max(program.getStartTimeUtcMillis(), mStartUtcMillis);
long programEndTime = program.getEndTimeUtcMillis();
if (programStartTime > lastProgramEndTime) {
// Gap since the last program.
entries.add(new TableEntry(channelId, lastProgramEndTime, programStartTime));
lastProgramEndTime = programStartTime;
}
if (programEndTime > lastProgramEndTime) {
ScheduledRecording scheduledRecording =
mDvrDataManager == null
? null
: mDvrDataManager.getScheduledRecordingForProgramId(
program.getId());
entries.add(
new TableEntry(
channelId,
program,
scheduledRecording,
lastProgramEndTime,
programEndTime,
false));
lastProgramEndTime = programEndTime;
}
}
}
if (entries.size() > 1) {
TableEntry secondEntry = entries.get(1);
if (secondEntry.entryStartUtcMillis < mStartUtcMillis + FIRST_ENTRY_MIN_DURATION) {
// If the first entry's width doesn't have enough width, it is not good to show
// the first entry from UI perspective. So we clip it out.
entries.remove(0);
entries.set(
0,
new TableEntry(
secondEntry.channelId,
secondEntry.program,
secondEntry.scheduledRecording,
mStartUtcMillis,
secondEntry.entryEndUtcMillis,
secondEntry.mIsBlocked));
}
}
return entries;
}
private void notifyGenresUpdated() {
for (Listener listener : mListeners) {
listener.onGenresUpdated();
}
}
private void notifyChannelsUpdated() {
for (Listener listener : mListeners) {
listener.onChannelsUpdated();
}
}
private void notifyTimeRangeUpdated() {
for (Listener listener : mListeners) {
listener.onTimeRangeUpdated();
}
}
private void notifyTableEntriesUpdated() {
for (TableEntriesUpdatedListener listener : mTableEntriesUpdatedListeners) {
listener.onTableEntriesUpdated();
}
}
private void notifyTableEntryUpdated(TableEntry entry) {
for (TableEntryChangedListener listener : mTableEntryChangedListeners) {
listener.onTableEntryChanged(entry);
}
}
/**
* Entry for program guide table. An "entry" can be either an actual program or a gap between
* programs. This is needed for {@link ProgramListAdapter} because {@link
* androidx.leanback.widget.HorizontalGridView} ignores margins between items.
*/
static class TableEntry {
/** Channel ID which this entry is included. */
final long channelId;
/** Program corresponding to the entry. {@code null} means that this entry is a gap. */
final Program program;
final ScheduledRecording scheduledRecording;
/** Start time of entry in UTC milliseconds. */
final long entryStartUtcMillis;
/** End time of entry in UTC milliseconds */
final long entryEndUtcMillis;
private final boolean mIsBlocked;
private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) {
this(channelId, null, startUtcMillis, endUtcMillis, false);
}
private TableEntry(
long channelId, long startUtcMillis, long endUtcMillis, boolean blocked) {
this(channelId, null, null, startUtcMillis, endUtcMillis, blocked);
}
private TableEntry(
long channelId,
ProgramImpl program,
long entryStartUtcMillis,
long entryEndUtcMillis,
boolean isBlocked) {
this(channelId, program, null, entryStartUtcMillis, entryEndUtcMillis, isBlocked);
}
private TableEntry(
long channelId,
Program program,
ScheduledRecording scheduledRecording,
long entryStartUtcMillis,
long entryEndUtcMillis,
boolean isBlocked) {
this.channelId = channelId;
this.program = program;
this.scheduledRecording = scheduledRecording;
this.entryStartUtcMillis = entryStartUtcMillis;
this.entryEndUtcMillis = entryEndUtcMillis;
mIsBlocked = isBlocked;
}
/** A stable id useful for {@link androidx.recyclerview.widget.RecyclerView.Adapter}. */
long getId() {
// using a negative entryEndUtcMillis keeps it from conflicting with program Id
return program != null ? program.getId() : -entryEndUtcMillis;
}
/** Returns true if this is a gap. */
boolean isGap() {
return !Program.isProgramValid(program);
}
/** Returns true if this channel is blocked. */
boolean isBlocked() {
return mIsBlocked;
}
/** Returns true if this program is on the air. */
boolean isCurrentProgram() {
long current = System.currentTimeMillis();
return entryStartUtcMillis <= current && entryEndUtcMillis > current;
}
/** Returns if this program has the genre. */
boolean hasGenre(int genreId) {
return !isGap() && program.hasGenre(genreId);
}
/** Returns the width of table entry, in pixels. */
int getWidth() {
return GuideUtils.convertMillisToPixel(entryStartUtcMillis, entryEndUtcMillis);
}
@Override
public String toString() {
return "TableEntry{"
+ "hashCode="
+ hashCode()
+ ", channelId="
+ channelId
+ ", program="
+ program
+ ", startTime="
+ Utils.toTimeString(entryStartUtcMillis)
+ ", endTimeTime="
+ Utils.toTimeString(entryEndUtcMillis)
+ "}";
}
}
@VisibleForTesting
public static TableEntry createTableEntryForTest(
long channelId,
Program program,
ScheduledRecording scheduledRecording,
long entryStartUtcMillis,
long entryEndUtcMillis,
boolean isBlocked) {
return new TableEntry(
channelId,
program,
scheduledRecording,
entryStartUtcMillis,
entryEndUtcMillis,
isBlocked);
}
interface Listener {
void onGenresUpdated();
void onChannelsUpdated();
void onTimeRangeUpdated();
}
interface TableEntriesUpdatedListener {
void onTableEntriesUpdated();
}
interface TableEntryChangedListener {
void onTableEntryChanged(TableEntry entry);
}
static class ListenerAdapter implements Listener {
@Override
public void onGenresUpdated() {}
@Override
public void onChannelsUpdated() {}
@Override
public void onTimeRangeUpdated() {}
}
}