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