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