| /* |
| * 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.util.ArraySet; |
| 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.dvr.DvrDataManager; |
| import com.android.tv.dvr.DvrScheduleManager; |
| import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener; |
| import com.android.tv.dvr.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; |
| |
| /** |
| * 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; |
| |
| public final ScheduledRecording scheduledRecording; |
| |
| /** 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 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, Program 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 android.support.v7.widget.RecyclerView.Adapter}. |
| */ |
| public 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. |
| */ |
| 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 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.Listener mProgramDataManagerListener = |
| new ProgramDataManager.Listener() { |
| @Override |
| public void onProgramUpdated() { |
| updateTableEntries(true); |
| } |
| }; |
| |
| 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; |
| } |
| |
| public void programGuideVisibilityChanged(boolean visible) { |
| mProgramDataManager.setPauseProgramUpdate(visible); |
| if (visible) { |
| mChannelDataManager.addListener(mChannelDataManagerListener); |
| mProgramDataManager.addListener(mProgramDataManagerListener); |
| if (mDvrDataManager != null) { |
| if (!mDvrDataManager.isDvrScheduleLoadFinished()) { |
| mDvrDataManager.addDvrScheduleLoadFinishedListener(mDvrLoadedListener); |
| } |
| mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener); |
| } |
| if (mDvrScheduleManager != null) { |
| mDvrScheduleManager.addOnConflictStateChangeListener( |
| mOnConflictStateChangeListener); |
| } |
| } else { |
| mChannelDataManager.removeListener(mChannelDataManagerListener); |
| mProgramDataManager.removeListener(mProgramDataManagerListener); |
| if (mDvrDataManager != null) { |
| mDvrDataManager.removeDvrScheduleLoadFinishedListener(mDvrLoadedListener); |
| mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); |
| } |
| if (mDvrScheduleManager != null) { |
| mDvrScheduleManager.removeOnConflictStateChangeListener( |
| mOnConflictStateChangeListener); |
| } |
| } |
| } |
| |
| /** |
| * Adds a {@link Listener}. |
| */ |
| public void addListener(Listener listener) { |
| mListeners.add(listener); |
| } |
| |
| /** |
| * Registers a listener to be invoked when table entries are updated. |
| */ |
| public void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { |
| mTableEntriesUpdatedListeners.add(listener); |
| } |
| |
| /** |
| * Registers a listener to be invoked when a table entry is changed. |
| */ |
| public void addTableEntryChangedListener(TableEntryChangedListener listener) { |
| mTableEntryChangedListeners.add(listener); |
| } |
| |
| /** |
| * Removes a {@link Listener}. |
| */ |
| public void removeListener(Listener listener) { |
| mListeners.remove(listener); |
| } |
| |
| /** |
| * Removes a previously installed table entries update listener. |
| */ |
| public void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { |
| mTableEntriesUpdatedListeners.remove(listener); |
| } |
| |
| /** |
| * Removes a previously installed table entry changed listener. |
| */ |
| public void removeTableEntryChangedListener(TableEntryChangedListener listener) { |
| mTableEntryChangedListeners.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<>()); |
| } |
| 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 clearPreviousTableEntries) { |
| if (DEBUG) Log.d(TAG, "updateChannels"); |
| mChannels = mChannelDataManager.getBrowsableChannelList(); |
| mSelectedGenreId = GenreItems.ID_ALL_CHANNELS; |
| mFilteredChannels = mChannels; |
| notifyChannelsUpdated(); |
| updateTableEntries(clearPreviousTableEntries); |
| } |
| |
| private void updateTableEntries(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)); |
| } |
| } |
| } |
| } |
| |
| 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(); |
| } |
| } |
| |
| private void notifyTableEntryUpdated(TableEntry entry) { |
| for (TableEntryChangedListener listener : mTableEntryChangedListeners) { |
| listener.onTableEntryChanged(entry); |
| } |
| } |
| |
| 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); |
| } |
| |
| @Nullable |
| private TableEntry getTableEntry(ScheduledRecording scheduledRecording) { |
| return getTableEntry(scheduledRecording.getChannelId(), scheduledRecording.getProgramId()); |
| } |
| |
| @Nullable |
| private TableEntry getTableEntry(long channelId, long entryId) { |
| List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); |
| if (entries != null) { |
| for (TableEntry entry : entries) { |
| if (entry.getId() == entryId) { |
| return entry; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * Update the initial time range to manage. It updates program entries and genre as well. |
| */ |
| public void updateInitialTimeRange(long startUtcMillis, long endUtcMillis) { |
| mStartUtcMillis = startUtcMillis; |
| if (endUtcMillis > mEndUtcMillis) { |
| mEndUtcMillis = endUtcMillis; |
| } |
| |
| mProgramDataManager.setPrefetchTimeRange(mStartUtcMillis); |
| updateChannels(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 index of channel with {@code channelId} within the currently managed channels. |
| * Returns -1 if such a channel is not found. |
| */ |
| public 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}. |
| */ |
| 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) { |
| 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; |
| } |
| |
| public interface Listener { |
| void onGenresUpdated(); |
| void onChannelsUpdated(); |
| void onTimeRangeUpdated(); |
| } |
| |
| public interface TableEntriesUpdatedListener { |
| void onTableEntriesUpdated(); |
| } |
| |
| public interface TableEntryChangedListener { |
| void onTableEntryChanged(TableEntry entry); |
| } |
| |
| 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 #updateInitialTimeRange}. |
| */ |
| public long getStartTime() { |
| return mStartUtcMillis; |
| } |
| |
| /** |
| * Returns the program index of the program with {@code entryId} or -1 if not found. |
| */ |
| public 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. |
| */ |
| public int getProgramIndexAtTime(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; |
| } |
| } |