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 421 * placeholder (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between 422 * programs. 423 */ getTableEntry(long channelId, int index)424 TableEntry getTableEntry(long channelId, int index) { 425 mProgramDataManager.prefetchChannel(channelId, index); 426 return mChannelIdEntriesMap.get(channelId).get(index); 427 } 428 429 /** Returns list genre ID's which has a channel. */ getFilteredGenreIds()430 List<Integer> getFilteredGenreIds() { 431 return mFilteredGenreIds; 432 } 433 getSelectedGenreId()434 int getSelectedGenreId() { 435 return mSelectedGenreId; 436 } 437 438 // Note that This can be happens only if program guide isn't shown 439 // because an user has to select channels as browsable through UI. updateChannels(boolean clearPreviousTableEntries)440 private void updateChannels(boolean clearPreviousTableEntries) { 441 if (DEBUG) Log.d(TAG, "updateChannels"); 442 mChannels = mChannelDataManager.getBrowsableChannelList(); 443 mSelectedGenreId = GenreItems.ID_ALL_CHANNELS; 444 mFilteredChannels = mChannels; 445 updateTableEntriesWithoutNotification(clearPreviousTableEntries); 446 // Channel update notification should be called after updating table entries, so that 447 // the listener can get the entries. 448 notifyChannelsUpdated(); 449 notifyTableEntriesUpdated(); 450 buildGenreFilters(); 451 } 452 453 /** Sets the channel list for testing */ setChannels(List<Channel> channels)454 void setChannels(List<Channel> channels) { 455 mChannels = new ArrayList<>(channels); 456 mSelectedGenreId = GenreItems.ID_ALL_CHANNELS; 457 mFilteredChannels = mChannels; 458 buildGenreFilters(); 459 } 460 updateTableEntries(boolean clear)461 private void updateTableEntries(boolean clear) { 462 updateTableEntriesWithoutNotification(clear); 463 notifyTableEntriesUpdated(); 464 buildGenreFilters(); 465 } 466 467 /** Updates the table entries without notifying the change. */ updateTableEntriesWithoutNotification(boolean clear)468 private void updateTableEntriesWithoutNotification(boolean clear) { 469 if (clear) { 470 mChannelIdEntriesMap.clear(); 471 } 472 boolean parentalControlsEnabled = 473 mTvInputManagerHelper.getParentalControlSettings().isParentalControlsEnabled(); 474 for (Channel channel : mChannels) { 475 long channelId = channel.getId(); 476 // Inline the updating of the mChannelIdEntriesMap here so we can only call 477 // getParentalControlSettings once. 478 List<TableEntry> entries = createProgramEntries(channelId, parentalControlsEnabled); 479 mChannelIdEntriesMap.put(channelId, entries); 480 481 int size = entries.size(); 482 if (DEBUG) { 483 Log.d( 484 TAG, 485 "Programs are loaded for channel " 486 + channel.getId() 487 + ", loaded size = " 488 + size); 489 } 490 if (size == 0) { 491 continue; 492 } 493 TableEntry lastEntry = entries.get(size - 1); 494 if (mEndUtcMillis < lastEntry.entryEndUtcMillis 495 && lastEntry.entryEndUtcMillis != Long.MAX_VALUE) { 496 mEndUtcMillis = lastEntry.entryEndUtcMillis; 497 } 498 } 499 if (mEndUtcMillis > mStartUtcMillis) { 500 for (Channel channel : mChannels) { 501 long channelId = channel.getId(); 502 List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); 503 if (entries.isEmpty()) { 504 entries.add(new TableEntry(channelId, mStartUtcMillis, mEndUtcMillis)); 505 } else { 506 TableEntry lastEntry = entries.get(entries.size() - 1); 507 if (mEndUtcMillis > lastEntry.entryEndUtcMillis) { 508 entries.add( 509 new TableEntry( 510 channelId, lastEntry.entryEndUtcMillis, mEndUtcMillis)); 511 } else if (lastEntry.entryEndUtcMillis == Long.MAX_VALUE) { 512 entries.remove(entries.size() - 1); 513 entries.add( 514 new TableEntry( 515 lastEntry.channelId, 516 lastEntry.program, 517 lastEntry.scheduledRecording, 518 lastEntry.entryStartUtcMillis, 519 mEndUtcMillis, 520 lastEntry.mIsBlocked)); 521 } 522 } 523 } 524 } 525 } 526 527 /** 528 * Build genre filters based on the current programs. This categories channels by its current 529 * program's canonical genres and subsequent @{link resetChannelListWithGenre(int)} calls will 530 * reset channel list with built channel list. This is expected to be called whenever program 531 * guide is shown. 532 */ buildGenreFilters()533 private void buildGenreFilters() { 534 if (DEBUG) Log.d(TAG, "buildGenreFilters"); 535 536 mGenreChannelList.clear(); 537 for (int i = 0; i < GenreItems.getGenreCount(); i++) { 538 mGenreChannelList.add(new ArrayList<>()); 539 } 540 for (Channel channel : mChannels) { 541 Program currentProgram = mProgramDataManager.getCurrentProgram(channel.getId()); 542 if (currentProgram != null && currentProgram.getCanonicalGenres() != null) { 543 for (String genre : currentProgram.getCanonicalGenres()) { 544 mGenreChannelList.get(GenreItems.getId(genre)).add(channel); 545 } 546 } 547 } 548 mGenreChannelList.set(GenreItems.ID_ALL_CHANNELS, mChannels); 549 mFilteredGenreIds.clear(); 550 mFilteredGenreIds.add(0); 551 for (int i = 1; i < GenreItems.getGenreCount(); i++) { 552 if (mGenreChannelList.get(i).size() > 0) { 553 mFilteredGenreIds.add(i); 554 } 555 } 556 mSelectedGenreId = GenreItems.ID_ALL_CHANNELS; 557 mFilteredChannels = mChannels; 558 notifyGenresUpdated(); 559 } 560 561 @Nullable getTableEntry(ScheduledRecording scheduledRecording)562 private TableEntry getTableEntry(ScheduledRecording scheduledRecording) { 563 return getTableEntry(scheduledRecording.getChannelId(), scheduledRecording.getProgramId()); 564 } 565 566 @Nullable getTableEntry(long channelId, long entryId)567 private TableEntry getTableEntry(long channelId, long entryId) { 568 if (mChannelIdEntriesMap.isEmpty()) { 569 return null; 570 } 571 List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); 572 if (entries != null) { 573 for (TableEntry entry : entries) { 574 if (entry.getId() == entryId) { 575 return entry; 576 } 577 } 578 } 579 return null; 580 } 581 updateEntry(TableEntry old, TableEntry newEntry)582 private void updateEntry(TableEntry old, TableEntry newEntry) { 583 List<TableEntry> entries = mChannelIdEntriesMap.get(old.channelId); 584 int index = entries.indexOf(old); 585 entries.set(index, newEntry); 586 notifyTableEntryUpdated(newEntry); 587 } 588 setTimeRange(long fromUtcMillis, long toUtcMillis)589 private void setTimeRange(long fromUtcMillis, long toUtcMillis) { 590 if (DEBUG) { 591 Log.d( 592 TAG, 593 "setTimeRange. {FromTime=" 594 + Utils.toTimeString(fromUtcMillis) 595 + ", ToTime=" 596 + Utils.toTimeString(toUtcMillis) 597 + "}"); 598 } 599 if (mFromUtcMillis != fromUtcMillis || mToUtcMillis != toUtcMillis) { 600 mFromUtcMillis = fromUtcMillis; 601 mToUtcMillis = toUtcMillis; 602 notifyTimeRangeUpdated(); 603 } 604 } 605 createProgramEntries(long channelId, boolean parentalControlsEnabled)606 private List<TableEntry> createProgramEntries(long channelId, boolean parentalControlsEnabled) { 607 List<TableEntry> entries = new ArrayList<>(); 608 boolean channelLocked = 609 parentalControlsEnabled && mChannelDataManager.getChannel(channelId).isLocked(); 610 if (channelLocked) { 611 entries.add(new TableEntry(channelId, mStartUtcMillis, Long.MAX_VALUE, true)); 612 } else { 613 long lastProgramEndTime = mStartUtcMillis; 614 List<Program> programs = mProgramDataManager.getPrograms(channelId, mStartUtcMillis); 615 for (Program program : programs) { 616 if (program.getChannelId() == INVALID_ID) { 617 // Placeholder program. 618 continue; 619 } 620 long programStartTime = Math.max(program.getStartTimeUtcMillis(), mStartUtcMillis); 621 long programEndTime = program.getEndTimeUtcMillis(); 622 if (programStartTime > lastProgramEndTime) { 623 // Gap since the last program. 624 entries.add(new TableEntry(channelId, lastProgramEndTime, programStartTime)); 625 lastProgramEndTime = programStartTime; 626 } 627 if (programEndTime > lastProgramEndTime) { 628 ScheduledRecording scheduledRecording = 629 mDvrDataManager == null 630 ? null 631 : mDvrDataManager.getScheduledRecordingForProgramId( 632 program.getId()); 633 entries.add( 634 new TableEntry( 635 channelId, 636 program, 637 scheduledRecording, 638 lastProgramEndTime, 639 programEndTime, 640 false)); 641 lastProgramEndTime = programEndTime; 642 } 643 } 644 } 645 646 if (entries.size() > 1) { 647 TableEntry secondEntry = entries.get(1); 648 if (secondEntry.entryStartUtcMillis < mStartUtcMillis + FIRST_ENTRY_MIN_DURATION) { 649 // If the first entry's width doesn't have enough width, it is not good to show 650 // the first entry from UI perspective. So we clip it out. 651 entries.remove(0); 652 entries.set( 653 0, 654 new TableEntry( 655 secondEntry.channelId, 656 secondEntry.program, 657 secondEntry.scheduledRecording, 658 mStartUtcMillis, 659 secondEntry.entryEndUtcMillis, 660 secondEntry.mIsBlocked)); 661 } 662 } 663 return entries; 664 } 665 notifyGenresUpdated()666 private void notifyGenresUpdated() { 667 for (Listener listener : mListeners) { 668 listener.onGenresUpdated(); 669 } 670 } 671 notifyChannelsUpdated()672 private void notifyChannelsUpdated() { 673 for (Listener listener : mListeners) { 674 listener.onChannelsUpdated(); 675 } 676 } 677 notifyTimeRangeUpdated()678 private void notifyTimeRangeUpdated() { 679 for (Listener listener : mListeners) { 680 listener.onTimeRangeUpdated(); 681 } 682 } 683 notifyTableEntriesUpdated()684 private void notifyTableEntriesUpdated() { 685 for (TableEntriesUpdatedListener listener : mTableEntriesUpdatedListeners) { 686 listener.onTableEntriesUpdated(); 687 } 688 } 689 notifyTableEntryUpdated(TableEntry entry)690 private void notifyTableEntryUpdated(TableEntry entry) { 691 for (TableEntryChangedListener listener : mTableEntryChangedListeners) { 692 listener.onTableEntryChanged(entry); 693 } 694 } 695 696 /** 697 * Entry for program guide table. An "entry" can be either an actual program or a gap between 698 * programs. This is needed for {@link ProgramListAdapter} because {@link 699 * androidx.leanback.widget.HorizontalGridView} ignores margins between items. 700 */ 701 static class TableEntry { 702 /** Channel ID which this entry is included. */ 703 final long channelId; 704 705 /** Program corresponding to the entry. {@code null} means that this entry is a gap. */ 706 final Program program; 707 708 final ScheduledRecording scheduledRecording; 709 710 /** Start time of entry in UTC milliseconds. */ 711 final long entryStartUtcMillis; 712 713 /** End time of entry in UTC milliseconds */ 714 final long entryEndUtcMillis; 715 716 private final boolean mIsBlocked; 717 TableEntry(long channelId, long startUtcMillis, long endUtcMillis)718 private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) { 719 this(channelId, null, startUtcMillis, endUtcMillis, false); 720 } 721 TableEntry( long channelId, long startUtcMillis, long endUtcMillis, boolean blocked)722 private TableEntry( 723 long channelId, long startUtcMillis, long endUtcMillis, boolean blocked) { 724 this(channelId, null, null, startUtcMillis, endUtcMillis, blocked); 725 } 726 TableEntry( long channelId, ProgramImpl program, long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked)727 private TableEntry( 728 long channelId, 729 ProgramImpl program, 730 long entryStartUtcMillis, 731 long entryEndUtcMillis, 732 boolean isBlocked) { 733 this(channelId, program, null, entryStartUtcMillis, entryEndUtcMillis, isBlocked); 734 } 735 TableEntry( long channelId, Program program, ScheduledRecording scheduledRecording, long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked)736 private TableEntry( 737 long channelId, 738 Program program, 739 ScheduledRecording scheduledRecording, 740 long entryStartUtcMillis, 741 long entryEndUtcMillis, 742 boolean isBlocked) { 743 this.channelId = channelId; 744 this.program = program; 745 this.scheduledRecording = scheduledRecording; 746 this.entryStartUtcMillis = entryStartUtcMillis; 747 this.entryEndUtcMillis = entryEndUtcMillis; 748 mIsBlocked = isBlocked; 749 } 750 751 /** A stable id useful for {@link androidx.recyclerview.widget.RecyclerView.Adapter}. */ getId()752 long getId() { 753 // using a negative entryEndUtcMillis keeps it from conflicting with program Id 754 return program != null ? program.getId() : -entryEndUtcMillis; 755 } 756 757 /** Returns true if this is a gap. */ isGap()758 boolean isGap() { 759 return !Program.isProgramValid(program); 760 } 761 762 /** Returns true if this channel is blocked. */ isBlocked()763 boolean isBlocked() { 764 return mIsBlocked; 765 } 766 767 /** Returns true if this program is on the air. */ isCurrentProgram()768 boolean isCurrentProgram() { 769 long current = System.currentTimeMillis(); 770 return entryStartUtcMillis <= current && entryEndUtcMillis > current; 771 } 772 773 /** Returns if this program has the genre. */ hasGenre(int genreId)774 boolean hasGenre(int genreId) { 775 return !isGap() && program.hasGenre(genreId); 776 } 777 778 /** Returns the width of table entry, in pixels. */ getWidth()779 int getWidth() { 780 return GuideUtils.convertMillisToPixel(entryStartUtcMillis, entryEndUtcMillis); 781 } 782 783 @Override toString()784 public String toString() { 785 return "TableEntry{" 786 + "hashCode=" 787 + hashCode() 788 + ", channelId=" 789 + channelId 790 + ", program=" 791 + program 792 + ", startTime=" 793 + Utils.toTimeString(entryStartUtcMillis) 794 + ", endTimeTime=" 795 + Utils.toTimeString(entryEndUtcMillis) 796 + "}"; 797 } 798 } 799 800 @VisibleForTesting createTableEntryForTest( long channelId, Program program, ScheduledRecording scheduledRecording, long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked)801 public static TableEntry createTableEntryForTest( 802 long channelId, 803 Program program, 804 ScheduledRecording scheduledRecording, 805 long entryStartUtcMillis, 806 long entryEndUtcMillis, 807 boolean isBlocked) { 808 return new TableEntry( 809 channelId, 810 program, 811 scheduledRecording, 812 entryStartUtcMillis, 813 entryEndUtcMillis, 814 isBlocked); 815 } 816 817 interface Listener { onGenresUpdated()818 void onGenresUpdated(); 819 onChannelsUpdated()820 void onChannelsUpdated(); 821 onTimeRangeUpdated()822 void onTimeRangeUpdated(); 823 } 824 825 interface TableEntriesUpdatedListener { onTableEntriesUpdated()826 void onTableEntriesUpdated(); 827 } 828 829 interface TableEntryChangedListener { onTableEntryChanged(TableEntry entry)830 void onTableEntryChanged(TableEntry entry); 831 } 832 833 static class ListenerAdapter implements Listener { 834 @Override onGenresUpdated()835 public void onGenresUpdated() {} 836 837 @Override onChannelsUpdated()838 public void onChannelsUpdated() {} 839 840 @Override onTimeRangeUpdated()841 public void onTimeRangeUpdated() {} 842 } 843 } 844