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