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