• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
DvrScheduleManager(Context context)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. */
getStartedRecordings(String inputId)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 
buildData()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 
onSchedulesChanged()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. */
isInitialized()338     public boolean isInitialized() {
339         return mInitialized;
340     }
341 
342     /** Adds a {@link ScheduledRecordingListener}. */
addScheduledRecordingListener(ScheduledRecordingListener listener)343     public final void addScheduledRecordingListener(ScheduledRecordingListener listener) {
344         mScheduledRecordingListeners.add(listener);
345     }
346 
347     /** Removes a {@link ScheduledRecordingListener}. */
removeScheduledRecordingListener(ScheduledRecordingListener listener)348     public final void removeScheduledRecordingListener(ScheduledRecordingListener listener) {
349         mScheduledRecordingListeners.remove(listener);
350     }
351 
352     /** Calls {@link ScheduledRecordingListener#onScheduledRecordingAdded} for each listener. */
notifyScheduledRecordingAdded(ScheduledRecording... scheduledRecordings)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. */
notifyScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings)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      */
notifyScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings)369     private void notifyScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
370         for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
371             l.onScheduledRecordingStatusChanged(scheduledRecordings);
372         }
373     }
374 
375     /** Adds a {@link OnInitializeListener}. */
addOnInitializeListener(OnInitializeListener listener)376     public final void addOnInitializeListener(OnInitializeListener listener) {
377         mOnInitializeListeners.add(listener);
378     }
379 
380     /** Removes a {@link OnInitializeListener}. */
removeOnInitializeListener(OnInitializeListener listener)381     public final void removeOnInitializeListener(OnInitializeListener listener) {
382         mOnInitializeListeners.remove(listener);
383     }
384 
385     /** Calls {@link OnInitializeListener#onInitialize} for each listener. */
notifyInitialize()386     private void notifyInitialize() {
387         for (OnInitializeListener l : mOnInitializeListeners) {
388             l.onInitialize();
389         }
390     }
391 
392     /** Adds a {@link OnConflictStateChangeListener}. */
addOnConflictStateChangeListener(OnConflictStateChangeListener listener)393     public final void addOnConflictStateChangeListener(OnConflictStateChangeListener listener) {
394         mOnConflictStateChangeListeners.add(listener);
395     }
396 
397     /** Removes a {@link OnConflictStateChangeListener}. */
removeOnConflictStateChangeListener(OnConflictStateChangeListener listener)398     public final void removeOnConflictStateChangeListener(OnConflictStateChangeListener listener) {
399         mOnConflictStateChangeListeners.remove(listener);
400     }
401 
402     /** Calls {@link OnConflictStateChangeListener#onConflictStateChange} for each listener. */
notifyConflictStateChange( boolean conflict, ScheduledRecording... scheduledRecordings)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      */
suggestNewPriority()415     public long suggestNewPriority() {
416         if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) {
417             return DEFAULT_PRIORITY;
418         }
419         return suggestHighestPriority();
420     }
421 
suggestHighestPriority()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}. */
suggestHighestPriority(ScheduledRecording 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}. */
suggestHighestPriority(String inputId, Range<Long> peroid, long basePriority)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      */
suggestNewSeriesPriority()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      */
suggestSeriesPriority(int order)487     public static long suggestSeriesPriority(int order) {
488         return DEFAULT_SERIES_PRIORITY + order * PRIORITY_OFFSET;
489     }
490 
suggestHighestSeriesPriority()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      */
getConflictingSchedules(Program program)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      */
getConflictingSchedules(SeriesRecording seriesRecording)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      */
getConflictingSchedules( long channelId, long startTimeMs, long endTimeMs)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
getConflictingSchedulesInfo(String inputId)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      */
isConflicting(ScheduledRecording schedule)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      */
isPartiallyConflicting(@onNull ScheduledRecording schedule)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      */
getConflictingSchedulesForTune(long channelId)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
getConflictingSchedulesForTune( String inputId, long channelId, long currentTimeMs, long newPriority, List<ScheduledRecording> startedRecordings, int tunerCount)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      */
getConflictingSchedulesForWatching(long channelId)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 
getConflictingSchedules( TvInputInfo input, List<ScheduledRecording> schedulesToAdd)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
getConflictingSchedulesForWatching( String inputId, long channelId, long currentTimeMs, long newPriority, @NonNull List<ScheduledRecording> schedules, int tunerCount)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
getConflictingSchedules( List<ScheduledRecording> schedulesToAdd, List<ScheduledRecording> currentSchedules, int tunerCount)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. */
getConflictingSchedules( List<ScheduledRecording> schedules, int tunerCount)806     public static List<ScheduledRecording> getConflictingSchedules(
807             List<ScheduledRecording> schedules, int tunerCount) {
808         return getConflictingSchedules(schedules, tunerCount, null);
809     }
810 
811     @VisibleForTesting
getConflictingSchedules( List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods)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
getConflictingSchedulesInfo( List<ScheduledRecording> schedules, int tunerCount)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      */
getConflictingSchedulesInfo( List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods)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                 (ConflictInfo lhs, ConflictInfo rhs) ->
927                         RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule));
928         return result;
929     }
930 
removeFinishedRecordings( List<ScheduledRecording> recordings, long currentTimeMs)931     private static void removeFinishedRecordings(
932             List<ScheduledRecording> recordings, long currentTimeMs) {
933         for (Iterator<ScheduledRecording> iter = recordings.iterator(); iter.hasNext(); ) {
934             if (iter.next().getEndTimeMs() <= currentTimeMs) {
935                 iter.remove();
936             }
937         }
938     }
939 
940     /** @see InputTaskScheduler#getReplacableTask */
findReplaceableRecording( List<ScheduledRecording> recordings, ScheduledRecording schedule)941     private static ScheduledRecording findReplaceableRecording(
942             List<ScheduledRecording> recordings, ScheduledRecording schedule) {
943         // Returns the recording with the following priority.
944         // 1. The recording with the lowest priority is returned.
945         // 2. If the priorities are the same, the recording which finishes early is returned.
946         // 3. If 1) and 2) are the same, the early created schedule is returned.
947         ScheduledRecording candidate = null;
948         for (ScheduledRecording recording : recordings) {
949             if (schedule.getPriority() > recording.getPriority()) {
950                 if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, recording) > 0) {
951                     candidate = recording;
952                 }
953             }
954         }
955         return candidate;
956     }
957 
getEarliestEndTime(List<ScheduledRecording> recordings)958     private static long getEarliestEndTime(List<ScheduledRecording> recordings) {
959         long earliest = Long.MAX_VALUE;
960         for (ScheduledRecording recording : recordings) {
961             if (earliest > recording.getEndTimeMs()) {
962                 earliest = recording.getEndTimeMs();
963             }
964         }
965         return earliest;
966     }
967 
968     @VisibleForTesting
969     static class ConflictInfo {
970         public ScheduledRecording schedule;
971         public boolean partialConflict;
972 
ConflictInfo(ScheduledRecording schedule, boolean partialConflict)973         ConflictInfo(ScheduledRecording schedule, boolean partialConflict) {
974             this.schedule = schedule;
975             this.partialConflict = partialConflict;
976         }
977     }
978 
979     /** A listener which is notified the initialization of schedule manager. */
980     public interface OnInitializeListener {
981         /** Called when the schedule manager has been initialized. */
onInitialize()982         void onInitialize();
983     }
984 
985     /** A listener which is notified the conflict state change of the schedules. */
986     public interface OnConflictStateChangeListener {
987         /**
988          * Called when the conflicting schedules change.
989          *
990          * <p>Note that this can be called before {@link
991          * ScheduledRecordingListener#onScheduledRecordingAdded} is called.
992          *
993          * @param conflict {@code true} if the {@code schedules} are the new conflicts, otherwise
994          *     {@code false}.
995          * @param schedules the schedules
996          */
onConflictStateChange(boolean conflict, ScheduledRecording... schedules)997         void onConflictStateChange(boolean conflict, ScheduledRecording... schedules);
998     }
999 }
1000