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