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