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