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