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