• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ProgramManager(TvInputManagerHelper tvInputManagerHelper, ChannelDataManager channelDataManager, ProgramDataManager programDataManager, @Nullable DvrDataManager dvrDataManager, @Nullable DvrScheduleManager dvrScheduleManager)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 
programGuideVisibilityChanged(boolean visible)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      */
addListener(Listener listener)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      */
addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener)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      */
addTableEntryChangedListener(TableEntryChangedListener listener)241     void addTableEntryChangedListener(TableEntryChangedListener listener) {
242         mTableEntryChangedListeners.add(listener);
243     }
244 
245     /**
246      * Removes a {@link Listener}.
247      */
removeListener(Listener listener)248     void removeListener(Listener listener) {
249         mListeners.remove(listener);
250     }
251 
252     /**
253      * Removes a previously installed table entries update listener.
254      */
removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener)255     void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
256         mTableEntriesUpdatedListeners.remove(listener);
257     }
258 
259     /**
260      * Removes a previously installed table entry changed listener.
261      */
removeTableEntryChangedListener(TableEntryChangedListener listener)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      */
resetChannelListWithGenre(int genreId)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      */
updateInitialTimeRange(long startUtcMillis, long endUtcMillis)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      */
shiftTime(long timeMillisToScroll)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      */
getShiftedTime()322     long getShiftedTime() {
323         return mFromUtcMillis - mStartUtcMillis;
324     }
325 
326     /**
327      * Returns the start time set by {@link #updateInitialTimeRange}.
328      */
getStartTime()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      */
getProgramIdIndex(long channelId, long entryId)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      */
getProgramIndexAtTime(long channelId, long time)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      */
getFromUtcMillis()366     long getFromUtcMillis() {
367         return mFromUtcMillis;
368     }
369 
370     /**
371      * Returns the end time of currently managed time range, in UTC millisecond.
372      */
getToUtcMillis()373     long getToUtcMillis() {
374         return mToUtcMillis;
375     }
376 
377     /**
378      * Returns the number of the currently managed channels.
379      */
getChannelCount()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      */
getChannel(int channelIndex)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      */
getChannelIndex(Channel channel)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      */
getChannelIndex(long channelId)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      */
getTableEntryCount(long channelId)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      */
getTableEntry(long channelId, int index)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      */
getFilteredGenreIds()431     List<Integer> getFilteredGenreIds() {
432         return mFilteredGenreIds;
433     }
434 
getSelectedGenreId()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.
updateChannels(boolean clearPreviousTableEntries)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 
updateTableEntries(boolean clear)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      */
updateTableEntriesWithoutNotification(boolean clear)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      */
buildGenreFilters()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
getTableEntry(ScheduledRecording scheduledRecording)549     private TableEntry getTableEntry(ScheduledRecording scheduledRecording) {
550         return getTableEntry(scheduledRecording.getChannelId(), scheduledRecording.getProgramId());
551     }
552 
553     @Nullable
getTableEntry(long channelId, long entryId)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 
updateEntry(TableEntry old, TableEntry newEntry)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 
setTimeRange(long fromUtcMillis, long toUtcMillis)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 
createProgramEntries(long channelId, boolean parentalControlsEnabled)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 
notifyGenresUpdated()633     private void notifyGenresUpdated() {
634         for (Listener listener : mListeners) {
635             listener.onGenresUpdated();
636         }
637     }
638 
notifyChannelsUpdated()639     private void notifyChannelsUpdated() {
640         for (Listener listener : mListeners) {
641             listener.onChannelsUpdated();
642         }
643     }
644 
notifyTimeRangeUpdated()645     private void notifyTimeRangeUpdated() {
646         for (Listener listener : mListeners) {
647             listener.onTimeRangeUpdated();
648         }
649     }
650 
notifyTableEntriesUpdated()651     private void notifyTableEntriesUpdated() {
652         for (TableEntriesUpdatedListener listener : mTableEntriesUpdatedListeners) {
653             listener.onTableEntriesUpdated();
654         }
655     }
656 
notifyTableEntryUpdated(TableEntry entry)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 
TableEntry(long channelId, long startUtcMillis, long endUtcMillis)685         private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) {
686             this(channelId, null, startUtcMillis, endUtcMillis, false);
687         }
688 
TableEntry(long channelId, long startUtcMillis, long endUtcMillis, boolean blocked)689         private TableEntry(long channelId, long startUtcMillis, long endUtcMillis,
690                            boolean blocked) {
691             this(channelId, null, null, startUtcMillis, endUtcMillis, blocked);
692         }
693 
TableEntry(long channelId, Program program, long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked)694         private TableEntry(long channelId, Program program, long entryStartUtcMillis,
695                            long entryEndUtcMillis, boolean isBlocked) {
696             this(channelId, program, null, entryStartUtcMillis, entryEndUtcMillis, isBlocked);
697         }
698 
TableEntry(long channelId, Program program, ScheduledRecording scheduledRecording, long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked)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          */
getId()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          */
isGap()720         boolean isGap() {
721             return !Program.isValid(program);
722         }
723 
724         /**
725          * Returns true if this channel is blocked.
726          */
isBlocked()727         boolean isBlocked() {
728             return mIsBlocked;
729         }
730 
731         /**
732          * Returns true if this program is on the air.
733          */
isCurrentProgram()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          */
hasGenre(int genreId)742         boolean hasGenre(int genreId) {
743             return !isGap() && program.hasGenre(genreId);
744         }
745 
746         /**
747          * Returns the width of table entry, in pixels.
748          */
getWidth()749         int getWidth() {
750             return GuideUtils.convertMillisToPixel(entryStartUtcMillis, entryEndUtcMillis);
751         }
752 
753         @Override
toString()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 {
onGenresUpdated()765         void onGenresUpdated();
onChannelsUpdated()766         void onChannelsUpdated();
onTimeRangeUpdated()767         void onTimeRangeUpdated();
768     }
769 
770     interface TableEntriesUpdatedListener {
onTableEntriesUpdated()771         void onTableEntriesUpdated();
772     }
773 
774     interface TableEntryChangedListener {
onTableEntryChanged(TableEntry entry)775         void onTableEntryChanged(TableEntry entry);
776     }
777 
778     static class ListenerAdapter implements Listener {
779         @Override
onGenresUpdated()780         public void onGenresUpdated() { }
781 
782         @Override
onChannelsUpdated()783         public void onChannelsUpdated() { }
784 
785         @Override
onTimeRangeUpdated()786         public void onTimeRangeUpdated() { }
787     }
788 }
789