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