ProgramManager.java revision 0cc0713c1bf8027642987b750b80217569d2932a
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 17package com.android.tv.guide; 18 19import android.support.annotation.MainThread; 20import android.support.annotation.Nullable; 21import android.support.annotation.VisibleForTesting; 22import android.util.ArraySet; 23import android.util.Log; 24import com.android.tv.data.ChannelDataManager; 25import com.android.tv.data.GenreItems; 26import com.android.tv.data.Program; 27import com.android.tv.data.ProgramDataManager; 28import com.android.tv.data.api.Channel; 29import com.android.tv.dvr.DvrDataManager; 30import com.android.tv.dvr.DvrScheduleManager; 31import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener; 32import com.android.tv.dvr.data.ScheduledRecording; 33import com.android.tv.util.TvInputManagerHelper; 34import com.android.tv.util.Utils; 35import java.util.ArrayList; 36import java.util.HashMap; 37import java.util.List; 38import java.util.Map; 39import java.util.Set; 40import java.util.concurrent.TimeUnit; 41 42/** Manages the channels and programs for the program guide. */ 43@MainThread 44public 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 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 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}. */ 240 void addListener(Listener listener) { 241 mListeners.add(listener); 242 } 243 244 /** Registers a listener to be invoked when table entries are updated. */ 245 void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { 246 mTableEntriesUpdatedListeners.add(listener); 247 } 248 249 /** Registers a listener to be invoked when a table entry is changed. */ 250 void addTableEntryChangedListener(TableEntryChangedListener listener) { 251 mTableEntryChangedListeners.add(listener); 252 } 253 254 /** Removes a {@link Listener}. */ 255 void removeListener(Listener listener) { 256 mListeners.remove(listener); 257 } 258 259 /** Removes a previously installed table entries update listener. */ 260 void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { 261 mTableEntriesUpdatedListeners.remove(listener); 262 } 263 264 /** Removes a previously installed table entry changed 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 */ 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. */ 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. */ 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. */ 323 long getShiftedTime() { 324 return mFromUtcMillis - mStartUtcMillis; 325 } 326 327 /** Returns the start time set by {@link #updateInitialTimeRange}. */ 328 long getStartTime() { 329 return mStartUtcMillis; 330 } 331 332 /** Returns the program index of the program with {@code entryId} or -1 if not found. */ 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. */ 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. */ 358 long getFromUtcMillis() { 359 return mFromUtcMillis; 360 } 361 362 /** Returns the end time of currently managed time range, in UTC millisecond. */ 363 long getToUtcMillis() { 364 return mToUtcMillis; 365 } 366 367 /** Returns the number of the currently managed channels. */ 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 */ 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 */ 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 */ 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 */ 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 */ 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. */ 417 List<Integer> getFilteredGenreIds() { 418 return mFilteredGenreIds; 419 } 420 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. 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 440 private void updateTableEntries(boolean clear) { 441 updateTableEntriesWithoutNotification(clear); 442 notifyTableEntriesUpdated(); 443 buildGenreFilters(); 444 } 445 446 /** Updates the table entries without notifying the change. */ 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 */ 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 541 private TableEntry getTableEntry(ScheduledRecording scheduledRecording) { 542 return getTableEntry(scheduledRecording.getChannelId(), scheduledRecording.getProgramId()); 543 } 544 545 @Nullable 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 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 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 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 642 private void notifyGenresUpdated() { 643 for (Listener listener : mListeners) { 644 listener.onGenresUpdated(); 645 } 646 } 647 648 private void notifyChannelsUpdated() { 649 for (Listener listener : mListeners) { 650 listener.onChannelsUpdated(); 651 } 652 } 653 654 private void notifyTimeRangeUpdated() { 655 for (Listener listener : mListeners) { 656 listener.onTimeRangeUpdated(); 657 } 658 } 659 660 private void notifyTableEntriesUpdated() { 661 for (TableEntriesUpdatedListener listener : mTableEntriesUpdatedListeners) { 662 listener.onTableEntriesUpdated(); 663 } 664 } 665 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 694 private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) { 695 this(channelId, null, startUtcMillis, endUtcMillis, false); 696 } 697 698 private TableEntry( 699 long channelId, long startUtcMillis, long endUtcMillis, boolean blocked) { 700 this(channelId, null, null, startUtcMillis, endUtcMillis, blocked); 701 } 702 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 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}. */ 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. */ 734 boolean isGap() { 735 return !Program.isProgramValid(program); 736 } 737 738 /** Returns true if this channel is blocked. */ 739 boolean isBlocked() { 740 return mIsBlocked; 741 } 742 743 /** Returns true if this program is on the air. */ 744 boolean isCurrentProgram() { 745 long current = System.currentTimeMillis(); 746 return entryStartUtcMillis <= current && entryEndUtcMillis > current; 747 } 748 749 /** Returns if this program has the genre. */ 750 boolean hasGenre(int genreId) { 751 return !isGap() && program.hasGenre(genreId); 752 } 753 754 /** Returns the width of table entry, in pixels. */ 755 int getWidth() { 756 return GuideUtils.convertMillisToPixel(entryStartUtcMillis, entryEndUtcMillis); 757 } 758 759 @Override 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 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 { 794 void onGenresUpdated(); 795 796 void onChannelsUpdated(); 797 798 void onTimeRangeUpdated(); 799 } 800 801 interface TableEntriesUpdatedListener { 802 void onTableEntriesUpdated(); 803 } 804 805 interface TableEntryChangedListener { 806 void onTableEntryChanged(TableEntry entry); 807 } 808 809 static class ListenerAdapter implements Listener { 810 @Override 811 public void onGenresUpdated() {} 812 813 @Override 814 public void onChannelsUpdated() {} 815 816 @Override 817 public void onTimeRangeUpdated() {} 818 } 819} 820