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