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