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