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