Home | History | Annotate | Download | only in guide
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv.guide;
     18 
     19 import android.support.annotation.MainThread;
     20 import android.support.annotation.Nullable;
     21 import android.util.ArraySet;
     22 import android.util.Log;
     23 
     24 import com.android.tv.data.Channel;
     25 import com.android.tv.data.ChannelDataManager;
     26 import com.android.tv.data.GenreItems;
     27 import com.android.tv.data.Program;
     28 import com.android.tv.data.ProgramDataManager;
     29 import com.android.tv.dvr.DvrDataManager;
     30 import com.android.tv.dvr.ScheduledRecording;
     31 import com.android.tv.util.TvInputManagerHelper;
     32 import com.android.tv.util.Utils;
     33 
     34 import java.util.ArrayList;
     35 import java.util.HashMap;
     36 import java.util.List;
     37 import java.util.Map;
     38 import java.util.Set;
     39 import java.util.concurrent.TimeUnit;
     40 
     41 /**
     42  * Manages the channels and programs for the program guide.
     43  */
     44 @MainThread
     45 public class ProgramManager {
     46     private static final String TAG = "ProgramManager";
     47     private static final boolean DEBUG = false;
     48 
     49     /**
     50      * If the first entry's visible duration is shorter than this value, we clip the entry out.
     51      * Note: If this value is larger than 1 min, it could cause mismatches between the entry's
     52      * position and detailed view's time range.
     53      */
     54     static final long FIRST_ENTRY_MIN_DURATION = TimeUnit.MINUTES.toMillis(1);
     55 
     56     private static final long INVALID_ID = -1;
     57 
     58     private final TvInputManagerHelper mTvInputManagerHelper;
     59     private final ChannelDataManager mChannelDataManager;
     60     private final ProgramDataManager mProgramDataManager;
     61     private final DvrDataManager mDvrDataManager;  // Only set if DVR is enabled
     62 
     63     private long mStartUtcMillis;
     64     private long mEndUtcMillis;
     65     private long mFromUtcMillis;
     66     private long mToUtcMillis;
     67     private Program mSelectedProgram;
     68 
     69     /**
     70      * Entry for program guide table. An "entry" can be either an actual program or a gap between
     71      * programs. This is needed for {@link ProgramListAdapter} because
     72      * {@link android.support.v17.leanback.widget.HorizontalGridView} ignores margins between items.
     73      */
     74     public static class TableEntry {
     75         /** Channel ID which this entry is included. */
     76         public final long channelId;
     77 
     78         /** Program corresponding to the entry. {@code null} means that this entry is a gap. */
     79         public final Program program;
     80 
     81         public final ScheduledRecording scheduledRecording;
     82 
     83         /** Start time of entry in UTC milliseconds. */
     84         public final long entryStartUtcMillis;
     85 
     86         /** End time of entry in UTC milliseconds */
     87         public final long entryEndUtcMillis;
     88 
     89         private final boolean mIsBlocked;
     90 
     91         private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) {
     92             this(channelId, null, startUtcMillis, endUtcMillis, false);
     93         }
     94 
     95         private TableEntry(long channelId, long startUtcMillis, long endUtcMillis,
     96                 boolean blocked) {
     97             this(channelId, null, null, startUtcMillis, endUtcMillis, blocked);
     98         }
     99 
    100         private TableEntry(long channelId, Program program, long entryStartUtcMillis,
    101                 long entryEndUtcMillis, boolean isBlocked) {
    102             this(channelId, program, null, entryStartUtcMillis, entryEndUtcMillis, isBlocked);
    103         }
    104 
    105         private TableEntry(long channelId, Program program, ScheduledRecording scheduledRecording,
    106                 long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked) {
    107             this.channelId = channelId;
    108             this.program = program;
    109             this.scheduledRecording = scheduledRecording;
    110             this.entryStartUtcMillis = entryStartUtcMillis;
    111             this.entryEndUtcMillis = entryEndUtcMillis;
    112             mIsBlocked = isBlocked;
    113         }
    114 
    115         /**
    116          * A stable id useful for {@link android.support.v7.widget.RecyclerView.Adapter}.
    117          */
    118         public long getId() {
    119             // using a negative entryEndUtcMillis keeps it from conflicting with program Id
    120             return program != null ? program.getId() : -entryEndUtcMillis;
    121         }
    122 
    123         /**
    124          * Returns true if this is a gap.
    125          */
    126         public boolean isGap() {
    127             return !Program.isValid(program);
    128         }
    129 
    130         /**
    131          * Returns true if this channel is blocked.
    132          */
    133         public boolean isBlocked() {
    134             return mIsBlocked;
    135         }
    136 
    137         /**
    138          * Returns true if this program is on the air.
    139          */
    140         public boolean isCurrentProgram() {
    141             long current = System.currentTimeMillis();
    142             return entryStartUtcMillis <= current && entryEndUtcMillis > current;
    143         }
    144 
    145         /**
    146          * Returns if this program has the genre.
    147          */
    148         public boolean hasGenre(int genreId) {
    149             return !isGap() && program.hasGenre(genreId);
    150         }
    151 
    152         /**
    153          * Returns the width of table entry, in pixels.
    154          */
    155         public int getWidth() {
    156             return GuideUtils.convertMillisToPixel(entryStartUtcMillis, entryEndUtcMillis);
    157         }
    158 
    159         @Override
    160         public String toString() {
    161             return "TableEntry{"
    162                     + "hashCode=" + hashCode()
    163                     + ", channelId=" + channelId
    164                     + ", program=" + program
    165                     + ", startTime=" + Utils.toTimeString(entryStartUtcMillis)
    166                     + ", endTimeTime=" + Utils.toTimeString(entryEndUtcMillis) + "}";
    167         }
    168     }
    169 
    170     private List<Channel> mChannels = new ArrayList<>();
    171     private final Map<Long, List<TableEntry>> mChannelIdEntriesMap = new HashMap<>();
    172     private final List<List<Channel>> mGenreChannelList = new ArrayList<>();
    173     private final List<Integer> mFilteredGenreIds = new ArrayList<>();
    174 
    175     // Position of selected genre to filter channel list.
    176     private int mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
    177     // Channel list after applying genre filter.
    178     // Should be matched with mSelectedGenreId always.
    179     private List<Channel> mFilteredChannels = mChannels;
    180 
    181     private final Set<Listener> mListeners = new ArraySet<>();
    182     private final Set<TableEntriesUpdatedListener> mTableEntriesUpdatedListeners = new ArraySet<>();
    183 
    184     private final Set<TableEntryChangedListener> mTableEntryChangedListeners = new ArraySet<>();
    185 
    186     private final ChannelDataManager.Listener mChannelDataManagerListener =
    187             new ChannelDataManager.Listener() {
    188                 @Override
    189                 public void onLoadFinished() {
    190                     updateChannels(true, false);
    191                 }
    192 
    193                 @Override
    194                 public void onChannelListUpdated() {
    195                     updateChannels(true, false);
    196                 }
    197 
    198                 @Override
    199                 public void onChannelBrowsableChanged() {
    200                     updateChannels(true, false);
    201                 }
    202             };
    203 
    204     private final ProgramDataManager.Listener mProgramDataManagerListener =
    205             new ProgramDataManager.Listener() {
    206                 @Override
    207                 public void onProgramUpdated() {
    208                     updateTableEntries(true, true);
    209                 }
    210             };
    211 
    212     private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener =
    213             new DvrDataManager.ScheduledRecordingListener() {
    214         @Override
    215         public void onScheduledRecordingAdded(ScheduledRecording scheduledRecording) {
    216             TableEntry oldEntry = getTableEntry(scheduledRecording);
    217             if (oldEntry != null) {
    218                 TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program,
    219                         scheduledRecording, oldEntry.entryStartUtcMillis,
    220                         oldEntry.entryEndUtcMillis, oldEntry.isBlocked());
    221                 updateEntry(oldEntry, newEntry);
    222             }
    223         }
    224 
    225         @Override
    226         public void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording) {
    227             TableEntry oldEntry = getTableEntry(scheduledRecording);
    228             if (oldEntry != null) {
    229                 TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program, null,
    230                         oldEntry.entryStartUtcMillis, oldEntry.entryEndUtcMillis,
    231                         oldEntry.isBlocked());
    232                 updateEntry(oldEntry, newEntry);
    233             }
    234         }
    235 
    236         @Override
    237         public void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording) {
    238             TableEntry oldEntry = getTableEntry(scheduledRecording);
    239             if (oldEntry != null) {
    240                 TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program,
    241                         scheduledRecording, oldEntry.entryStartUtcMillis,
    242                         oldEntry.entryEndUtcMillis, oldEntry.isBlocked());
    243                 updateEntry(oldEntry, newEntry);
    244             }
    245         }
    246     };
    247 
    248     public ProgramManager(TvInputManagerHelper tvInputManagerHelper,
    249             ChannelDataManager channelDataManager, ProgramDataManager programDataManager,
    250             @Nullable DvrDataManager dvrDataManager) {
    251         mTvInputManagerHelper = tvInputManagerHelper;
    252         mChannelDataManager = channelDataManager;
    253         mProgramDataManager = programDataManager;
    254         mDvrDataManager = dvrDataManager;
    255     }
    256 
    257     public void programGuideVisibilityChanged(boolean visible) {
    258         mProgramDataManager.setPauseProgramUpdate(visible);
    259         if (visible) {
    260             mChannelDataManager.addListener(mChannelDataManagerListener);
    261             mProgramDataManager.addListener(mProgramDataManagerListener);
    262             if (mDvrDataManager != null) {
    263                 mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener);
    264             }
    265         } else {
    266             mChannelDataManager.removeListener(mChannelDataManagerListener);
    267             mProgramDataManager.removeListener(mProgramDataManagerListener);
    268             if (mDvrDataManager != null) {
    269                 mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
    270             }
    271         }
    272     }
    273 
    274     /**
    275      * Adds a {@link Listener}.
    276      */
    277     public void addListener(Listener listener) {
    278         mListeners.add(listener);
    279     }
    280 
    281     /**
    282      * Registers a listener to be invoked when table entries are updated.
    283      */
    284     public void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
    285         mTableEntriesUpdatedListeners.add(listener);
    286     }
    287 
    288     /**
    289      * Registers a listener to be invoked when a table entry is changed.
    290      */
    291     public void addTableEntryChangedListener(TableEntryChangedListener listener) {
    292         mTableEntryChangedListeners.add(listener);
    293     }
    294 
    295     /**
    296      * Removes a {@link Listener}.
    297      */
    298     public void removeListener(Listener listener) {
    299         mListeners.remove(listener);
    300     }
    301 
    302     /**
    303      * Removes a previously installed table entries update listener.
    304      */
    305     public void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
    306         mTableEntriesUpdatedListeners.remove(listener);
    307     }
    308 
    309     /**
    310      * Removes a previously installed table entry changed listener.
    311      */
    312     public void removeTableEntryChangedListener(TableEntryChangedListener listener) {
    313         mTableEntryChangedListeners.remove(listener);
    314     }
    315 
    316     /**
    317      * Build genre filters based on the current programs.
    318      * This categories channels by its current program's canonical genres
    319      * and subsequent @{link resetChannelListWithGenre(int)} calls will reset channel list
    320      * with built channel list.
    321      * This is expected to be called whenever program guide is shown.
    322      */
    323     public void buildGenreFilters() {
    324         if (DEBUG) Log.d(TAG, "buildGenreFilters");
    325 
    326         mGenreChannelList.clear();
    327         for (int i = 0; i < GenreItems.getGenreCount(); i++) {
    328             mGenreChannelList.add(new ArrayList<Channel>());
    329         }
    330         for (Channel channel : mChannels) {
    331             // TODO: Use programs in visible area instead of using current programs only.
    332             Program currentProgram = mProgramDataManager.getCurrentProgram(channel.getId());
    333             if (currentProgram != null && currentProgram.getCanonicalGenres() != null) {
    334                 for (String genre : currentProgram.getCanonicalGenres()) {
    335                     mGenreChannelList.get(GenreItems.getId(genre)).add(channel);
    336                 }
    337             }
    338         }
    339         mGenreChannelList.set(GenreItems.ID_ALL_CHANNELS, mChannels);
    340         mFilteredGenreIds.clear();
    341         mFilteredGenreIds.add(0);
    342         for (int i = 1; i < GenreItems.getGenreCount(); i++) {
    343             if (mGenreChannelList.get(i).size() > 0) {
    344                 mFilteredGenreIds.add(i);
    345             }
    346         }
    347         mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
    348         mFilteredChannels = mChannels;
    349         notifyGenresUpdated();
    350     }
    351 
    352     /**
    353      * Resets channel list with given genre.
    354      * Caller should call {@link #buildGenreFilters()} prior to call this API to make
    355      * This notifies channel updates to listeners.
    356      */
    357     public void resetChannelListWithGenre(int genreId) {
    358         if (genreId == mSelectedGenreId) {
    359             return;
    360         }
    361         mFilteredChannels = mGenreChannelList.get(genreId);
    362         mSelectedGenreId = genreId;
    363         if (DEBUG) {
    364             Log.d(TAG, "resetChannelListWithGenre: " + GenreItems.getCanonicalGenre(genreId)
    365                     + " has " + mFilteredChannels.size() + " channels out of " + mChannels.size());
    366         }
    367         if (mGenreChannelList.get(mSelectedGenreId) == null) {
    368             throw new IllegalStateException("Genre filter isn't ready.");
    369         }
    370         notifyChannelsUpdated();
    371     }
    372 
    373     /**
    374      * Returns list genre ID's which has a channel.
    375      */
    376     public List<Integer> getFilteredGenreIds() {
    377         return mFilteredGenreIds;
    378     }
    379 
    380     public int getSelectedGenreId() {
    381         return mSelectedGenreId;
    382     }
    383 
    384     // Note that This can be happens only if program guide isn't shown
    385     // because an user has to select channels as browsable through UI.
    386     private void updateChannels(boolean notify, boolean clearPreviousTableEntries) {
    387         if (DEBUG) Log.d(TAG, "updateChannels");
    388         mChannels = mChannelDataManager.getBrowsableChannelList();
    389         mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
    390         mFilteredChannels = mChannels;
    391         if (notify) {
    392             notifyChannelsUpdated();
    393         }
    394         updateTableEntries(notify, clearPreviousTableEntries);
    395     }
    396 
    397     private void updateTableEntries(boolean notify, boolean clear) {
    398         if (clear) {
    399             mChannelIdEntriesMap.clear();
    400         }
    401         boolean parentalControlsEnabled = mTvInputManagerHelper.getParentalControlSettings()
    402                 .isParentalControlsEnabled();
    403         for (Channel channel : mChannels) {
    404             long channelId = channel.getId();
    405             // Inline the updating of the mChannelIdEntriesMap here so we can only call
    406             // getParentalControlSettings once.
    407             List<TableEntry> entries = createProgramEntries(channelId, parentalControlsEnabled);
    408             mChannelIdEntriesMap.put(channelId, entries);
    409 
    410             int size = entries.size();
    411             if (DEBUG) {
    412                 Log.d(TAG, "Programs are loaded for channel " + channel.getId()
    413                         + ", loaded size = " + size);
    414             }
    415             if (size == 0) {
    416                 continue;
    417             }
    418             TableEntry lastEntry = entries.get(size - 1);
    419             if (mEndUtcMillis < lastEntry.entryEndUtcMillis
    420                     && lastEntry.entryEndUtcMillis != Long.MAX_VALUE) {
    421                 mEndUtcMillis = lastEntry.entryEndUtcMillis;
    422             }
    423         }
    424         if (mEndUtcMillis > mStartUtcMillis) {
    425             for (Channel channel : mChannels) {
    426                 long channelId = channel.getId();
    427                 List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
    428                 if (entries.isEmpty()) {
    429                     entries.add(new TableEntry(channelId, mStartUtcMillis, mEndUtcMillis));
    430                 } else {
    431                     TableEntry lastEntry = entries.get(entries.size() - 1);
    432                     if (mEndUtcMillis > lastEntry.entryEndUtcMillis) {
    433                         entries.add(new TableEntry(channelId, lastEntry.entryEndUtcMillis,
    434                                 mEndUtcMillis));
    435                     } else if (lastEntry.entryEndUtcMillis == Long.MAX_VALUE) {
    436                         entries.remove(entries.size() - 1);
    437                         entries.add(new TableEntry(lastEntry.channelId, lastEntry.program,
    438                                 lastEntry.scheduledRecording,
    439                                 lastEntry.entryStartUtcMillis, mEndUtcMillis,
    440                                 lastEntry.mIsBlocked));
    441                     }
    442                 }
    443             }
    444         }
    445 
    446         if (notify) {
    447             notifyTableEntriesUpdated();
    448         }
    449         buildGenreFilters();
    450     }
    451 
    452     private void notifyGenresUpdated() {
    453         for (Listener listener : mListeners) {
    454             listener.onGenresUpdated();
    455         }
    456     }
    457 
    458     private void notifyChannelsUpdated() {
    459         for (Listener listener : mListeners) {
    460             listener.onChannelsUpdated();
    461         }
    462     }
    463 
    464     private void notifyTimeRangeUpdated() {
    465         for (Listener listener : mListeners) {
    466             listener.onTimeRangeUpdated();
    467         }
    468     }
    469 
    470     private void notifyTableEntriesUpdated() {
    471         for (TableEntriesUpdatedListener listener : mTableEntriesUpdatedListeners) {
    472             listener.onTableEntriesUpdated();
    473         }
    474     }
    475 
    476     private void notifyTableEntryUpdated(TableEntry entry) {
    477         for (TableEntryChangedListener listener : mTableEntryChangedListeners) {
    478             listener.onTableEntryChanged(entry);
    479         }
    480     }
    481 
    482     private void updateEntry(TableEntry old, TableEntry newEntry) {
    483         List<TableEntry> entries = mChannelIdEntriesMap.get(old.channelId);
    484         int index = entries.indexOf(old);
    485         entries.set(index, newEntry);
    486         notifyTableEntryUpdated(newEntry);
    487     }
    488 
    489     @Nullable
    490     private TableEntry getTableEntry(ScheduledRecording scheduledRecording) {
    491         return getTableEntry(scheduledRecording.getChannelId(), scheduledRecording.getProgramId());
    492     }
    493 
    494     @Nullable
    495     private TableEntry getTableEntry(long channelId, long entryId) {
    496         List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
    497         if (entries != null) {
    498             for (TableEntry entry : entries) {
    499                 if (entry.getId() == entryId) {
    500                     return entry;
    501                 }
    502             }
    503         }
    504         return null;
    505     }
    506 
    507     /**
    508      * Returns the start time of currently managed time range, in UTC millisecond.
    509      */
    510     public long getFromUtcMillis() {
    511         return mFromUtcMillis;
    512     }
    513 
    514     /**
    515      * Returns the end time of currently managed time range, in UTC millisecond.
    516      */
    517     public long getToUtcMillis() {
    518         return mToUtcMillis;
    519     }
    520 
    521     /**
    522      * Update the initial time range to manage. It updates program entries and genre as well.
    523      */
    524     public void updateInitialTimeRange(long startUtcMillis, long endUtcMillis) {
    525         mStartUtcMillis = startUtcMillis;
    526         if (endUtcMillis > mEndUtcMillis) {
    527             mEndUtcMillis = endUtcMillis;
    528         }
    529 
    530         mProgramDataManager.setPrefetchTimeRange(mStartUtcMillis);
    531         updateChannels(true, true);
    532         setTimeRange(startUtcMillis, endUtcMillis);
    533     }
    534 
    535     private void setTimeRange(long fromUtcMillis, long toUtcMillis) {
    536         if (DEBUG) {
    537             Log.d(TAG, "setTimeRange. {FromTime="
    538                     + Utils.toTimeString(fromUtcMillis) + ", ToTime="
    539                     + Utils.toTimeString(toUtcMillis) + "}");
    540         }
    541         if (mFromUtcMillis != fromUtcMillis || mToUtcMillis != toUtcMillis) {
    542             mFromUtcMillis = fromUtcMillis;
    543             mToUtcMillis = toUtcMillis;
    544             notifyTimeRangeUpdated();
    545         }
    546     }
    547 
    548     /**
    549      * Returns the number of the currently managed channels.
    550      */
    551     public int getChannelCount() {
    552         return mFilteredChannels.size();
    553     }
    554 
    555     /**
    556      * Returns a {@link Channel} at a given {@code channelIndex} of the currently managed channels.
    557      * Returns {@code null} if such a channel is not found.
    558      */
    559     public Channel getChannel(int channelIndex) {
    560         if (channelIndex < 0 || channelIndex >= getChannelCount()) {
    561             return null;
    562         }
    563         return mFilteredChannels.get(channelIndex);
    564     }
    565 
    566     /**
    567      * Returns the index of provided {@link Channel} within the currently managed channels.
    568      * Returns -1 if such a channel is not found.
    569      */
    570     public int getChannelIndex(Channel channel) {
    571         return mFilteredChannels.indexOf(channel);
    572     }
    573 
    574     /**
    575      * Returns the index of channel with  {@code channelId} within the currently managed channels.
    576      * Returns -1 if such a channel is not found.
    577      */
    578     public int getChannelIndex(long channelId) {
    579         return getChannelIndex(mChannelDataManager.getChannel(channelId));
    580     }
    581 
    582     /**
    583      * Returns the number of "entries", which lies within the currently managed time range, for a
    584      * given {@code channelId}.
    585      */
    586     public int getTableEntryCount(long channelId) {
    587         return mChannelIdEntriesMap.get(channelId).size();
    588     }
    589 
    590     /**
    591      * Returns an entry as {@link Program} for a given {@code channelId} and {@code index} of
    592      * entries within the currently managed time range. Returned {@link Program} can be a dummy one
    593      * (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between programs.
    594      */
    595     public TableEntry getTableEntry(long channelId, int index) {
    596         return mChannelIdEntriesMap.get(channelId).get(index);
    597     }
    598 
    599     private List<TableEntry> createProgramEntries(long channelId, boolean parentalControlsEnabled) {
    600         List<TableEntry> entries = new ArrayList<>();
    601         boolean channelLocked = parentalControlsEnabled
    602                 && mChannelDataManager.getChannel(channelId).isLocked();
    603         if (channelLocked) {
    604             entries.add(new TableEntry(channelId, mStartUtcMillis, Long.MAX_VALUE, true));
    605         } else {
    606             long lastProgramEndTime = mStartUtcMillis;
    607             List<Program> programs = mProgramDataManager.getPrograms(channelId, mStartUtcMillis);
    608             for (Program program : programs) {
    609                 if (program.getChannelId() == INVALID_ID) {
    610                     // Dummy program.
    611                     continue;
    612                 }
    613                 long programStartTime = Math.max(program.getStartTimeUtcMillis(),
    614                         mStartUtcMillis);
    615                 long programEndTime = program.getEndTimeUtcMillis();
    616                 if (programStartTime > lastProgramEndTime) {
    617                     // Gap since the last program.
    618                     entries.add(new TableEntry(channelId, lastProgramEndTime,
    619                             programStartTime));
    620                     lastProgramEndTime = programStartTime;
    621                 }
    622                 if (programEndTime > lastProgramEndTime) {
    623                     ScheduledRecording scheduledRecording = mDvrDataManager == null ? null
    624                             : mDvrDataManager.getScheduledRecordingForProgramId(program.getId());
    625                     entries.add(new TableEntry(channelId, program, scheduledRecording,
    626                             lastProgramEndTime, programEndTime, false));
    627                     lastProgramEndTime = programEndTime;
    628                 }
    629             }
    630         }
    631 
    632         if (entries.size() > 1) {
    633             TableEntry secondEntry = entries.get(1);
    634             if (secondEntry.entryStartUtcMillis < mStartUtcMillis + FIRST_ENTRY_MIN_DURATION) {
    635                 // If the first entry's width doesn't have enough width, it is not good to show
    636                 // the first entry from UI perspective. So we clip it out.
    637                 entries.remove(0);
    638                 entries.set(0, new TableEntry(secondEntry.channelId, secondEntry.program,
    639                         secondEntry.scheduledRecording, mStartUtcMillis,
    640                         secondEntry.entryEndUtcMillis, secondEntry.mIsBlocked));
    641             }
    642         }
    643         return entries;
    644     }
    645 
    646     /**
    647      * Get the currently selected channel.
    648      */
    649     public Channel getSelectedChannel() {
    650         return mChannelDataManager.getChannel(mSelectedProgram.getChannelId());
    651     }
    652 
    653     /**
    654      * Get the currently selected program.
    655      */
    656     public Program getSelectedProgram() {
    657         return mSelectedProgram;
    658     }
    659 
    660     public interface Listener {
    661         void onGenresUpdated();
    662         void onChannelsUpdated();
    663         void onTimeRangeUpdated();
    664     }
    665 
    666     public interface TableEntriesUpdatedListener {
    667         void onTableEntriesUpdated();
    668     }
    669 
    670     public interface TableEntryChangedListener {
    671         void onTableEntryChanged(TableEntry entry);
    672     }
    673 
    674     public static class ListenerAdapter implements Listener {
    675         @Override
    676         public void onGenresUpdated() { }
    677 
    678         @Override
    679         public void onChannelsUpdated() { }
    680 
    681         @Override
    682         public void onTimeRangeUpdated() { }
    683     }
    684 
    685     /**
    686      * Shifts the time range by the given time. Also makes ProgramGuide scroll the views.
    687      */
    688     public void shiftTime(long timeMillisToScroll) {
    689         long fromUtcMillis = mFromUtcMillis + timeMillisToScroll;
    690         long toUtcMillis = mToUtcMillis + timeMillisToScroll;
    691         if (fromUtcMillis < mStartUtcMillis) {
    692             fromUtcMillis = mStartUtcMillis;
    693             toUtcMillis += mStartUtcMillis - fromUtcMillis;
    694         }
    695         if (toUtcMillis > mEndUtcMillis) {
    696             fromUtcMillis -= toUtcMillis - mEndUtcMillis;
    697             toUtcMillis = mEndUtcMillis;
    698         }
    699         setTimeRange(fromUtcMillis, toUtcMillis);
    700     }
    701 
    702     /**
    703      * Returned the scrolled(shifted) time in milliseconds.
    704      */
    705     public long getShiftedTime() {
    706         return mFromUtcMillis - mStartUtcMillis;
    707     }
    708 
    709     /**
    710      * Returns the start time set by {@link #updateInitialTimeRange}.
    711      */
    712     public long getStartTime() {
    713         return mStartUtcMillis;
    714     }
    715 
    716     /**
    717      * Returns the program index of the program with {@code entryId} or -1 if not found.
    718      */
    719     public int getProgramIdIndex(long channelId, long entryId) {
    720         List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
    721         if (entries != null) {
    722             for (int i = 0; i < entries.size(); i++) {
    723                 if (entries.get(i).getId() == entryId) {
    724                     return i;
    725                 }
    726             }
    727         }
    728         return -1;
    729     }
    730 
    731     /**
    732      * Returns the program index of the program at {@code time} or -1 if not found.
    733      */
    734     public int getProgramIndexAtTime(long channelId, long time) {
    735         List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
    736         for (int i = 0; i < entries.size(); ++i) {
    737             TableEntry entry = entries.get(i);
    738             if (entry.entryStartUtcMillis <= time
    739                     && time < entry.entryEndUtcMillis) {
    740                 return i;
    741             }
    742         }
    743         return -1;
    744     }
    745 }
    746