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.dvr; 18 19 import android.annotation.TargetApi; 20 import android.content.Context; 21 import android.media.tv.TvInputInfo; 22 import android.os.Build; 23 import android.support.annotation.MainThread; 24 import android.support.annotation.NonNull; 25 import android.support.annotation.VisibleForTesting; 26 import android.util.ArraySet; 27 import android.util.LongSparseArray; 28 import android.util.Range; 29 30 import com.android.tv.ApplicationSingletons; 31 import com.android.tv.TvApplication; 32 import com.android.tv.common.SoftPreconditions; 33 import com.android.tv.data.Channel; 34 import com.android.tv.data.ChannelDataManager; 35 import com.android.tv.data.Program; 36 import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; 37 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; 38 import com.android.tv.util.CompositeComparator; 39 import com.android.tv.util.Utils; 40 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.Comparator; 44 import java.util.HashMap; 45 import java.util.Iterator; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Set; 49 import java.util.concurrent.CopyOnWriteArraySet; 50 51 /** 52 * A class to manage the schedules. 53 */ 54 @TargetApi(Build.VERSION_CODES.N) 55 @MainThread 56 public class DvrScheduleManager { 57 private static final String TAG = "DvrScheduleManager"; 58 59 /** 60 * The default priority of scheduled recording. 61 */ 62 public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1; 63 /** 64 * The default priority of series recording. 65 */ 66 public static final long DEFAULT_SERIES_PRIORITY = DEFAULT_PRIORITY >> 1; 67 // The new priority will have the offset from the existing one. 68 private static final long PRIORITY_OFFSET = 1024; 69 70 private static final Comparator<ScheduledRecording> RESULT_COMPARATOR = 71 new CompositeComparator<>( 72 ScheduledRecording.PRIORITY_COMPARATOR.reversed(), 73 ScheduledRecording.START_TIME_COMPARATOR, 74 ScheduledRecording.ID_COMPARATOR.reversed()); 75 76 // The candidate comparator should be the consistent with 77 // InputTaskScheduler#CANDIDATE_COMPARATOR. 78 private static final Comparator<ScheduledRecording> CANDIDATE_COMPARATOR = 79 new CompositeComparator<>( 80 ScheduledRecording.PRIORITY_COMPARATOR, 81 ScheduledRecording.END_TIME_COMPARATOR, 82 ScheduledRecording.ID_COMPARATOR); 83 84 private final Context mContext; 85 private final DvrDataManagerImpl mDataManager; 86 private final ChannelDataManager mChannelDataManager; 87 88 private final Map<String, List<ScheduledRecording>> mInputScheduleMap = new HashMap<>(); 89 // The inner map is a hash map from scheduled recording to its conflicting status, i.e., 90 // the boolean value true denotes the schedule is just partially conflicting, which means 91 // although there's conflictit, it might still be recorded partially. 92 private final Map<String, Map<ScheduledRecording, Boolean>> mInputConflictInfoMap = 93 new HashMap<>(); 94 95 private boolean mInitialized; 96 97 private final Set<OnInitializeListener> mOnInitializeListeners = new CopyOnWriteArraySet<>(); 98 private final Set<ScheduledRecordingListener> mScheduledRecordingListeners = new ArraySet<>(); 99 private final Set<OnConflictStateChangeListener> mOnConflictStateChangeListeners = 100 new ArraySet<>(); 101 102 public DvrScheduleManager(Context context) { 103 mContext = context; 104 ApplicationSingletons appSingletons = TvApplication.getSingletons(context); 105 mDataManager = (DvrDataManagerImpl) appSingletons.getDvrDataManager(); 106 mChannelDataManager = appSingletons.getChannelDataManager(); 107 if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) { 108 buildData(); 109 } else { 110 mDataManager.addDvrScheduleLoadFinishedListener( 111 new OnDvrScheduleLoadFinishedListener() { 112 @Override 113 public void onDvrScheduleLoadFinished() { 114 mDataManager.removeDvrScheduleLoadFinishedListener(this); 115 if (mChannelDataManager.isDbLoadFinished() && !mInitialized) { 116 buildData(); 117 } 118 } 119 }); 120 } 121 ScheduledRecordingListener scheduledRecordingListener = new ScheduledRecordingListener() { 122 @Override 123 public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { 124 if (!mInitialized) { 125 return; 126 } 127 for (ScheduledRecording schedule : scheduledRecordings) { 128 if (!schedule.isNotStarted() && !schedule.isInProgress()) { 129 continue; 130 } 131 TvInputInfo input = Utils 132 .getTvInputInfoForInputId(mContext, schedule.getInputId()); 133 if (!SoftPreconditions.checkArgument(input != null, TAG, 134 "Input was removed for : " + schedule)) { 135 // Input removed. 136 mInputScheduleMap.remove(schedule.getInputId()); 137 mInputConflictInfoMap.remove(schedule.getInputId()); 138 continue; 139 } 140 String inputId = input.getId(); 141 List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); 142 if (schedules == null) { 143 schedules = new ArrayList<>(); 144 mInputScheduleMap.put(inputId, schedules); 145 } 146 schedules.add(schedule); 147 } 148 onSchedulesChanged(); 149 notifyScheduledRecordingAdded(scheduledRecordings); 150 } 151 152 @Override 153 public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { 154 if (!mInitialized) { 155 return; 156 } 157 for (ScheduledRecording schedule : scheduledRecordings) { 158 TvInputInfo input = Utils 159 .getTvInputInfoForInputId(mContext, schedule.getInputId()); 160 if (input == null) { 161 // Input removed. 162 mInputScheduleMap.remove(schedule.getInputId()); 163 mInputConflictInfoMap.remove(schedule.getInputId()); 164 continue; 165 } 166 String inputId = input.getId(); 167 List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); 168 if (schedules != null) { 169 schedules.remove(schedule); 170 if (schedules.isEmpty()) { 171 mInputScheduleMap.remove(inputId); 172 } 173 } 174 Map<ScheduledRecording, Boolean> conflictInfo = 175 mInputConflictInfoMap.get(inputId); 176 if (conflictInfo != null) { 177 conflictInfo.remove(schedule); 178 if (conflictInfo.isEmpty()) { 179 mInputConflictInfoMap.remove(inputId); 180 } 181 } 182 } 183 onSchedulesChanged(); 184 notifyScheduledRecordingRemoved(scheduledRecordings); 185 } 186 187 @Override 188 public void onScheduledRecordingStatusChanged( 189 ScheduledRecording... scheduledRecordings) { 190 if (!mInitialized) { 191 return; 192 } 193 for (ScheduledRecording schedule : scheduledRecordings) { 194 TvInputInfo input = Utils 195 .getTvInputInfoForInputId(mContext, schedule.getInputId()); 196 if (!SoftPreconditions.checkArgument(input != null, TAG, 197 "Input was removed for : " + schedule)) { 198 // Input removed. 199 mInputScheduleMap.remove(schedule.getInputId()); 200 mInputConflictInfoMap.remove(schedule.getInputId()); 201 continue; 202 } 203 String inputId = input.getId(); 204 List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); 205 if (schedules == null) { 206 schedules = new ArrayList<>(); 207 mInputScheduleMap.put(inputId, schedules); 208 } 209 // Compare ID because ScheduledRecording.equals() doesn't work if the state 210 // is changed. 211 for (Iterator<ScheduledRecording> i = schedules.iterator(); i.hasNext(); ) { 212 if (i.next().getId() == schedule.getId()) { 213 i.remove(); 214 break; 215 } 216 } 217 if (schedule.isNotStarted() || schedule.isInProgress()) { 218 schedules.add(schedule); 219 } 220 if (schedules.isEmpty()) { 221 mInputScheduleMap.remove(inputId); 222 } 223 // Update conflict list as well 224 Map<ScheduledRecording, Boolean> conflictInfo = 225 mInputConflictInfoMap.get(inputId); 226 if (conflictInfo != null) { 227 // Compare ID because ScheduledRecording.equals() doesn't work if the state 228 // is changed. 229 ScheduledRecording oldSchedule = null; 230 for (ScheduledRecording s : conflictInfo.keySet()) { 231 if (s.getId() == schedule.getId()) { 232 oldSchedule = s; 233 break; 234 } 235 } 236 if (oldSchedule != null) { 237 conflictInfo.put(schedule, conflictInfo.get(oldSchedule)); 238 conflictInfo.remove(oldSchedule); 239 } 240 } 241 } 242 onSchedulesChanged(); 243 notifyScheduledRecordingStatusChanged(scheduledRecordings); 244 } 245 }; 246 mDataManager.addScheduledRecordingListener(scheduledRecordingListener); 247 ChannelDataManager.Listener channelDataManagerListener = new ChannelDataManager.Listener() { 248 @Override 249 public void onLoadFinished() { 250 if (mDataManager.isDvrScheduleLoadFinished() && !mInitialized) { 251 buildData(); 252 } 253 } 254 255 @Override 256 public void onChannelListUpdated() { 257 if (mDataManager.isDvrScheduleLoadFinished()) { 258 buildData(); 259 } 260 } 261 262 @Override 263 public void onChannelBrowsableChanged() { 264 } 265 }; 266 mChannelDataManager.addListener(channelDataManagerListener); 267 } 268 269 /** 270 * Returns the started recordings for the given input. 271 */ 272 private List<ScheduledRecording> getStartedRecordings(String inputId) { 273 if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) { 274 return Collections.emptyList(); 275 } 276 List<ScheduledRecording> result = new ArrayList<>(); 277 List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); 278 if (schedules != null) { 279 for (ScheduledRecording schedule : schedules) { 280 if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { 281 result.add(schedule); 282 } 283 } 284 } 285 return result; 286 } 287 288 private void buildData() { 289 mInputScheduleMap.clear(); 290 for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) { 291 if (!schedule.isNotStarted() && !schedule.isInProgress()) { 292 continue; 293 } 294 Channel channel = mChannelDataManager.getChannel(schedule.getChannelId()); 295 if (channel != null) { 296 String inputId = channel.getInputId(); 297 // Do not check whether the input is valid or not. The input might be temporarily 298 // invalid. 299 List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); 300 if (schedules == null) { 301 schedules = new ArrayList<>(); 302 mInputScheduleMap.put(inputId, schedules); 303 } 304 schedules.add(schedule); 305 } 306 } 307 if (!mInitialized) { 308 mInitialized = true; 309 notifyInitialize(); 310 } 311 onSchedulesChanged(); 312 } 313 314 private void onSchedulesChanged() { 315 // TODO: notify conflict state change when some conflicting recording becomes partially 316 // conflicting, vice versa. 317 List<ScheduledRecording> addedConflicts = new ArrayList<>(); 318 List<ScheduledRecording> removedConflicts = new ArrayList<>(); 319 for (String inputId : mInputScheduleMap.keySet()) { 320 Map<ScheduledRecording, Boolean> oldConflictsInfo = mInputConflictInfoMap.get(inputId); 321 Map<Long, ScheduledRecording> oldConflictMap = new HashMap<>(); 322 if (oldConflictsInfo != null) { 323 for (ScheduledRecording r : oldConflictsInfo.keySet()) { 324 oldConflictMap.put(r.getId(), r); 325 } 326 } 327 Map<ScheduledRecording, Boolean> conflictInfo = getConflictingSchedulesInfo(inputId); 328 if (conflictInfo.isEmpty()) { 329 mInputConflictInfoMap.remove(inputId); 330 } else { 331 mInputConflictInfoMap.put(inputId, conflictInfo); 332 List<ScheduledRecording> conflicts = new ArrayList<>(conflictInfo.keySet()); 333 for (ScheduledRecording r : conflicts) { 334 if (oldConflictMap.remove(r.getId()) == null) { 335 addedConflicts.add(r); 336 } 337 } 338 } 339 removedConflicts.addAll(oldConflictMap.values()); 340 } 341 if (!removedConflicts.isEmpty()) { 342 notifyConflictStateChange(false, ScheduledRecording.toArray(removedConflicts)); 343 } 344 if (!addedConflicts.isEmpty()) { 345 notifyConflictStateChange(true, ScheduledRecording.toArray(addedConflicts)); 346 } 347 } 348 349 /** 350 * Returns {@code true} if this class has been initialized. 351 */ 352 public boolean isInitialized() { 353 return mInitialized; 354 } 355 356 /** 357 * Adds a {@link ScheduledRecordingListener}. 358 */ 359 public final void addScheduledRecordingListener(ScheduledRecordingListener listener) { 360 mScheduledRecordingListeners.add(listener); 361 } 362 363 /** 364 * Removes a {@link ScheduledRecordingListener}. 365 */ 366 public final void removeScheduledRecordingListener(ScheduledRecordingListener listener) { 367 mScheduledRecordingListeners.remove(listener); 368 } 369 370 /** 371 * Calls {@link ScheduledRecordingListener#onScheduledRecordingAdded} for each listener. 372 */ 373 private void notifyScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { 374 for (ScheduledRecordingListener l : mScheduledRecordingListeners) { 375 l.onScheduledRecordingAdded(scheduledRecordings); 376 } 377 } 378 379 /** 380 * Calls {@link ScheduledRecordingListener#onScheduledRecordingRemoved} for each listener. 381 */ 382 private void notifyScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { 383 for (ScheduledRecordingListener l : mScheduledRecordingListeners) { 384 l.onScheduledRecordingRemoved(scheduledRecordings); 385 } 386 } 387 388 /** 389 * Calls {@link ScheduledRecordingListener#onScheduledRecordingStatusChanged} for each listener. 390 */ 391 private void notifyScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { 392 for (ScheduledRecordingListener l : mScheduledRecordingListeners) { 393 l.onScheduledRecordingStatusChanged(scheduledRecordings); 394 } 395 } 396 397 /** 398 * Adds a {@link OnInitializeListener}. 399 */ 400 public final void addOnInitializeListener(OnInitializeListener listener) { 401 mOnInitializeListeners.add(listener); 402 } 403 404 /** 405 * Removes a {@link OnInitializeListener}. 406 */ 407 public final void removeOnInitializeListener(OnInitializeListener listener) { 408 mOnInitializeListeners.remove(listener); 409 } 410 411 /** 412 * Calls {@link OnInitializeListener#onInitialize} for each listener. 413 */ 414 private void notifyInitialize() { 415 for (OnInitializeListener l : mOnInitializeListeners) { 416 l.onInitialize(); 417 } 418 } 419 420 /** 421 * Adds a {@link OnConflictStateChangeListener}. 422 */ 423 public final void addOnConflictStateChangeListener(OnConflictStateChangeListener listener) { 424 mOnConflictStateChangeListeners.add(listener); 425 } 426 427 /** 428 * Removes a {@link OnConflictStateChangeListener}. 429 */ 430 public final void removeOnConflictStateChangeListener(OnConflictStateChangeListener listener) { 431 mOnConflictStateChangeListeners.remove(listener); 432 } 433 434 /** 435 * Calls {@link OnConflictStateChangeListener#onConflictStateChange} for each listener. 436 */ 437 private void notifyConflictStateChange(boolean conflict, 438 ScheduledRecording... scheduledRecordings) { 439 for (OnConflictStateChangeListener l : mOnConflictStateChangeListeners) { 440 l.onConflictStateChange(conflict, scheduledRecordings); 441 } 442 } 443 444 /** 445 * Returns the priority for the program if it is recorded. 446 * <p> 447 * The recording will have the higher priority than the existing ones. 448 */ 449 public long suggestNewPriority() { 450 if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) { 451 return DEFAULT_PRIORITY; 452 } 453 return suggestHighestPriority(); 454 } 455 456 private long suggestHighestPriority() { 457 long highestPriority = DEFAULT_PRIORITY - PRIORITY_OFFSET; 458 for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) { 459 if (schedule.getPriority() > highestPriority) { 460 highestPriority = schedule.getPriority(); 461 } 462 } 463 return highestPriority + PRIORITY_OFFSET; 464 } 465 466 /** 467 * Suggests the higher priority than the schedules which overlap with {@code schedule}. 468 */ 469 public long suggestHighestPriority(ScheduledRecording schedule) { 470 List<ScheduledRecording> schedules = mInputScheduleMap.get(schedule.getInputId()); 471 if (schedules == null) { 472 return DEFAULT_PRIORITY; 473 } 474 long highestPriority = Long.MIN_VALUE; 475 for (ScheduledRecording r : schedules) { 476 if (!r.equals(schedule) && r.isOverLapping(schedule) 477 && r.getPriority() > highestPriority) { 478 highestPriority = r.getPriority(); 479 } 480 } 481 if (highestPriority == Long.MIN_VALUE || highestPriority < schedule.getPriority()) { 482 return schedule.getPriority(); 483 } 484 return highestPriority + PRIORITY_OFFSET; 485 } 486 487 /** 488 * Suggests the higher priority than the schedules which overlap with {@code schedule}. 489 */ 490 public long suggestHighestPriority(String inputId, Range<Long> peroid, long basePriority) { 491 List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); 492 if (schedules == null) { 493 return DEFAULT_PRIORITY; 494 } 495 long highestPriority = Long.MIN_VALUE; 496 for (ScheduledRecording r : schedules) { 497 if (r.isOverLapping(peroid) && r.getPriority() > highestPriority) { 498 highestPriority = r.getPriority(); 499 } 500 } 501 if (highestPriority == Long.MIN_VALUE || highestPriority < basePriority) { 502 return basePriority; 503 } 504 return highestPriority + PRIORITY_OFFSET; 505 } 506 507 /** 508 * Returns the priority for a series recording. 509 * <p> 510 * The recording will have the higher priority than the existing series. 511 */ 512 public long suggestNewSeriesPriority() { 513 if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) { 514 return DEFAULT_SERIES_PRIORITY; 515 } 516 return suggestHighestSeriesPriority(); 517 } 518 519 /** 520 * Returns the priority for a series recording by order of series recording priority. 521 * 522 * Higher order will have higher priority. 523 */ 524 public static long suggestSeriesPriority(int order) { 525 return DEFAULT_SERIES_PRIORITY + order * PRIORITY_OFFSET; 526 } 527 528 private long suggestHighestSeriesPriority() { 529 long highestPriority = DEFAULT_SERIES_PRIORITY - PRIORITY_OFFSET; 530 for (SeriesRecording schedule : mDataManager.getSeriesRecordings()) { 531 if (schedule.getPriority() > highestPriority) { 532 highestPriority = schedule.getPriority(); 533 } 534 } 535 return highestPriority + PRIORITY_OFFSET; 536 } 537 538 /** 539 * Returns a sorted list of all scheduled recordings that will not be recorded if 540 * this program is going to be recorded, with their priorities in decending order. 541 * <p> 542 * An empty list means there is no conflicts. If there is conflict, a priority higher than 543 * the first recording in the returned list should be assigned to the new schedule of this 544 * program to guarantee the program would be completely recorded. 545 */ 546 public List<ScheduledRecording> getConflictingSchedules(Program program) { 547 SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); 548 SoftPreconditions.checkState(Program.isValid(program), TAG, 549 "Program is invalid: " + program); 550 SoftPreconditions.checkState( 551 program.getStartTimeUtcMillis() < program.getEndTimeUtcMillis(), TAG, 552 "Program duration is empty: " + program); 553 if (!mInitialized || !Program.isValid(program) 554 || program.getStartTimeUtcMillis() >= program.getEndTimeUtcMillis()) { 555 return Collections.emptyList(); 556 } 557 TvInputInfo input = Utils.getTvInputInfoForProgram(mContext, program); 558 if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { 559 return Collections.emptyList(); 560 } 561 return getConflictingSchedules(input, Collections.singletonList( 562 ScheduledRecording.builder(input.getId(), program) 563 .setPriority(suggestHighestPriority()) 564 .build())); 565 } 566 567 /** 568 * Returns list of all conflicting scheduled recordings with schedules belonging to {@code 569 * seriesRecording} 570 * recording. 571 * <p> 572 * Any empty list means there is no conflicts. 573 */ 574 public List<ScheduledRecording> getConflictingSchedules(SeriesRecording seriesRecording) { 575 SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); 576 SoftPreconditions.checkState(seriesRecording != null, TAG, "series recording is null"); 577 if (!mInitialized || seriesRecording == null) { 578 return Collections.emptyList(); 579 } 580 TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, seriesRecording.getInputId()); 581 if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { 582 return Collections.emptyList(); 583 } 584 List<ScheduledRecording> schedulesForSeries = mDataManager.getScheduledRecordings( 585 seriesRecording.getId()); 586 return getConflictingSchedules(input, schedulesForSeries); 587 } 588 589 /** 590 * Returns a sorted list of all scheduled recordings that will not be recorded if 591 * this channel is going to be recorded, with their priority in decending order. 592 * <p> 593 * An empty list means there is no conflicts. If there is conflict, a priority higher than 594 * the first recording in the returned list should be assigned to the new schedule of this 595 * channel to guarantee the channel would be completely recorded in the designated time range. 596 */ 597 public List<ScheduledRecording> getConflictingSchedules(long channelId, long startTimeMs, 598 long endTimeMs) { 599 SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); 600 SoftPreconditions.checkState(channelId != Channel.INVALID_ID, TAG, "Invalid channel ID"); 601 SoftPreconditions.checkState(startTimeMs < endTimeMs, TAG, "Recording duration is empty."); 602 if (!mInitialized || channelId == Channel.INVALID_ID || startTimeMs >= endTimeMs) { 603 return Collections.emptyList(); 604 } 605 TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, channelId); 606 if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { 607 return Collections.emptyList(); 608 } 609 return getConflictingSchedules(input, Collections.singletonList( 610 ScheduledRecording.builder(input.getId(), channelId, startTimeMs, endTimeMs) 611 .setPriority(suggestHighestPriority()) 612 .build())); 613 } 614 615 /** 616 * Returns all the scheduled recordings that conflicts and will not be recorded or clipped for 617 * the given input. 618 */ 619 @NonNull 620 private Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(String inputId) { 621 SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); 622 TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId); 623 SoftPreconditions.checkState(input != null, TAG, "Can't find input for : " + inputId); 624 if (!mInitialized || input == null) { 625 return Collections.emptyMap(); 626 } 627 List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId()); 628 if (schedules == null || schedules.isEmpty()) { 629 return Collections.emptyMap(); 630 } 631 return getConflictingSchedulesInfo(schedules, input.getTunerCount()); 632 } 633 634 /** 635 * Checks if the schedule is conflicting. 636 * 637 * <p>Note that the {@code schedule} should be the existing one. If not, this returns 638 * {@code false}. 639 */ 640 public boolean isConflicting(ScheduledRecording schedule) { 641 SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); 642 TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); 643 SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID : " 644 + schedule.getChannelId()); 645 if (!mInitialized || input == null) { 646 return false; 647 } 648 Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId()); 649 return conflicts != null && conflicts.containsKey(schedule); 650 } 651 652 /** 653 * Checks if the schedule is partially conflicting, i.e., part of the scheduled program might be 654 * recorded even if the priority of the schedule is not raised. 655 * <p> 656 * If the given schedule is not conflicting or is totally conflicting, i.e., cannot be recorded 657 * at all, this method returns {@code false} in both cases. 658 */ 659 public boolean isPartiallyConflicting(@NonNull ScheduledRecording schedule) { 660 SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); 661 TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); 662 SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID : " 663 + schedule.getChannelId()); 664 if (!mInitialized || input == null) { 665 return false; 666 } 667 Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId()); 668 return conflicts != null && conflicts.getOrDefault(schedule, false); 669 } 670 671 /** 672 * Returns priority ordered list of all scheduled recordings that will not be recorded if 673 * this channel is tuned to. 674 */ 675 public List<ScheduledRecording> getConflictingSchedulesForTune(long channelId) { 676 SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); 677 SoftPreconditions.checkState(channelId != Channel.INVALID_ID, TAG, "Invalid channel ID"); 678 TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, channelId); 679 SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID: " 680 + channelId); 681 if (!mInitialized || channelId == Channel.INVALID_ID || input == null) { 682 return Collections.emptyList(); 683 } 684 return getConflictingSchedulesForTune(input.getId(), channelId, System.currentTimeMillis(), 685 suggestHighestPriority(), getStartedRecordings(input.getId()), 686 input.getTunerCount()); 687 } 688 689 @VisibleForTesting 690 public static List<ScheduledRecording> getConflictingSchedulesForTune(String inputId, 691 long channelId, long currentTimeMs, long newPriority, 692 List<ScheduledRecording> startedRecordings, int tunerCount) { 693 boolean channelFound = false; 694 for (ScheduledRecording schedule : startedRecordings) { 695 if (schedule.getChannelId() == channelId) { 696 channelFound = true; 697 break; 698 } 699 } 700 List<ScheduledRecording> schedules; 701 if (!channelFound) { 702 // The current channel is not being recorded. 703 schedules = new ArrayList<>(startedRecordings); 704 schedules.add(ScheduledRecording 705 .builder(inputId, channelId, currentTimeMs, currentTimeMs + 1) 706 .setPriority(newPriority) 707 .build()); 708 } else { 709 schedules = startedRecordings; 710 } 711 return getConflictingSchedules(schedules, tunerCount); 712 } 713 714 /** 715 * Returns priority ordered list of all scheduled recordings that will not be recorded if 716 * the user keeps watching this channel. 717 * <p> 718 * Note that if the user keeps watching the channel, the channel can be recorded. 719 */ 720 public List<ScheduledRecording> getConflictingSchedulesForWatching(long channelId) { 721 SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); 722 SoftPreconditions.checkState(channelId != Channel.INVALID_ID, TAG, "Invalid channel ID"); 723 TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, channelId); 724 SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID: " 725 + channelId); 726 if (!mInitialized || channelId == Channel.INVALID_ID || input == null) { 727 return Collections.emptyList(); 728 } 729 List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId()); 730 if (schedules == null || schedules.isEmpty()) { 731 return Collections.emptyList(); 732 } 733 return getConflictingSchedulesForWatching(input.getId(), channelId, 734 System.currentTimeMillis(), suggestNewPriority(), schedules, input.getTunerCount()); 735 } 736 737 private List<ScheduledRecording> getConflictingSchedules(TvInputInfo input, 738 List<ScheduledRecording> schedulesToAdd) { 739 SoftPreconditions.checkNotNull(input); 740 if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { 741 return Collections.emptyList(); 742 } 743 List<ScheduledRecording> currentSchedules = mInputScheduleMap.get(input.getId()); 744 if (currentSchedules == null || currentSchedules.isEmpty()) { 745 return Collections.emptyList(); 746 } 747 return getConflictingSchedules(schedulesToAdd, currentSchedules, input.getTunerCount()); 748 } 749 750 @VisibleForTesting 751 static List<ScheduledRecording> getConflictingSchedulesForWatching(String inputId, 752 long channelId, long currentTimeMs, long newPriority, 753 @NonNull List<ScheduledRecording> schedules, int tunerCount) { 754 List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules); 755 List<ScheduledRecording> schedulesSameChannel = new ArrayList<>(); 756 for (ScheduledRecording schedule : schedules) { 757 if (schedule.getChannelId() == channelId) { 758 schedulesSameChannel.add(schedule); 759 schedulesToCheck.remove(schedule); 760 } 761 } 762 // Assume that the user will watch the current channel forever. 763 schedulesToCheck.add(ScheduledRecording 764 .builder(inputId, channelId, currentTimeMs, Long.MAX_VALUE) 765 .setPriority(newPriority) 766 .build()); 767 List<ScheduledRecording> result = new ArrayList<>(); 768 result.addAll(getConflictingSchedules(schedulesSameChannel, 1)); 769 result.addAll(getConflictingSchedules(schedulesToCheck, tunerCount)); 770 Collections.sort(result, RESULT_COMPARATOR); 771 return result; 772 } 773 774 @VisibleForTesting 775 static List<ScheduledRecording> getConflictingSchedules(List<ScheduledRecording> schedulesToAdd, 776 List<ScheduledRecording> currentSchedules, int tunerCount) { 777 List<ScheduledRecording> schedulesToCheck = new ArrayList<>(currentSchedules); 778 // When the duplicate schedule is to be added, remove the current duplicate recording. 779 for (Iterator<ScheduledRecording> iter = schedulesToCheck.iterator(); iter.hasNext(); ) { 780 ScheduledRecording schedule = iter.next(); 781 for (ScheduledRecording toAdd : schedulesToAdd) { 782 if (schedule.getType() == ScheduledRecording.TYPE_PROGRAM) { 783 if (toAdd.getProgramId() == schedule.getProgramId()) { 784 iter.remove(); 785 break; 786 } 787 } else { 788 if (toAdd.getChannelId() == schedule.getChannelId() 789 && toAdd.getStartTimeMs() == schedule.getStartTimeMs() 790 && toAdd.getEndTimeMs() == schedule.getEndTimeMs()) { 791 iter.remove(); 792 break; 793 } 794 } 795 } 796 } 797 schedulesToCheck.addAll(schedulesToAdd); 798 List<Range<Long>> ranges = new ArrayList<>(); 799 for (ScheduledRecording schedule : schedulesToAdd) { 800 ranges.add(new Range<>(schedule.getStartTimeMs(), schedule.getEndTimeMs())); 801 } 802 return getConflictingSchedules(schedulesToCheck, tunerCount, ranges); 803 } 804 805 /** 806 * Returns all conflicting scheduled recordings for the given schedules and count of tuner. 807 */ 808 public static List<ScheduledRecording> getConflictingSchedules( 809 List<ScheduledRecording> schedules, int tunerCount) { 810 return getConflictingSchedules(schedules, tunerCount, null); 811 } 812 813 @VisibleForTesting 814 static List<ScheduledRecording> getConflictingSchedules( 815 List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) { 816 List<ScheduledRecording> result = new ArrayList<>( 817 getConflictingSchedulesInfo(schedules, tunerCount, periods).keySet()); 818 Collections.sort(result, RESULT_COMPARATOR); 819 return result; 820 } 821 822 @VisibleForTesting 823 static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo( 824 List<ScheduledRecording> schedules, int tunerCount) { 825 return getConflictingSchedulesInfo(schedules, tunerCount, null); 826 } 827 828 /** 829 * This is the core method to calculate all the conflicting schedules (in given periods). 830 * <p> 831 * Note that this method will ignore duplicated schedules with a same hash code. (Please refer 832 * to {@link ScheduledRecording#hashCode}.) 833 * 834 * @return A {@link HashMap} from {@link ScheduledRecording} to {@link Boolean}. The boolean 835 * value denotes if the scheduled recording is partially conflicting, i.e., is possible 836 * to be partially recorded under the given schedules and tuner count {@code true}, 837 * or not {@code false}. 838 */ 839 private static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo( 840 List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) { 841 List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules); 842 // Sort by the same order as that in InputTaskScheduler. 843 Collections.sort(schedulesToCheck, InputTaskScheduler.getRecordingOrderComparator()); 844 List<ScheduledRecording> recordings = new ArrayList<>(); 845 Map<ScheduledRecording, Boolean> conflicts = new HashMap<>(); 846 Map<ScheduledRecording, ScheduledRecording> modified2OriginalSchedules = new HashMap<>(); 847 // Simulate InputTaskScheduler. 848 while (!schedulesToCheck.isEmpty()) { 849 ScheduledRecording schedule = schedulesToCheck.remove(0); 850 removeFinishedRecordings(recordings, schedule.getStartTimeMs()); 851 if (recordings.size() < tunerCount) { 852 recordings.add(schedule); 853 if (modified2OriginalSchedules.containsKey(schedule)) { 854 // Schedule has been modified, which means it's already conflicted. 855 // Modify its state to partially conflicted. 856 conflicts.put(modified2OriginalSchedules.get(schedule), true); 857 } 858 } else { 859 ScheduledRecording candidate = findReplaceableRecording(recordings, schedule); 860 if (candidate != null) { 861 if (!modified2OriginalSchedules.containsKey(candidate)) { 862 conflicts.put(candidate, true); 863 } 864 recordings.remove(candidate); 865 recordings.add(schedule); 866 if (modified2OriginalSchedules.containsKey(schedule)) { 867 // Schedule has been modified, which means it's already conflicted. 868 // Modify its state to partially conflicted. 869 conflicts.put(modified2OriginalSchedules.get(schedule), true); 870 } 871 } else { 872 if (!modified2OriginalSchedules.containsKey(schedule)) { 873 // if schedule has been modified, it's already conflicted. 874 // No need to add it again. 875 conflicts.put(schedule, false); 876 } 877 long earliestEndTime = getEarliestEndTime(recordings); 878 if (earliestEndTime < schedule.getEndTimeMs()) { 879 // The schedule can starts when other recording ends even though it's 880 // clipped. 881 ScheduledRecording modifiedSchedule = ScheduledRecording.buildFrom(schedule) 882 .setStartTimeMs(earliestEndTime).build(); 883 ScheduledRecording originalSchedule = 884 modified2OriginalSchedules.getOrDefault(schedule, schedule); 885 modified2OriginalSchedules.put(modifiedSchedule, originalSchedule); 886 int insertPosition = Collections.binarySearch(schedulesToCheck, 887 modifiedSchedule, 888 ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); 889 if (insertPosition >= 0) { 890 schedulesToCheck.add(insertPosition, modifiedSchedule); 891 } else { 892 schedulesToCheck.add(-insertPosition - 1, modifiedSchedule); 893 } 894 } 895 } 896 } 897 } 898 // Returns only the schedules with the given range. 899 if (periods != null && !periods.isEmpty()) { 900 for (Iterator<ScheduledRecording> iter = conflicts.keySet().iterator(); 901 iter.hasNext(); ) { 902 boolean overlapping = false; 903 ScheduledRecording schedule = iter.next(); 904 for (Range<Long> period : periods) { 905 if (schedule.isOverLapping(period)) { 906 overlapping = true; 907 break; 908 } 909 } 910 if (!overlapping) { 911 iter.remove(); 912 } 913 } 914 } 915 return conflicts; 916 } 917 918 private static void removeFinishedRecordings(List<ScheduledRecording> recordings, 919 long currentTimeMs) { 920 for (Iterator<ScheduledRecording> iter = recordings.iterator(); iter.hasNext(); ) { 921 if (iter.next().getEndTimeMs() <= currentTimeMs) { 922 iter.remove(); 923 } 924 } 925 } 926 927 /** 928 * @see InputTaskScheduler#getReplacableTask 929 */ 930 private static ScheduledRecording findReplaceableRecording(List<ScheduledRecording> recordings, 931 ScheduledRecording schedule) { 932 // Returns the recording with the following priority. 933 // 1. The recording with the lowest priority is returned. 934 // 2. If the priorities are the same, the recording which finishes early is returned. 935 // 3. If 1) and 2) are the same, the early created schedule is returned. 936 ScheduledRecording candidate = null; 937 for (ScheduledRecording recording : recordings) { 938 if (schedule.getPriority() > recording.getPriority()) { 939 if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, recording) > 0) { 940 candidate = recording; 941 } 942 } 943 } 944 return candidate; 945 } 946 947 private static long getEarliestEndTime(List<ScheduledRecording> recordings) { 948 long earliest = Long.MAX_VALUE; 949 for (ScheduledRecording recording : recordings) { 950 if (earliest > recording.getEndTimeMs()) { 951 earliest = recording.getEndTimeMs(); 952 } 953 } 954 return earliest; 955 } 956 957 /** 958 * A listener which is notified the initialization of schedule manager. 959 */ 960 public interface OnInitializeListener { 961 /** 962 * Called when the schedule manager has been initialized. 963 */ 964 void onInitialize(); 965 } 966 967 /** 968 * A listener which is notified the conflict state change of the schedules. 969 */ 970 public interface OnConflictStateChangeListener { 971 /** 972 * Called when the conflicting schedules change. 973 * 974 * @param conflict {@code true} if the {@code schedules} are the new conflicts, otherwise 975 * {@code false}. 976 * @param schedules the schedules 977 */ 978 void onConflictStateChange(boolean conflict, ScheduledRecording... schedules); 979 } 980 }