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