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