aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/guide/ProgramManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/guide/ProgramManager.java')
-rw-r--r--src/com/android/tv/guide/ProgramManager.java603
1 files changed, 603 insertions, 0 deletions
diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java
new file mode 100644
index 00000000..3310e33e
--- /dev/null
+++ b/src/com/android/tv/guide/ProgramManager.java
@@ -0,0 +1,603 @@
+/*
+ * 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.util.Log;
+
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.GenreItems;
+import com.android.tv.data.Program;
+import com.android.tv.data.ProgramDataManager;
+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.concurrent.TimeUnit;
+
+/**
+ * Manages the channels and programs for the program guide.
+ */
+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 long mStartUtcMillis;
+ private long mEndUtcMillis;
+ private long mFromUtcMillis;
+ private long mToUtcMillis;
+ private Program mSelectedProgram;
+
+ /**
+ * 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 android.support.v17.leanback.widget.HorizontalGridView} ignores margins between items.
+ */
+ public static class TableEntry {
+ /** Channel ID which this entry is included. */
+ public final long channelId;
+
+ /** Program corresponding to the entry. {@code null} means that this entry is a gap. */
+ public final Program program;
+
+ /** Start time of entry in UTC milliseconds. */
+ public final long entryStartUtcMillis;
+
+ /** End time of entry in UTC milliseconds */
+ public final long entryEndUtcMillis;
+
+ private final boolean mIsBlocked;
+
+ private TableEntry(long startUtcMillis, long endUtcMillis) {
+ this(INVALID_ID, null, startUtcMillis, endUtcMillis, false);
+ }
+
+ 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, startUtcMillis, endUtcMillis, blocked);
+ }
+
+ private TableEntry(long channelId, Program program,
+ long entryStartUtcMillis, long entryEndUtcMillis) {
+ this(channelId, program, entryStartUtcMillis, entryEndUtcMillis, false);
+ }
+
+ private TableEntry(long channelId, Program program,
+ long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked) {
+ this.channelId = channelId;
+ this.program = program;
+ this.entryStartUtcMillis = entryStartUtcMillis;
+ this.entryEndUtcMillis = entryEndUtcMillis;
+ mIsBlocked = isBlocked;
+ }
+
+ /**
+ * Returns true if this is a gap.
+ */
+ public boolean isGap() {
+ return !Program.isValid(program);
+ }
+
+ /**
+ * Returns true if this channel is blocked.
+ */
+ public boolean isBlocked() {
+ return mIsBlocked;
+ }
+
+ /**
+ * Returns true if this program is on the air.
+ */
+ public boolean isCurrentProgram() {
+ long current = System.currentTimeMillis();
+ return entryStartUtcMillis <= current && entryEndUtcMillis > current;
+ }
+
+ /**
+ * Returns if this program has the genre.
+ */
+ public boolean hasGenre(int genreId) {
+ return !isGap() && program.hasGenre(genreId);
+ }
+
+ /**
+ * Returns the width of table entry, in pixels.
+ */
+ public 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) + "}";
+ }
+ }
+
+ 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 final List<Listener> mListeners = new ArrayList<>();
+ private final List<TableEntriesUpdatedListener>
+ mTableEntriesUpdatedListeners = new ArrayList<>();
+
+ public ProgramManager(TvInputManagerHelper tvInputManagerHelper,
+ ChannelDataManager channelDataManager,
+ ProgramDataManager programDataManager) {
+ mTvInputManagerHelper = tvInputManagerHelper;
+ mChannelDataManager = channelDataManager;
+ mChannelDataManager.addListener(new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ updateChannels(true);
+ }
+
+ @Override
+ public void onChannelListUpdated() {
+ updateChannels(true);
+ }
+
+ @Override
+ public void onChannelBrowsableChanged() {
+ updateChannels(true);
+ }
+ });
+
+ mProgramDataManager = programDataManager;
+ mProgramDataManager.addListener(new ProgramDataManager.Listener() {
+ @Override
+ public void onProgramUpdated() {
+ updateTableEntries(true, true);
+ }
+ });
+ }
+
+ public void programGuideVisibilityChanged(boolean visible) {
+ mProgramDataManager.setPauseProgramUpdate(visible);
+ }
+
+ /**
+ * Add a {@link Listener}.
+ */
+ public void addListener(Listener listener) {
+ mListeners.add(listener);
+ }
+
+ /**
+ * Register a listener to be invoked when table entries are updated.
+ */
+ public void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
+ mTableEntriesUpdatedListeners.add(listener);
+ }
+
+ /**
+ * Remove a {@link Listener}.
+ */
+ public void removeListener(Listener listener) {
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Remove a previously installed table entries update listener.
+ */
+ public void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
+ mTableEntriesUpdatedListeners.remove(listener);
+ }
+
+ /**
+ * 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.
+ */
+ public void buildGenreFilters() {
+ if (DEBUG) Log.d(TAG, "buildGenreFilters");
+
+ mGenreChannelList.clear();
+ for (int i = 0; i < GenreItems.getGenreCount(); i++) {
+ mGenreChannelList.add(new ArrayList<Channel>());
+ }
+ for (Channel channel : mChannels) {
+ // TODO: Use programs in visible area instead of using current programs only.
+ 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();
+ }
+
+ /**
+ * Resets channel list with given genre.
+ * Caller should call {@link #buildGenreFilters()} prior to call this API to make
+ * This notifies channel updates to listeners.
+ */
+ public 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();
+ }
+
+ /**
+ * Returns list genre ID's which has a channel.
+ */
+ public List<Integer> getFilteredGenreIds() {
+ return mFilteredGenreIds;
+ }
+
+ public 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 notify) {
+ if (DEBUG) Log.d(TAG, "updateChannels");
+ mChannels = mChannelDataManager.getBrowsableChannelList();
+ mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
+ mFilteredChannels = mChannels;
+ if (notify) {
+ notifyChannelsUpdated();
+ }
+ updateTableEntries(notify, false);
+ }
+
+ private void updateTableEntries(boolean notify, 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.entryStartUtcMillis, mEndUtcMillis,
+ lastEntry.mIsBlocked));
+ }
+ }
+ }
+ }
+
+ if (notify) {
+ notifyTableEntriesUpdated();
+ }
+ buildGenreFilters();
+ }
+
+ 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();
+ }
+ }
+
+ /**
+ * Returns the start time of currently managed time range, in UTC millisecond.
+ */
+ public long getFromUtcMillis() {
+ return mFromUtcMillis;
+ }
+
+ /**
+ * Returns the end time of currently managed time range, in UTC millisecond.
+ */
+ public long getToUtcMillis() {
+ return mToUtcMillis;
+ }
+
+ /**
+ * Set the initial time range to manage.
+ */
+ public void setInitialTimeRange(long startUtcMillis, long endUtcMillis) {
+ mStartUtcMillis = startUtcMillis;
+ if (endUtcMillis > mEndUtcMillis) {
+ mEndUtcMillis = endUtcMillis;
+ }
+ updateChannels(true);
+
+ mProgramDataManager.setPrefetchTimeRange(mStartUtcMillis);
+
+ // Need to clear when the UI starts.
+ updateTableEntries(true, true);
+ setTimeRange(startUtcMillis, endUtcMillis);
+ }
+
+ 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();
+ }
+ }
+
+ /**
+ * Returns the number of the currently managed channels.
+ */
+ public 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.
+ */
+ public 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.
+ */
+ public int getChannelIndex(Channel channel) {
+ return mFilteredChannels.indexOf(channel);
+ }
+
+ /**
+ * Returns the number of "entries", which lies within the currently managed time range, for a
+ * given {@code channelId}.
+ */
+ public int getTableEntryCount(long channelId) {
+ return mChannelIdEntriesMap.get(channelId).size();
+ }
+
+ /**
+ * Returns an entry as {@link Program} for a given {@code channelId} and {@code index} of
+ * entries within the currently managed time range. Returned {@link Program} can be a dummy one
+ * (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between programs.
+ */
+ public TableEntry getTableEntry(long channelId, int index) {
+ return mChannelIdEntriesMap.get(channelId).get(index);
+ }
+
+ 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) {
+ // Dummy 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) {
+ entries.add(new TableEntry(channelId, program, lastProgramEndTime,
+ programEndTime));
+ 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,
+ mStartUtcMillis, secondEntry.entryEndUtcMillis));
+ }
+ }
+ return entries;
+ }
+
+ /**
+ * Get the currently selected channel.
+ */
+ public Channel getSelectedChannel() {
+ return mChannelDataManager.getChannel(mSelectedProgram.getChannelId());
+ }
+
+ /**
+ * Get the currently selected program.
+ */
+ public Program getSelectedProgram() {
+ return mSelectedProgram;
+ }
+
+ public interface Listener {
+ void onGenresUpdated();
+ void onChannelsUpdated();
+ void onTimeRangeUpdated();
+ }
+
+ public interface TableEntriesUpdatedListener {
+ void onTableEntriesUpdated();
+ }
+
+ public static class ListenerAdapter implements Listener {
+ @Override
+ public void onGenresUpdated() { }
+
+ @Override
+ public void onChannelsUpdated() { }
+
+ @Override
+ public void onTimeRangeUpdated() { }
+ }
+
+ /**
+ * Shifts the time range by the given time. Also makes ProgramGuide scroll the views.
+ */
+ public void shiftTime(long timeMillisToScroll) {
+ long fromUtcMillis = mFromUtcMillis + timeMillisToScroll;
+ long toUtcMillis = mToUtcMillis + timeMillisToScroll;
+ if (fromUtcMillis < mStartUtcMillis) {
+ fromUtcMillis = mStartUtcMillis;
+ toUtcMillis += mStartUtcMillis - fromUtcMillis;
+ }
+ if (toUtcMillis > mEndUtcMillis) {
+ fromUtcMillis -= toUtcMillis - mEndUtcMillis;
+ toUtcMillis = mEndUtcMillis;
+ }
+ setTimeRange(fromUtcMillis, toUtcMillis);
+ }
+
+ /**
+ * Returned the scrolled(shifted) time in milliseconds.
+ */
+ public long getShiftedTime() {
+ return mFromUtcMillis - mStartUtcMillis;
+ }
+
+ /**
+ * Returns the start time set by {@link #setInitialTimeRange}.
+ */
+ public long getStartTime() {
+ return mStartUtcMillis;
+ }
+
+ /**
+ * Returns the program index of the program at {@code time}.
+ */
+ public int getProgramIndex(long channelId, long time) {
+ List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
+ for (int i = 0; i < entries.size(); ++i) {
+ TableEntry entry = entries.get(i);
+ if (entry.entryStartUtcMillis <= time
+ && time < entry.entryEndUtcMillis) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}