ProgramManager.java revision 0cc0713c1bf8027642987b750b80217569d2932a
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
17package com.android.tv.guide;
18
19import android.support.annotation.MainThread;
20import android.support.annotation.Nullable;
21import android.support.annotation.VisibleForTesting;
22import android.util.ArraySet;
23import android.util.Log;
24import com.android.tv.data.ChannelDataManager;
25import com.android.tv.data.GenreItems;
26import com.android.tv.data.Program;
27import com.android.tv.data.ProgramDataManager;
28import com.android.tv.data.api.Channel;
29import com.android.tv.dvr.DvrDataManager;
30import com.android.tv.dvr.DvrScheduleManager;
31import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener;
32import com.android.tv.dvr.data.ScheduledRecording;
33import com.android.tv.util.TvInputManagerHelper;
34import com.android.tv.util.Utils;
35import java.util.ArrayList;
36import java.util.HashMap;
37import java.util.List;
38import java.util.Map;
39import java.util.Set;
40import java.util.concurrent.TimeUnit;
41
42/** Manages the channels and programs for the program guide. */
43@MainThread
44public 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
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
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}. */
240    void addListener(Listener listener) {
241        mListeners.add(listener);
242    }
243
244    /** Registers a listener to be invoked when table entries are updated. */
245    void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
246        mTableEntriesUpdatedListeners.add(listener);
247    }
248
249    /** Registers a listener to be invoked when a table entry is changed. */
250    void addTableEntryChangedListener(TableEntryChangedListener listener) {
251        mTableEntryChangedListeners.add(listener);
252    }
253
254    /** Removes a {@link Listener}. */
255    void removeListener(Listener listener) {
256        mListeners.remove(listener);
257    }
258
259    /** Removes a previously installed table entries update listener. */
260    void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
261        mTableEntriesUpdatedListeners.remove(listener);
262    }
263
264    /** Removes a previously installed table entry changed 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     */
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. */
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. */
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. */
323    long getShiftedTime() {
324        return mFromUtcMillis - mStartUtcMillis;
325    }
326
327    /** Returns the start time set by {@link #updateInitialTimeRange}. */
328    long getStartTime() {
329        return mStartUtcMillis;
330    }
331
332    /** Returns the program index of the program with {@code entryId} or -1 if not found. */
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. */
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. */
358    long getFromUtcMillis() {
359        return mFromUtcMillis;
360    }
361
362    /** Returns the end time of currently managed time range, in UTC millisecond. */
363    long getToUtcMillis() {
364        return mToUtcMillis;
365    }
366
367    /** Returns the number of the currently managed channels. */
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     */
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     */
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     */
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     */
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     */
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. */
417    List<Integer> getFilteredGenreIds() {
418        return mFilteredGenreIds;
419    }
420
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.
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
440    private void updateTableEntries(boolean clear) {
441        updateTableEntriesWithoutNotification(clear);
442        notifyTableEntriesUpdated();
443        buildGenreFilters();
444    }
445
446    /** Updates the table entries without notifying the change. */
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     */
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
541    private TableEntry getTableEntry(ScheduledRecording scheduledRecording) {
542        return getTableEntry(scheduledRecording.getChannelId(), scheduledRecording.getProgramId());
543    }
544
545    @Nullable
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
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
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
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
642    private void notifyGenresUpdated() {
643        for (Listener listener : mListeners) {
644            listener.onGenresUpdated();
645        }
646    }
647
648    private void notifyChannelsUpdated() {
649        for (Listener listener : mListeners) {
650            listener.onChannelsUpdated();
651        }
652    }
653
654    private void notifyTimeRangeUpdated() {
655        for (Listener listener : mListeners) {
656            listener.onTimeRangeUpdated();
657        }
658    }
659
660    private void notifyTableEntriesUpdated() {
661        for (TableEntriesUpdatedListener listener : mTableEntriesUpdatedListeners) {
662            listener.onTableEntriesUpdated();
663        }
664    }
665
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
694        private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) {
695            this(channelId, null, startUtcMillis, endUtcMillis, false);
696        }
697
698        private TableEntry(
699                long channelId, long startUtcMillis, long endUtcMillis, boolean blocked) {
700            this(channelId, null, null, startUtcMillis, endUtcMillis, blocked);
701        }
702
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
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}. */
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. */
734        boolean isGap() {
735            return !Program.isProgramValid(program);
736        }
737
738        /** Returns true if this channel is blocked. */
739        boolean isBlocked() {
740            return mIsBlocked;
741        }
742
743        /** Returns true if this program is on the air. */
744        boolean isCurrentProgram() {
745            long current = System.currentTimeMillis();
746            return entryStartUtcMillis <= current && entryEndUtcMillis > current;
747        }
748
749        /** Returns if this program has the genre. */
750        boolean hasGenre(int genreId) {
751            return !isGap() && program.hasGenre(genreId);
752        }
753
754        /** Returns the width of table entry, in pixels. */
755        int getWidth() {
756            return GuideUtils.convertMillisToPixel(entryStartUtcMillis, entryEndUtcMillis);
757        }
758
759        @Override
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
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 {
794        void onGenresUpdated();
795
796        void onChannelsUpdated();
797
798        void onTimeRangeUpdated();
799    }
800
801    interface TableEntriesUpdatedListener {
802        void onTableEntriesUpdated();
803    }
804
805    interface TableEntryChangedListener {
806        void onTableEntryChanged(TableEntry entry);
807    }
808
809    static class ListenerAdapter implements Listener {
810        @Override
811        public void onGenresUpdated() {}
812
813        @Override
814        public void onChannelsUpdated() {}
815
816        @Override
817        public void onTimeRangeUpdated() {}
818    }
819}
820