1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.guide; 18 19 import android.support.annotation.MainThread; 20 import android.support.annotation.Nullable; 21 import android.util.ArraySet; 22 import android.util.Log; 23 24 import com.android.tv.data.Channel; 25 import com.android.tv.data.ChannelDataManager; 26 import com.android.tv.data.GenreItems; 27 import com.android.tv.data.Program; 28 import com.android.tv.data.ProgramDataManager; 29 import com.android.tv.dvr.DvrDataManager; 30 import com.android.tv.dvr.ScheduledRecording; 31 import com.android.tv.util.TvInputManagerHelper; 32 import com.android.tv.util.Utils; 33 34 import java.util.ArrayList; 35 import java.util.HashMap; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.Set; 39 import java.util.concurrent.TimeUnit; 40 41 /** 42 * Manages the channels and programs for the program guide. 43 */ 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 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 TableEntry(long channelId, long startUtcMillis, long endUtcMillis)91 private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) { 92 this(channelId, null, startUtcMillis, endUtcMillis, false); 93 } 94 TableEntry(long channelId, long startUtcMillis, long endUtcMillis, boolean blocked)95 private TableEntry(long channelId, long startUtcMillis, long endUtcMillis, 96 boolean blocked) { 97 this(channelId, null, null, startUtcMillis, endUtcMillis, blocked); 98 } 99 TableEntry(long channelId, Program program, long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked)100 private TableEntry(long channelId, Program program, long entryStartUtcMillis, 101 long entryEndUtcMillis, boolean isBlocked) { 102 this(channelId, program, null, entryStartUtcMillis, entryEndUtcMillis, isBlocked); 103 } 104 TableEntry(long channelId, Program program, ScheduledRecording scheduledRecording, long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked)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 */ getId()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 */ isGap()126 public boolean isGap() { 127 return !Program.isValid(program); 128 } 129 130 /** 131 * Returns true if this channel is blocked. 132 */ isBlocked()133 public boolean isBlocked() { 134 return mIsBlocked; 135 } 136 137 /** 138 * Returns true if this program is on the air. 139 */ isCurrentProgram()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 */ hasGenre(int genreId)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 */ getWidth()155 public int getWidth() { 156 return GuideUtils.convertMillisToPixel(entryStartUtcMillis, entryEndUtcMillis); 157 } 158 159 @Override toString()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 ProgramManager(TvInputManagerHelper tvInputManagerHelper, ChannelDataManager channelDataManager, ProgramDataManager programDataManager, @Nullable DvrDataManager dvrDataManager)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 programGuideVisibilityChanged(boolean visible)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 */ addListener(Listener listener)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 */ addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener)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 */ addTableEntryChangedListener(TableEntryChangedListener listener)291 public void addTableEntryChangedListener(TableEntryChangedListener listener) { 292 mTableEntryChangedListeners.add(listener); 293 } 294 295 /** 296 * Removes a {@link Listener}. 297 */ removeListener(Listener listener)298 public void removeListener(Listener listener) { 299 mListeners.remove(listener); 300 } 301 302 /** 303 * Removes a previously installed table entries update listener. 304 */ removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener)305 public void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { 306 mTableEntriesUpdatedListeners.remove(listener); 307 } 308 309 /** 310 * Removes a previously installed table entry changed listener. 311 */ removeTableEntryChangedListener(TableEntryChangedListener listener)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 */ buildGenreFilters()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 */ resetChannelListWithGenre(int genreId)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 */ getFilteredGenreIds()376 public List<Integer> getFilteredGenreIds() { 377 return mFilteredGenreIds; 378 } 379 getSelectedGenreId()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. updateChannels(boolean notify, boolean clearPreviousTableEntries)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 updateTableEntries(boolean notify, boolean clear)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 notifyGenresUpdated()452 private void notifyGenresUpdated() { 453 for (Listener listener : mListeners) { 454 listener.onGenresUpdated(); 455 } 456 } 457 notifyChannelsUpdated()458 private void notifyChannelsUpdated() { 459 for (Listener listener : mListeners) { 460 listener.onChannelsUpdated(); 461 } 462 } 463 notifyTimeRangeUpdated()464 private void notifyTimeRangeUpdated() { 465 for (Listener listener : mListeners) { 466 listener.onTimeRangeUpdated(); 467 } 468 } 469 notifyTableEntriesUpdated()470 private void notifyTableEntriesUpdated() { 471 for (TableEntriesUpdatedListener listener : mTableEntriesUpdatedListeners) { 472 listener.onTableEntriesUpdated(); 473 } 474 } 475 notifyTableEntryUpdated(TableEntry entry)476 private void notifyTableEntryUpdated(TableEntry entry) { 477 for (TableEntryChangedListener listener : mTableEntryChangedListeners) { 478 listener.onTableEntryChanged(entry); 479 } 480 } 481 updateEntry(TableEntry old, TableEntry newEntry)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 getTableEntry(ScheduledRecording scheduledRecording)490 private TableEntry getTableEntry(ScheduledRecording scheduledRecording) { 491 return getTableEntry(scheduledRecording.getChannelId(), scheduledRecording.getProgramId()); 492 } 493 494 @Nullable getTableEntry(long channelId, long entryId)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 */ getFromUtcMillis()510 public long getFromUtcMillis() { 511 return mFromUtcMillis; 512 } 513 514 /** 515 * Returns the end time of currently managed time range, in UTC millisecond. 516 */ getToUtcMillis()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 */ updateInitialTimeRange(long startUtcMillis, long endUtcMillis)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 setTimeRange(long fromUtcMillis, long toUtcMillis)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 */ getChannelCount()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 */ getChannel(int channelIndex)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 */ getChannelIndex(Channel channel)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 */ getChannelIndex(long channelId)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 */ getTableEntryCount(long channelId)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 */ getTableEntry(long channelId, int index)595 public TableEntry getTableEntry(long channelId, int index) { 596 return mChannelIdEntriesMap.get(channelId).get(index); 597 } 598 createProgramEntries(long channelId, boolean parentalControlsEnabled)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 */ getSelectedChannel()649 public Channel getSelectedChannel() { 650 return mChannelDataManager.getChannel(mSelectedProgram.getChannelId()); 651 } 652 653 /** 654 * Get the currently selected program. 655 */ getSelectedProgram()656 public Program getSelectedProgram() { 657 return mSelectedProgram; 658 } 659 660 public interface Listener { onGenresUpdated()661 void onGenresUpdated(); onChannelsUpdated()662 void onChannelsUpdated(); onTimeRangeUpdated()663 void onTimeRangeUpdated(); 664 } 665 666 public interface TableEntriesUpdatedListener { onTableEntriesUpdated()667 void onTableEntriesUpdated(); 668 } 669 670 public interface TableEntryChangedListener { onTableEntryChanged(TableEntry entry)671 void onTableEntryChanged(TableEntry entry); 672 } 673 674 public static class ListenerAdapter implements Listener { 675 @Override onGenresUpdated()676 public void onGenresUpdated() { } 677 678 @Override onChannelsUpdated()679 public void onChannelsUpdated() { } 680 681 @Override onTimeRangeUpdated()682 public void onTimeRangeUpdated() { } 683 } 684 685 /** 686 * Shifts the time range by the given time. Also makes ProgramGuide scroll the views. 687 */ shiftTime(long timeMillisToScroll)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 */ getShiftedTime()705 public long getShiftedTime() { 706 return mFromUtcMillis - mStartUtcMillis; 707 } 708 709 /** 710 * Returns the start time set by {@link #updateInitialTimeRange}. 711 */ getStartTime()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 */ getProgramIdIndex(long channelId, long entryId)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 */ getProgramIndexAtTime(long channelId, long time)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