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