165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko/*
265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Copyright (C) 2015 The Android Open Source Project
365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko *
465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Licensed under the Apache License, Version 2.0 (the "License");
565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * you may not use this file except in compliance with the License.
665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * You may obtain a copy of the License at
765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko *
865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko *      http://www.apache.org/licenses/LICENSE-2.0
965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko *
1065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Unless required by applicable law or agreed to in writing, software
1165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * distributed under the License is distributed on an "AS IS" BASIS,
1265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * See the License for the specific language governing permissions and
1465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * limitations under the License.
1565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko */
1665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
1765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkopackage com.android.tv.dvr;
1865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
1965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport android.annotation.TargetApi;
2065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport android.content.Context;
2165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport android.media.tv.TvInputInfo;
2265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport android.os.Build;
2365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport android.support.annotation.MainThread;
2465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport android.support.annotation.NonNull;
2565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport android.support.annotation.VisibleForTesting;
2665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport android.util.ArraySet;
2765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport android.util.Range;
28944779887775bd950cf1abf348d2df461593f6abLive Channels Teamimport com.android.tv.TvSingletons;
2965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport com.android.tv.common.SoftPreconditions;
3065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport com.android.tv.data.ChannelDataManager;
3165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport com.android.tv.data.Program;
320cc0713c1bf8027642987b750b80217569d2932aLive Channels Teamimport com.android.tv.data.api.Channel;
3365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
3465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
35633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalkoimport com.android.tv.dvr.data.ScheduledRecording;
36633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalkoimport com.android.tv.dvr.data.SeriesRecording;
3795961816a768da387f0b5523cf4363ace2044089Nick Chalkoimport com.android.tv.dvr.recorder.InputTaskScheduler;
3895961816a768da387f0b5523cf4363ace2044089Nick Chalkoimport com.android.tv.util.CompositeComparator;
3965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport com.android.tv.util.Utils;
4065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport java.util.ArrayList;
4165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport java.util.Collections;
42d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalkoimport java.util.Comparator;
4365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport java.util.HashMap;
4465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport java.util.Iterator;
4565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport java.util.List;
4665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport java.util.Map;
4765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport java.util.Set;
48d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalkoimport java.util.concurrent.CopyOnWriteArraySet;
4965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
5095961816a768da387f0b5523cf4363ace2044089Nick Chalko/** A class to manage the schedules. */
5165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko@TargetApi(Build.VERSION_CODES.N)
5265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko@MainThread
53944779887775bd950cf1abf348d2df461593f6abLive Channels Team@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
5465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkopublic class DvrScheduleManager {
5565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private static final String TAG = "DvrScheduleManager";
5665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
5795961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** The default priority of scheduled recording. */
5865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1;
5995961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** The default priority of series recording. */
6065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public static final long DEFAULT_SERIES_PRIORITY = DEFAULT_PRIORITY >> 1;
6165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    // The new priority will have the offset from the existing one.
6265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private static final long PRIORITY_OFFSET = 1024;
6365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
64d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    private static final Comparator<ScheduledRecording> RESULT_COMPARATOR =
65d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            new CompositeComparator<>(
66d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    ScheduledRecording.PRIORITY_COMPARATOR.reversed(),
67d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    ScheduledRecording.START_TIME_COMPARATOR,
68d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    ScheduledRecording.ID_COMPARATOR.reversed());
69d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
70d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    // The candidate comparator should be the consistent with
71d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    // InputTaskScheduler#CANDIDATE_COMPARATOR.
72d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    private static final Comparator<ScheduledRecording> CANDIDATE_COMPARATOR =
73d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            new CompositeComparator<>(
74d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    ScheduledRecording.PRIORITY_COMPARATOR,
75d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    ScheduledRecording.END_TIME_COMPARATOR,
76d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    ScheduledRecording.ID_COMPARATOR);
77d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
7865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private final Context mContext;
7965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private final DvrDataManagerImpl mDataManager;
8065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private final ChannelDataManager mChannelDataManager;
8165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
8265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private final Map<String, List<ScheduledRecording>> mInputScheduleMap = new HashMap<>();
83d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    // The inner map is a hash map from scheduled recording to its conflicting status, i.e.,
84d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    // the boolean value true denotes the schedule is just partially conflicting, which means
85633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko    // although there's conflict, it might still be recorded partially.
86633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko    private final Map<String, Map<Long, ConflictInfo>> mInputConflictInfoMap = new HashMap<>();
8765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
8865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private boolean mInitialized;
8965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
90d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    private final Set<OnInitializeListener> mOnInitializeListeners = new CopyOnWriteArraySet<>();
9165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private final Set<ScheduledRecordingListener> mScheduledRecordingListeners = new ArraySet<>();
9265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private final Set<OnConflictStateChangeListener> mOnConflictStateChangeListeners =
9365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            new ArraySet<>();
9465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
9565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public DvrScheduleManager(Context context) {
9665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        mContext = context;
97944779887775bd950cf1abf348d2df461593f6abLive Channels Team        TvSingletons tvSingletons = TvSingletons.getSingletons(context);
98944779887775bd950cf1abf348d2df461593f6abLive Channels Team        mDataManager = (DvrDataManagerImpl) tvSingletons.getDvrDataManager();
99944779887775bd950cf1abf348d2df461593f6abLive Channels Team        mChannelDataManager = tvSingletons.getChannelDataManager();
10065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) {
10165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            buildData();
10265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        } else {
10365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            mDataManager.addDvrScheduleLoadFinishedListener(
10465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                    new OnDvrScheduleLoadFinishedListener() {
10565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                        @Override
10665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                        public void onDvrScheduleLoadFinished() {
10765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                            mDataManager.removeDvrScheduleLoadFinishedListener(this);
10865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                            if (mChannelDataManager.isDbLoadFinished() && !mInitialized) {
10965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                                buildData();
11065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                            }
11165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                        }
11265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                    });
11365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
11495961816a768da387f0b5523cf4363ace2044089Nick Chalko        ScheduledRecordingListener scheduledRecordingListener =
11595961816a768da387f0b5523cf4363ace2044089Nick Chalko                new ScheduledRecordingListener() {
11695961816a768da387f0b5523cf4363ace2044089Nick Chalko                    @Override
11795961816a768da387f0b5523cf4363ace2044089Nick Chalko                    public void onScheduledRecordingAdded(
11895961816a768da387f0b5523cf4363ace2044089Nick Chalko                            ScheduledRecording... scheduledRecordings) {
11995961816a768da387f0b5523cf4363ace2044089Nick Chalko                        if (!mInitialized) {
12095961816a768da387f0b5523cf4363ace2044089Nick Chalko                            return;
12195961816a768da387f0b5523cf4363ace2044089Nick Chalko                        }
12295961816a768da387f0b5523cf4363ace2044089Nick Chalko                        for (ScheduledRecording schedule : scheduledRecordings) {
12395961816a768da387f0b5523cf4363ace2044089Nick Chalko                            if (!schedule.isNotStarted() && !schedule.isInProgress()) {
12495961816a768da387f0b5523cf4363ace2044089Nick Chalko                                continue;
12595961816a768da387f0b5523cf4363ace2044089Nick Chalko                            }
12695961816a768da387f0b5523cf4363ace2044089Nick Chalko                            TvInputInfo input =
12795961816a768da387f0b5523cf4363ace2044089Nick Chalko                                    Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
12895961816a768da387f0b5523cf4363ace2044089Nick Chalko                            if (!SoftPreconditions.checkArgument(
129944779887775bd950cf1abf348d2df461593f6abLive Channels Team                                    input != null, TAG, "Input was removed for : %s", schedule)) {
13095961816a768da387f0b5523cf4363ace2044089Nick Chalko                                // Input removed.
13195961816a768da387f0b5523cf4363ace2044089Nick Chalko                                mInputScheduleMap.remove(schedule.getInputId());
13295961816a768da387f0b5523cf4363ace2044089Nick Chalko                                mInputConflictInfoMap.remove(schedule.getInputId());
13395961816a768da387f0b5523cf4363ace2044089Nick Chalko                                continue;
13495961816a768da387f0b5523cf4363ace2044089Nick Chalko                            }
13595961816a768da387f0b5523cf4363ace2044089Nick Chalko                            String inputId = input.getId();
13695961816a768da387f0b5523cf4363ace2044089Nick Chalko                            List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId);
13795961816a768da387f0b5523cf4363ace2044089Nick Chalko                            if (schedules == null) {
13895961816a768da387f0b5523cf4363ace2044089Nick Chalko                                schedules = new ArrayList<>();
13995961816a768da387f0b5523cf4363ace2044089Nick Chalko                                mInputScheduleMap.put(inputId, schedules);
14095961816a768da387f0b5523cf4363ace2044089Nick Chalko                            }
14195961816a768da387f0b5523cf4363ace2044089Nick Chalko                            schedules.add(schedule);
14295961816a768da387f0b5523cf4363ace2044089Nick Chalko                        }
14395961816a768da387f0b5523cf4363ace2044089Nick Chalko                        onSchedulesChanged();
14495961816a768da387f0b5523cf4363ace2044089Nick Chalko                        notifyScheduledRecordingAdded(scheduledRecordings);
14565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                    }
14665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
14795961816a768da387f0b5523cf4363ace2044089Nick Chalko                    @Override
14895961816a768da387f0b5523cf4363ace2044089Nick Chalko                    public void onScheduledRecordingRemoved(
14995961816a768da387f0b5523cf4363ace2044089Nick Chalko                            ScheduledRecording... scheduledRecordings) {
15095961816a768da387f0b5523cf4363ace2044089Nick Chalko                        if (!mInitialized) {
15195961816a768da387f0b5523cf4363ace2044089Nick Chalko                            return;
15265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                        }
15395961816a768da387f0b5523cf4363ace2044089Nick Chalko                        for (ScheduledRecording schedule : scheduledRecordings) {
15495961816a768da387f0b5523cf4363ace2044089Nick Chalko                            TvInputInfo input =
15595961816a768da387f0b5523cf4363ace2044089Nick Chalko                                    Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
15695961816a768da387f0b5523cf4363ace2044089Nick Chalko                            if (input == null) {
15795961816a768da387f0b5523cf4363ace2044089Nick Chalko                                // Input removed.
15895961816a768da387f0b5523cf4363ace2044089Nick Chalko                                mInputScheduleMap.remove(schedule.getInputId());
15995961816a768da387f0b5523cf4363ace2044089Nick Chalko                                mInputConflictInfoMap.remove(schedule.getInputId());
16095961816a768da387f0b5523cf4363ace2044089Nick Chalko                                continue;
16195961816a768da387f0b5523cf4363ace2044089Nick Chalko                            }
16295961816a768da387f0b5523cf4363ace2044089Nick Chalko                            String inputId = input.getId();
16395961816a768da387f0b5523cf4363ace2044089Nick Chalko                            List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId);
16495961816a768da387f0b5523cf4363ace2044089Nick Chalko                            if (schedules != null) {
16595961816a768da387f0b5523cf4363ace2044089Nick Chalko                                schedules.remove(schedule);
16695961816a768da387f0b5523cf4363ace2044089Nick Chalko                                if (schedules.isEmpty()) {
16795961816a768da387f0b5523cf4363ace2044089Nick Chalko                                    mInputScheduleMap.remove(inputId);
16895961816a768da387f0b5523cf4363ace2044089Nick Chalko                                }
16995961816a768da387f0b5523cf4363ace2044089Nick Chalko                            }
17095961816a768da387f0b5523cf4363ace2044089Nick Chalko                            Map<Long, ConflictInfo> conflictInfo =
17195961816a768da387f0b5523cf4363ace2044089Nick Chalko                                    mInputConflictInfoMap.get(inputId);
17295961816a768da387f0b5523cf4363ace2044089Nick Chalko                            if (conflictInfo != null) {
17395961816a768da387f0b5523cf4363ace2044089Nick Chalko                                conflictInfo.remove(schedule.getId());
17495961816a768da387f0b5523cf4363ace2044089Nick Chalko                                if (conflictInfo.isEmpty()) {
17595961816a768da387f0b5523cf4363ace2044089Nick Chalko                                    mInputConflictInfoMap.remove(inputId);
17695961816a768da387f0b5523cf4363ace2044089Nick Chalko                                }
17795961816a768da387f0b5523cf4363ace2044089Nick Chalko                            }
178d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        }
17995961816a768da387f0b5523cf4363ace2044089Nick Chalko                        onSchedulesChanged();
18095961816a768da387f0b5523cf4363ace2044089Nick Chalko                        notifyScheduledRecordingRemoved(scheduledRecordings);
181d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    }
18265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
18395961816a768da387f0b5523cf4363ace2044089Nick Chalko                    @Override
18495961816a768da387f0b5523cf4363ace2044089Nick Chalko                    public void onScheduledRecordingStatusChanged(
18595961816a768da387f0b5523cf4363ace2044089Nick Chalko                            ScheduledRecording... scheduledRecordings) {
18695961816a768da387f0b5523cf4363ace2044089Nick Chalko                        if (!mInitialized) {
18795961816a768da387f0b5523cf4363ace2044089Nick Chalko                            return;
18865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                        }
18995961816a768da387f0b5523cf4363ace2044089Nick Chalko                        for (ScheduledRecording schedule : scheduledRecordings) {
19095961816a768da387f0b5523cf4363ace2044089Nick Chalko                            TvInputInfo input =
19195961816a768da387f0b5523cf4363ace2044089Nick Chalko                                    Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
19295961816a768da387f0b5523cf4363ace2044089Nick Chalko                            if (!SoftPreconditions.checkArgument(
193944779887775bd950cf1abf348d2df461593f6abLive Channels Team                                    input != null, TAG, "Input was removed for : %s", schedule)) {
19495961816a768da387f0b5523cf4363ace2044089Nick Chalko                                // Input removed.
19595961816a768da387f0b5523cf4363ace2044089Nick Chalko                                mInputScheduleMap.remove(schedule.getInputId());
19695961816a768da387f0b5523cf4363ace2044089Nick Chalko                                mInputConflictInfoMap.remove(schedule.getInputId());
19795961816a768da387f0b5523cf4363ace2044089Nick Chalko                                continue;
19895961816a768da387f0b5523cf4363ace2044089Nick Chalko                            }
19995961816a768da387f0b5523cf4363ace2044089Nick Chalko                            String inputId = input.getId();
20095961816a768da387f0b5523cf4363ace2044089Nick Chalko                            List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId);
20195961816a768da387f0b5523cf4363ace2044089Nick Chalko                            if (schedules == null) {
20295961816a768da387f0b5523cf4363ace2044089Nick Chalko                                schedules = new ArrayList<>();
20395961816a768da387f0b5523cf4363ace2044089Nick Chalko                                mInputScheduleMap.put(inputId, schedules);
20495961816a768da387f0b5523cf4363ace2044089Nick Chalko                            }
20595961816a768da387f0b5523cf4363ace2044089Nick Chalko                            // Compare ID because ScheduledRecording.equals() doesn't work if the
20695961816a768da387f0b5523cf4363ace2044089Nick Chalko                            // state
20795961816a768da387f0b5523cf4363ace2044089Nick Chalko                            // is changed.
20895961816a768da387f0b5523cf4363ace2044089Nick Chalko                            for (Iterator<ScheduledRecording> i = schedules.iterator();
20995961816a768da387f0b5523cf4363ace2044089Nick Chalko                                    i.hasNext(); ) {
21095961816a768da387f0b5523cf4363ace2044089Nick Chalko                                if (i.next().getId() == schedule.getId()) {
21195961816a768da387f0b5523cf4363ace2044089Nick Chalko                                    i.remove();
21295961816a768da387f0b5523cf4363ace2044089Nick Chalko                                    break;
21395961816a768da387f0b5523cf4363ace2044089Nick Chalko                                }
21495961816a768da387f0b5523cf4363ace2044089Nick Chalko                            }
21595961816a768da387f0b5523cf4363ace2044089Nick Chalko                            if (schedule.isNotStarted() || schedule.isInProgress()) {
21695961816a768da387f0b5523cf4363ace2044089Nick Chalko                                schedules.add(schedule);
21795961816a768da387f0b5523cf4363ace2044089Nick Chalko                            }
21895961816a768da387f0b5523cf4363ace2044089Nick Chalko                            if (schedules.isEmpty()) {
21995961816a768da387f0b5523cf4363ace2044089Nick Chalko                                mInputScheduleMap.remove(inputId);
22095961816a768da387f0b5523cf4363ace2044089Nick Chalko                            }
22195961816a768da387f0b5523cf4363ace2044089Nick Chalko                            // Update conflict list as well
22295961816a768da387f0b5523cf4363ace2044089Nick Chalko                            Map<Long, ConflictInfo> conflictInfo =
22395961816a768da387f0b5523cf4363ace2044089Nick Chalko                                    mInputConflictInfoMap.get(inputId);
22495961816a768da387f0b5523cf4363ace2044089Nick Chalko                            if (conflictInfo != null) {
22595961816a768da387f0b5523cf4363ace2044089Nick Chalko                                ConflictInfo oldConflictInfo = conflictInfo.get(schedule.getId());
22695961816a768da387f0b5523cf4363ace2044089Nick Chalko                                if (oldConflictInfo != null) {
22795961816a768da387f0b5523cf4363ace2044089Nick Chalko                                    oldConflictInfo.schedule = schedule;
22895961816a768da387f0b5523cf4363ace2044089Nick Chalko                                }
22995961816a768da387f0b5523cf4363ace2044089Nick Chalko                            }
230d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        }
23195961816a768da387f0b5523cf4363ace2044089Nick Chalko                        onSchedulesChanged();
23295961816a768da387f0b5523cf4363ace2044089Nick Chalko                        notifyScheduledRecordingStatusChanged(scheduledRecordings);
233d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    }
23495961816a768da387f0b5523cf4363ace2044089Nick Chalko                };
23565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        mDataManager.addScheduledRecordingListener(scheduledRecordingListener);
23695961816a768da387f0b5523cf4363ace2044089Nick Chalko        ChannelDataManager.Listener channelDataManagerListener =
23795961816a768da387f0b5523cf4363ace2044089Nick Chalko                new ChannelDataManager.Listener() {
23895961816a768da387f0b5523cf4363ace2044089Nick Chalko                    @Override
23995961816a768da387f0b5523cf4363ace2044089Nick Chalko                    public void onLoadFinished() {
24095961816a768da387f0b5523cf4363ace2044089Nick Chalko                        if (mDataManager.isDvrScheduleLoadFinished() && !mInitialized) {
24195961816a768da387f0b5523cf4363ace2044089Nick Chalko                            buildData();
24295961816a768da387f0b5523cf4363ace2044089Nick Chalko                        }
24395961816a768da387f0b5523cf4363ace2044089Nick Chalko                    }
24465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
24595961816a768da387f0b5523cf4363ace2044089Nick Chalko                    @Override
24695961816a768da387f0b5523cf4363ace2044089Nick Chalko                    public void onChannelListUpdated() {
24795961816a768da387f0b5523cf4363ace2044089Nick Chalko                        if (mDataManager.isDvrScheduleLoadFinished()) {
24895961816a768da387f0b5523cf4363ace2044089Nick Chalko                            buildData();
24995961816a768da387f0b5523cf4363ace2044089Nick Chalko                        }
25095961816a768da387f0b5523cf4363ace2044089Nick Chalko                    }
25165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
25295961816a768da387f0b5523cf4363ace2044089Nick Chalko                    @Override
25395961816a768da387f0b5523cf4363ace2044089Nick Chalko                    public void onChannelBrowsableChanged() {}
25495961816a768da387f0b5523cf4363ace2044089Nick Chalko                };
25565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        mChannelDataManager.addListener(channelDataManagerListener);
25665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
25765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
25895961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Returns the started recordings for the given input. */
25965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private List<ScheduledRecording> getStartedRecordings(String inputId) {
26065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) {
26165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return Collections.emptyList();
26265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
26365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<ScheduledRecording> result = new ArrayList<>();
26465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId);
26565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (schedules != null) {
26665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            for (ScheduledRecording schedule : schedules) {
26765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
26865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                    result.add(schedule);
26965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                }
27065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
27165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
27265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        return result;
27365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
27465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
27565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private void buildData() {
27665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        mInputScheduleMap.clear();
27765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) {
27865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            if (!schedule.isNotStarted() && !schedule.isInProgress()) {
27965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                continue;
28065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
28165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            Channel channel = mChannelDataManager.getChannel(schedule.getChannelId());
28265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            if (channel != null) {
28365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                String inputId = channel.getInputId();
28465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                // Do not check whether the input is valid or not. The input might be temporarily
28565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                // invalid.
28665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId);
28765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                if (schedules == null) {
28865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                    schedules = new ArrayList<>();
28965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                    mInputScheduleMap.put(inputId, schedules);
29065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                }
29165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                schedules.add(schedule);
29265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
29365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
294d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        if (!mInitialized) {
295d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            mInitialized = true;
296d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            notifyInitialize();
297d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
29865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        onSchedulesChanged();
29965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
30065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
30165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private void onSchedulesChanged() {
302d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        // TODO: notify conflict state change when some conflicting recording becomes partially
303d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        //       conflicting, vice versa.
30465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<ScheduledRecording> addedConflicts = new ArrayList<>();
30565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<ScheduledRecording> removedConflicts = new ArrayList<>();
30665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        for (String inputId : mInputScheduleMap.keySet()) {
307633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            Map<Long, ConflictInfo> oldConflictInfo = mInputConflictInfoMap.get(inputId);
30865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            Map<Long, ScheduledRecording> oldConflictMap = new HashMap<>();
309633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            if (oldConflictInfo != null) {
310633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                for (ConflictInfo conflictInfo : oldConflictInfo.values()) {
311633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                    oldConflictMap.put(conflictInfo.schedule.getId(), conflictInfo.schedule);
31265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                }
31365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
314633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            List<ConflictInfo> conflicts = getConflictingSchedulesInfo(inputId);
315633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            if (conflicts.isEmpty()) {
316d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                mInputConflictInfoMap.remove(inputId);
317d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            } else {
318633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                Map<Long, ConflictInfo> conflictInfos = new HashMap<>();
319633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                for (ConflictInfo conflictInfo : conflicts) {
320633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                    conflictInfos.put(conflictInfo.schedule.getId(), conflictInfo);
321633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                    if (oldConflictMap.remove(conflictInfo.schedule.getId()) == null) {
322633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                        addedConflicts.add(conflictInfo.schedule);
323d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    }
32465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                }
325633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                mInputConflictInfoMap.put(inputId, conflictInfos);
32665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
32765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            removedConflicts.addAll(oldConflictMap.values());
32865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
32965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (!removedConflicts.isEmpty()) {
33065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            notifyConflictStateChange(false, ScheduledRecording.toArray(removedConflicts));
33165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
33265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (!addedConflicts.isEmpty()) {
33365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            notifyConflictStateChange(true, ScheduledRecording.toArray(addedConflicts));
33465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
33565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
33665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
33795961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Returns {@code true} if this class has been initialized. */
33865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public boolean isInitialized() {
33965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        return mInitialized;
34065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
34165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
34295961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Adds a {@link ScheduledRecordingListener}. */
34365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public final void addScheduledRecordingListener(ScheduledRecordingListener listener) {
34465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        mScheduledRecordingListeners.add(listener);
34565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
34665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
34795961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Removes a {@link ScheduledRecordingListener}. */
34865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public final void removeScheduledRecordingListener(ScheduledRecordingListener listener) {
34965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        mScheduledRecordingListeners.remove(listener);
35065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
35165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
35295961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Calls {@link ScheduledRecordingListener#onScheduledRecordingAdded} for each listener. */
35365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private void notifyScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
35465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
35565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            l.onScheduledRecordingAdded(scheduledRecordings);
35665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
35765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
35865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
35995961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Calls {@link ScheduledRecordingListener#onScheduledRecordingRemoved} for each listener. */
36065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private void notifyScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
36165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
36265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            l.onScheduledRecordingRemoved(scheduledRecordings);
36365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
36465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
36565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
36665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    /**
36765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     * Calls {@link ScheduledRecordingListener#onScheduledRecordingStatusChanged} for each listener.
36865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     */
36965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private void notifyScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
37065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
37165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            l.onScheduledRecordingStatusChanged(scheduledRecordings);
37265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
37365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
37465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
37595961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Adds a {@link OnInitializeListener}. */
376d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    public final void addOnInitializeListener(OnInitializeListener listener) {
377d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        mOnInitializeListeners.add(listener);
378d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
379d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
38095961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Removes a {@link OnInitializeListener}. */
381d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    public final void removeOnInitializeListener(OnInitializeListener listener) {
382d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        mOnInitializeListeners.remove(listener);
383d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
384d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
38595961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Calls {@link OnInitializeListener#onInitialize} for each listener. */
386d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    private void notifyInitialize() {
387d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        for (OnInitializeListener l : mOnInitializeListeners) {
388d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            l.onInitialize();
389d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
390d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
391d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
39295961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Adds a {@link OnConflictStateChangeListener}. */
39365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public final void addOnConflictStateChangeListener(OnConflictStateChangeListener listener) {
39465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        mOnConflictStateChangeListeners.add(listener);
39565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
39665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
39795961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Removes a {@link OnConflictStateChangeListener}. */
39865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public final void removeOnConflictStateChangeListener(OnConflictStateChangeListener listener) {
39965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        mOnConflictStateChangeListeners.remove(listener);
40065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
40165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
40295961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Calls {@link OnConflictStateChangeListener#onConflictStateChange} for each listener. */
40395961816a768da387f0b5523cf4363ace2044089Nick Chalko    private void notifyConflictStateChange(
40495961816a768da387f0b5523cf4363ace2044089Nick Chalko            boolean conflict, ScheduledRecording... scheduledRecordings) {
40565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        for (OnConflictStateChangeListener l : mOnConflictStateChangeListeners) {
40665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            l.onConflictStateChange(conflict, scheduledRecordings);
40765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
40865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
40965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
41065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    /**
41165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     * Returns the priority for the program if it is recorded.
41295961816a768da387f0b5523cf4363ace2044089Nick Chalko     *
41395961816a768da387f0b5523cf4363ace2044089Nick Chalko     * <p>The recording will have the higher priority than the existing ones.
41465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     */
41565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public long suggestNewPriority() {
41665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) {
41765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return DEFAULT_PRIORITY;
41865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
41965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        return suggestHighestPriority();
42065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
42165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
42265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private long suggestHighestPriority() {
42365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        long highestPriority = DEFAULT_PRIORITY - PRIORITY_OFFSET;
42465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) {
42565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            if (schedule.getPriority() > highestPriority) {
42665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                highestPriority = schedule.getPriority();
42765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
42865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
42965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        return highestPriority + PRIORITY_OFFSET;
43065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
43165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
43295961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Suggests the higher priority than the schedules which overlap with {@code schedule}. */
433d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    public long suggestHighestPriority(ScheduledRecording schedule) {
434d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        List<ScheduledRecording> schedules = mInputScheduleMap.get(schedule.getInputId());
435d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        if (schedules == null) {
436d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            return DEFAULT_PRIORITY;
437d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
438d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        long highestPriority = Long.MIN_VALUE;
439d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        for (ScheduledRecording r : schedules) {
44095961816a768da387f0b5523cf4363ace2044089Nick Chalko            if (!r.equals(schedule)
44195961816a768da387f0b5523cf4363ace2044089Nick Chalko                    && r.isOverLapping(schedule)
442d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    && r.getPriority() > highestPriority) {
443d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                highestPriority = r.getPriority();
444d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            }
445d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
446d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        if (highestPriority == Long.MIN_VALUE || highestPriority < schedule.getPriority()) {
447d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            return schedule.getPriority();
448d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
449d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        return highestPriority + PRIORITY_OFFSET;
450d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
451d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
45295961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Suggests the higher priority than the schedules which overlap with {@code schedule}. */
453d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    public long suggestHighestPriority(String inputId, Range<Long> peroid, long basePriority) {
454d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId);
455d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        if (schedules == null) {
456d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            return DEFAULT_PRIORITY;
457d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
458d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        long highestPriority = Long.MIN_VALUE;
459d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        for (ScheduledRecording r : schedules) {
460d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            if (r.isOverLapping(peroid) && r.getPriority() > highestPriority) {
461d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                highestPriority = r.getPriority();
462d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            }
463d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
464d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        if (highestPriority == Long.MIN_VALUE || highestPriority < basePriority) {
465d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            return basePriority;
466d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
467d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        return highestPriority + PRIORITY_OFFSET;
468d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
469d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
470d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    /**
47165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     * Returns the priority for a series recording.
47295961816a768da387f0b5523cf4363ace2044089Nick Chalko     *
47395961816a768da387f0b5523cf4363ace2044089Nick Chalko     * <p>The recording will have the higher priority than the existing series.
47465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     */
47565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public long suggestNewSeriesPriority() {
47665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) {
47765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return DEFAULT_SERIES_PRIORITY;
47865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
47965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        return suggestHighestSeriesPriority();
48065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
48165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
48265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    /**
48365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     * Returns the priority for a series recording by order of series recording priority.
48465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     *
48595961816a768da387f0b5523cf4363ace2044089Nick Chalko     * <p>Higher order will have higher priority.
48665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     */
48765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public static long suggestSeriesPriority(int order) {
48865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        return DEFAULT_SERIES_PRIORITY + order * PRIORITY_OFFSET;
48965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
49065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
49165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    private long suggestHighestSeriesPriority() {
49265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        long highestPriority = DEFAULT_SERIES_PRIORITY - PRIORITY_OFFSET;
49365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        for (SeriesRecording schedule : mDataManager.getSeriesRecordings()) {
49465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            if (schedule.getPriority() > highestPriority) {
49565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                highestPriority = schedule.getPriority();
49665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
49765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
49865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        return highestPriority + PRIORITY_OFFSET;
49965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
50065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
50165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    /**
50295961816a768da387f0b5523cf4363ace2044089Nick Chalko     * Returns a sorted list of all scheduled recordings that will not be recorded if this program
50395961816a768da387f0b5523cf4363ace2044089Nick Chalko     * is going to be recorded, with their priorities in decending order.
50495961816a768da387f0b5523cf4363ace2044089Nick Chalko     *
50595961816a768da387f0b5523cf4363ace2044089Nick Chalko     * <p>An empty list means there is no conflicts. If there is conflict, a priority higher than
506d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     * the first recording in the returned list should be assigned to the new schedule of this
507d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     * program to guarantee the program would be completely recorded.
50865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     */
50965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public List<ScheduledRecording> getConflictingSchedules(Program program) {
51065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
51165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkState(
5120cc0713c1bf8027642987b750b80217569d2932aLive Channels Team                Program.isProgramValid(program), TAG, "Program is invalid: " + program);
51395961816a768da387f0b5523cf4363ace2044089Nick Chalko        SoftPreconditions.checkState(
51495961816a768da387f0b5523cf4363ace2044089Nick Chalko                program.getStartTimeUtcMillis() < program.getEndTimeUtcMillis(),
51595961816a768da387f0b5523cf4363ace2044089Nick Chalko                TAG,
51665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                "Program duration is empty: " + program);
51795961816a768da387f0b5523cf4363ace2044089Nick Chalko        if (!mInitialized
5180cc0713c1bf8027642987b750b80217569d2932aLive Channels Team                || !Program.isProgramValid(program)
51965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                || program.getStartTimeUtcMillis() >= program.getEndTimeUtcMillis()) {
52065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return Collections.emptyList();
52165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
52265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        TvInputInfo input = Utils.getTvInputInfoForProgram(mContext, program);
52365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (input == null || !input.canRecord() || input.getTunerCount() <= 0) {
52465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return Collections.emptyList();
52565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
52695961816a768da387f0b5523cf4363ace2044089Nick Chalko        return getConflictingSchedules(
52795961816a768da387f0b5523cf4363ace2044089Nick Chalko                input,
52895961816a768da387f0b5523cf4363ace2044089Nick Chalko                Collections.singletonList(
52995961816a768da387f0b5523cf4363ace2044089Nick Chalko                        ScheduledRecording.builder(input.getId(), program)
53095961816a768da387f0b5523cf4363ace2044089Nick Chalko                                .setPriority(suggestHighestPriority())
53195961816a768da387f0b5523cf4363ace2044089Nick Chalko                                .build()));
53265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
53365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
53465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    /**
535633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko     * Returns list of all conflicting scheduled recordings for the given {@code seriesRecording}
536d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     * recording.
53795961816a768da387f0b5523cf4363ace2044089Nick Chalko     *
53895961816a768da387f0b5523cf4363ace2044089Nick Chalko     * <p>Any empty list means there is no conflicts.
539d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     */
540d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    public List<ScheduledRecording> getConflictingSchedules(SeriesRecording seriesRecording) {
541d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
542d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        SoftPreconditions.checkState(seriesRecording != null, TAG, "series recording is null");
543d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        if (!mInitialized || seriesRecording == null) {
544d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            return Collections.emptyList();
545d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
546d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, seriesRecording.getInputId());
547d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        if (input == null || !input.canRecord() || input.getTunerCount() <= 0) {
548d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            return Collections.emptyList();
549d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
55095961816a768da387f0b5523cf4363ace2044089Nick Chalko        List<ScheduledRecording> scheduledRecordingForSeries =
55195961816a768da387f0b5523cf4363ace2044089Nick Chalko                mDataManager.getScheduledRecordings(seriesRecording.getId());
552633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        List<ScheduledRecording> availableScheduledRecordingForSeries = new ArrayList<>();
553633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        for (ScheduledRecording scheduledRecording : scheduledRecordingForSeries) {
554633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            if (scheduledRecording.isNotStarted() || scheduledRecording.isInProgress()) {
555633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                availableScheduledRecordingForSeries.add(scheduledRecording);
556633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            }
557633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        }
558633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        if (availableScheduledRecordingForSeries.isEmpty()) {
559633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            return Collections.emptyList();
560633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        }
561633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        return getConflictingSchedules(input, availableScheduledRecordingForSeries);
562d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
563d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
564d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    /**
56595961816a768da387f0b5523cf4363ace2044089Nick Chalko     * Returns a sorted list of all scheduled recordings that will not be recorded if this channel
56695961816a768da387f0b5523cf4363ace2044089Nick Chalko     * is going to be recorded, with their priority in decending order.
56795961816a768da387f0b5523cf4363ace2044089Nick Chalko     *
56895961816a768da387f0b5523cf4363ace2044089Nick Chalko     * <p>An empty list means there is no conflicts. If there is conflict, a priority higher than
569d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     * the first recording in the returned list should be assigned to the new schedule of this
570d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     * channel to guarantee the channel would be completely recorded in the designated time range.
57165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     */
57295961816a768da387f0b5523cf4363ace2044089Nick Chalko    public List<ScheduledRecording> getConflictingSchedules(
57395961816a768da387f0b5523cf4363ace2044089Nick Chalko            long channelId, long startTimeMs, long endTimeMs) {
57465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
57565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkState(channelId != Channel.INVALID_ID, TAG, "Invalid channel ID");
57665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkState(startTimeMs < endTimeMs, TAG, "Recording duration is empty.");
57765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (!mInitialized || channelId == Channel.INVALID_ID || startTimeMs >= endTimeMs) {
57865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return Collections.emptyList();
57965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
58065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, channelId);
58165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (input == null || !input.canRecord() || input.getTunerCount() <= 0) {
58265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return Collections.emptyList();
58365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
58495961816a768da387f0b5523cf4363ace2044089Nick Chalko        return getConflictingSchedules(
58595961816a768da387f0b5523cf4363ace2044089Nick Chalko                input,
58695961816a768da387f0b5523cf4363ace2044089Nick Chalko                Collections.singletonList(
58795961816a768da387f0b5523cf4363ace2044089Nick Chalko                        ScheduledRecording.builder(input.getId(), channelId, startTimeMs, endTimeMs)
58895961816a768da387f0b5523cf4363ace2044089Nick Chalko                                .setPriority(suggestHighestPriority())
58995961816a768da387f0b5523cf4363ace2044089Nick Chalko                                .build()));
59065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
59165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
59265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    /**
59365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     * Returns all the scheduled recordings that conflicts and will not be recorded or clipped for
59465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     * the given input.
59565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     */
59665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    @NonNull
597633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko    private List<ConflictInfo> getConflictingSchedulesInfo(String inputId) {
59865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
59965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId);
60065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkState(input != null, TAG, "Can't find input for : " + inputId);
60165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (!mInitialized || input == null) {
602633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            return Collections.emptyList();
60365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
60465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId());
60565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (schedules == null || schedules.isEmpty()) {
606633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            return Collections.emptyList();
60765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
608d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        return getConflictingSchedulesInfo(schedules, input.getTunerCount());
60965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
61065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
61165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    /**
61265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     * Checks if the schedule is conflicting.
61365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     *
61495961816a768da387f0b5523cf4363ace2044089Nick Chalko     * <p>Note that the {@code schedule} should be the existing one. If not, this returns {@code
61595961816a768da387f0b5523cf4363ace2044089Nick Chalko     * false}.
61665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     */
61765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public boolean isConflicting(ScheduledRecording schedule) {
61865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
619d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
62095961816a768da387f0b5523cf4363ace2044089Nick Chalko        SoftPreconditions.checkState(
62195961816a768da387f0b5523cf4363ace2044089Nick Chalko                input != null, TAG, "Can't find input for channel ID : " + schedule.getChannelId());
622d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        if (!mInitialized || input == null) {
623d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            return false;
624d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
625633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        Map<Long, ConflictInfo> conflicts = mInputConflictInfoMap.get(input.getId());
626633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        return conflicts != null && conflicts.containsKey(schedule.getId());
627d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
628d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
629d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    /**
630d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     * Checks if the schedule is partially conflicting, i.e., part of the scheduled program might be
631d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     * recorded even if the priority of the schedule is not raised.
63295961816a768da387f0b5523cf4363ace2044089Nick Chalko     *
63395961816a768da387f0b5523cf4363ace2044089Nick Chalko     * <p>If the given schedule is not conflicting or is totally conflicting, i.e., cannot be
63495961816a768da387f0b5523cf4363ace2044089Nick Chalko     * recorded at all, this method returns {@code false} in both cases.
635d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     */
636d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    public boolean isPartiallyConflicting(@NonNull ScheduledRecording schedule) {
637d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
638d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
63995961816a768da387f0b5523cf4363ace2044089Nick Chalko        SoftPreconditions.checkState(
64095961816a768da387f0b5523cf4363ace2044089Nick Chalko                input != null, TAG, "Can't find input for channel ID : " + schedule.getChannelId());
64165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (!mInitialized || input == null) {
64265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return false;
64365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
644633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        Map<Long, ConflictInfo> conflicts = mInputConflictInfoMap.get(input.getId());
645633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        if (conflicts != null) {
646633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            ConflictInfo conflictInfo = conflicts.get(schedule.getId());
647633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            return conflictInfo != null && conflictInfo.partialConflict;
648633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        }
649633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        return false;
65065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
65165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
65265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    /**
65395961816a768da387f0b5523cf4363ace2044089Nick Chalko     * Returns priority ordered list of all scheduled recordings that will not be recorded if this
65495961816a768da387f0b5523cf4363ace2044089Nick Chalko     * channel is tuned to.
65565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     */
65665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public List<ScheduledRecording> getConflictingSchedulesForTune(long channelId) {
65765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
65865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkState(channelId != Channel.INVALID_ID, TAG, "Invalid channel ID");
65965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, channelId);
66095961816a768da387f0b5523cf4363ace2044089Nick Chalko        SoftPreconditions.checkState(
66195961816a768da387f0b5523cf4363ace2044089Nick Chalko                input != null, TAG, "Can't find input for channel ID: " + channelId);
66265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (!mInitialized || channelId == Channel.INVALID_ID || input == null) {
66365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return Collections.emptyList();
66465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
66595961816a768da387f0b5523cf4363ace2044089Nick Chalko        return getConflictingSchedulesForTune(
66695961816a768da387f0b5523cf4363ace2044089Nick Chalko                input.getId(),
66795961816a768da387f0b5523cf4363ace2044089Nick Chalko                channelId,
66895961816a768da387f0b5523cf4363ace2044089Nick Chalko                System.currentTimeMillis(),
66995961816a768da387f0b5523cf4363ace2044089Nick Chalko                suggestHighestPriority(),
67095961816a768da387f0b5523cf4363ace2044089Nick Chalko                getStartedRecordings(input.getId()),
67165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                input.getTunerCount());
67265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
67365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
67465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    @VisibleForTesting
67595961816a768da387f0b5523cf4363ace2044089Nick Chalko    public static List<ScheduledRecording> getConflictingSchedulesForTune(
67695961816a768da387f0b5523cf4363ace2044089Nick Chalko            String inputId,
67795961816a768da387f0b5523cf4363ace2044089Nick Chalko            long channelId,
67895961816a768da387f0b5523cf4363ace2044089Nick Chalko            long currentTimeMs,
67995961816a768da387f0b5523cf4363ace2044089Nick Chalko            long newPriority,
68095961816a768da387f0b5523cf4363ace2044089Nick Chalko            List<ScheduledRecording> startedRecordings,
68195961816a768da387f0b5523cf4363ace2044089Nick Chalko            int tunerCount) {
68265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        boolean channelFound = false;
68365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        for (ScheduledRecording schedule : startedRecordings) {
68465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            if (schedule.getChannelId() == channelId) {
68565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                channelFound = true;
68665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                break;
68765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
68865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
68965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<ScheduledRecording> schedules;
69065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (!channelFound) {
69165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            // The current channel is not being recorded.
69265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            schedules = new ArrayList<>(startedRecordings);
69395961816a768da387f0b5523cf4363ace2044089Nick Chalko            schedules.add(
69495961816a768da387f0b5523cf4363ace2044089Nick Chalko                    ScheduledRecording.builder(inputId, channelId, currentTimeMs, currentTimeMs + 1)
69595961816a768da387f0b5523cf4363ace2044089Nick Chalko                            .setPriority(newPriority)
69695961816a768da387f0b5523cf4363ace2044089Nick Chalko                            .build());
69765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        } else {
69865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            schedules = startedRecordings;
69965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
70065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        return getConflictingSchedules(schedules, tunerCount);
70165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
70265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
70365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    /**
70495961816a768da387f0b5523cf4363ace2044089Nick Chalko     * Returns priority ordered list of all scheduled recordings that will not be recorded if the
70595961816a768da387f0b5523cf4363ace2044089Nick Chalko     * user keeps watching this channel.
70695961816a768da387f0b5523cf4363ace2044089Nick Chalko     *
70795961816a768da387f0b5523cf4363ace2044089Nick Chalko     * <p>Note that if the user keeps watching the channel, the channel can be recorded.
70865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko     */
70965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public List<ScheduledRecording> getConflictingSchedulesForWatching(long channelId) {
71065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
71165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkState(channelId != Channel.INVALID_ID, TAG, "Invalid channel ID");
71265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, channelId);
71395961816a768da387f0b5523cf4363ace2044089Nick Chalko        SoftPreconditions.checkState(
71495961816a768da387f0b5523cf4363ace2044089Nick Chalko                input != null, TAG, "Can't find input for channel ID: " + channelId);
71565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (!mInitialized || channelId == Channel.INVALID_ID || input == null) {
71665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return Collections.emptyList();
71765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
71865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId());
71965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (schedules == null || schedules.isEmpty()) {
72065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return Collections.emptyList();
72165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
72295961816a768da387f0b5523cf4363ace2044089Nick Chalko        return getConflictingSchedulesForWatching(
72395961816a768da387f0b5523cf4363ace2044089Nick Chalko                input.getId(),
72495961816a768da387f0b5523cf4363ace2044089Nick Chalko                channelId,
72595961816a768da387f0b5523cf4363ace2044089Nick Chalko                System.currentTimeMillis(),
72695961816a768da387f0b5523cf4363ace2044089Nick Chalko                suggestNewPriority(),
72795961816a768da387f0b5523cf4363ace2044089Nick Chalko                schedules,
72895961816a768da387f0b5523cf4363ace2044089Nick Chalko                input.getTunerCount());
72965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
73065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
73195961816a768da387f0b5523cf4363ace2044089Nick Chalko    private List<ScheduledRecording> getConflictingSchedules(
73295961816a768da387f0b5523cf4363ace2044089Nick Chalko            TvInputInfo input, List<ScheduledRecording> schedulesToAdd) {
73365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        SoftPreconditions.checkNotNull(input);
73465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (input == null || !input.canRecord() || input.getTunerCount() <= 0) {
73565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return Collections.emptyList();
73665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
73765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<ScheduledRecording> currentSchedules = mInputScheduleMap.get(input.getId());
73865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        if (currentSchedules == null || currentSchedules.isEmpty()) {
73965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            return Collections.emptyList();
74065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
74165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        return getConflictingSchedules(schedulesToAdd, currentSchedules, input.getTunerCount());
74265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
74365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
74465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    @VisibleForTesting
74595961816a768da387f0b5523cf4363ace2044089Nick Chalko    static List<ScheduledRecording> getConflictingSchedulesForWatching(
74695961816a768da387f0b5523cf4363ace2044089Nick Chalko            String inputId,
74795961816a768da387f0b5523cf4363ace2044089Nick Chalko            long channelId,
74895961816a768da387f0b5523cf4363ace2044089Nick Chalko            long currentTimeMs,
74995961816a768da387f0b5523cf4363ace2044089Nick Chalko            long newPriority,
75095961816a768da387f0b5523cf4363ace2044089Nick Chalko            @NonNull List<ScheduledRecording> schedules,
75195961816a768da387f0b5523cf4363ace2044089Nick Chalko            int tunerCount) {
75265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules);
75365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<ScheduledRecording> schedulesSameChannel = new ArrayList<>();
75465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        for (ScheduledRecording schedule : schedules) {
75565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            if (schedule.getChannelId() == channelId) {
75665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                schedulesSameChannel.add(schedule);
75765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                schedulesToCheck.remove(schedule);
75865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
75965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
76065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        // Assume that the user will watch the current channel forever.
76195961816a768da387f0b5523cf4363ace2044089Nick Chalko        schedulesToCheck.add(
76295961816a768da387f0b5523cf4363ace2044089Nick Chalko                ScheduledRecording.builder(inputId, channelId, currentTimeMs, Long.MAX_VALUE)
76395961816a768da387f0b5523cf4363ace2044089Nick Chalko                        .setPriority(newPriority)
76495961816a768da387f0b5523cf4363ace2044089Nick Chalko                        .build());
76565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<ScheduledRecording> result = new ArrayList<>();
76665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        result.addAll(getConflictingSchedules(schedulesSameChannel, 1));
76765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        result.addAll(getConflictingSchedules(schedulesToCheck, tunerCount));
768d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        Collections.sort(result, RESULT_COMPARATOR);
76965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        return result;
77065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
77165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
77265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    @VisibleForTesting
77395961816a768da387f0b5523cf4363ace2044089Nick Chalko    static List<ScheduledRecording> getConflictingSchedules(
77495961816a768da387f0b5523cf4363ace2044089Nick Chalko            List<ScheduledRecording> schedulesToAdd,
77595961816a768da387f0b5523cf4363ace2044089Nick Chalko            List<ScheduledRecording> currentSchedules,
77695961816a768da387f0b5523cf4363ace2044089Nick Chalko            int tunerCount) {
77765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<ScheduledRecording> schedulesToCheck = new ArrayList<>(currentSchedules);
77865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        // When the duplicate schedule is to be added, remove the current duplicate recording.
77965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        for (Iterator<ScheduledRecording> iter = schedulesToCheck.iterator(); iter.hasNext(); ) {
78065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            ScheduledRecording schedule = iter.next();
78165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            for (ScheduledRecording toAdd : schedulesToAdd) {
78265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                if (schedule.getType() == ScheduledRecording.TYPE_PROGRAM) {
78365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                    if (toAdd.getProgramId() == schedule.getProgramId()) {
78465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                        iter.remove();
78565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                        break;
78665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                    }
78765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                } else {
78865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                    if (toAdd.getChannelId() == schedule.getChannelId()
78965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                            && toAdd.getStartTimeMs() == schedule.getStartTimeMs()
79065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                            && toAdd.getEndTimeMs() == schedule.getEndTimeMs()) {
79165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                        iter.remove();
79265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                        break;
79365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                    }
79465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                }
79565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
79665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
79765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        schedulesToCheck.addAll(schedulesToAdd);
79865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        List<Range<Long>> ranges = new ArrayList<>();
79965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        for (ScheduledRecording schedule : schedulesToAdd) {
80065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            ranges.add(new Range<>(schedule.getStartTimeMs(), schedule.getEndTimeMs()));
80165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
80265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        return getConflictingSchedules(schedulesToCheck, tunerCount, ranges);
80365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
80465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
80595961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** Returns all conflicting scheduled recordings for the given schedules and count of tuner. */
80665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public static List<ScheduledRecording> getConflictingSchedules(
80765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            List<ScheduledRecording> schedules, int tunerCount) {
808d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        return getConflictingSchedules(schedules, tunerCount, null);
80965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
81065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
81165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    @VisibleForTesting
812d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    static List<ScheduledRecording> getConflictingSchedules(
813d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) {
814633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        List<ScheduledRecording> result = new ArrayList<>();
815633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        for (ConflictInfo conflictInfo :
816633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                getConflictingSchedulesInfo(schedules, tunerCount, periods)) {
817633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            result.add(conflictInfo.schedule);
818633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        }
819d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        return result;
820d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
821d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
822d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    @VisibleForTesting
82395961816a768da387f0b5523cf4363ace2044089Nick Chalko    static List<ConflictInfo> getConflictingSchedulesInfo(
82495961816a768da387f0b5523cf4363ace2044089Nick Chalko            List<ScheduledRecording> schedules, int tunerCount) {
825d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        return getConflictingSchedulesInfo(schedules, tunerCount, null);
826d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
827d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
828d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    /**
829d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     * This is the core method to calculate all the conflicting schedules (in given periods).
83095961816a768da387f0b5523cf4363ace2044089Nick Chalko     *
83195961816a768da387f0b5523cf4363ace2044089Nick Chalko     * <p>Note that this method will ignore duplicated schedules with a same hash code. (Please
83295961816a768da387f0b5523cf4363ace2044089Nick Chalko     * refer to {@link ScheduledRecording#hashCode}.)
833d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     *
834d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     * @return A {@link HashMap} from {@link ScheduledRecording} to {@link Boolean}. The boolean
83595961816a768da387f0b5523cf4363ace2044089Nick Chalko     *     value denotes if the scheduled recording is partially conflicting, i.e., is possible to
83695961816a768da387f0b5523cf4363ace2044089Nick Chalko     *     be partially recorded under the given schedules and tuner count {@code true}, or not
83795961816a768da387f0b5523cf4363ace2044089Nick Chalko     *     {@code false}.
838d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko     */
839633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko    private static List<ConflictInfo> getConflictingSchedulesInfo(
840d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) {
841d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules);
842d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        // Sort by the same order as that in InputTaskScheduler.
843d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        Collections.sort(schedulesToCheck, InputTaskScheduler.getRecordingOrderComparator());
844d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        List<ScheduledRecording> recordings = new ArrayList<>();
845633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        Map<ScheduledRecording, ConflictInfo> conflicts = new HashMap<>();
846d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        Map<ScheduledRecording, ScheduledRecording> modified2OriginalSchedules = new HashMap<>();
847d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        // Simulate InputTaskScheduler.
848d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        while (!schedulesToCheck.isEmpty()) {
849d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            ScheduledRecording schedule = schedulesToCheck.remove(0);
850d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            removeFinishedRecordings(recordings, schedule.getStartTimeMs());
851d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            if (recordings.size() < tunerCount) {
852d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                recordings.add(schedule);
853d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                if (modified2OriginalSchedules.containsKey(schedule)) {
854d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    // Schedule has been modified, which means it's already conflicted.
855d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    // Modify its state to partially conflicted.
856633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                    ScheduledRecording originalSchedule = modified2OriginalSchedules.get(schedule);
857633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                    conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true));
85865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                }
859d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            } else {
860d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                ScheduledRecording candidate = findReplaceableRecording(recordings, schedule);
861d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                if (candidate != null) {
862d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    if (!modified2OriginalSchedules.containsKey(candidate)) {
863633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                        conflicts.put(candidate, new ConflictInfo(candidate, true));
864d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    }
865d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    recordings.remove(candidate);
866d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    recordings.add(schedule);
867d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    if (modified2OriginalSchedules.containsKey(schedule)) {
868d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        // Schedule has been modified, which means it's already conflicted.
869d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        // Modify its state to partially conflicted.
870633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                        ScheduledRecording originalSchedule =
871633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                                modified2OriginalSchedules.get(schedule);
872633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                        conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true));
873d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    }
874d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                } else {
875d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    if (!modified2OriginalSchedules.containsKey(schedule)) {
876d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        // if schedule has been modified, it's already conflicted.
877d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        // No need to add it again.
878633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko                        conflicts.put(schedule, new ConflictInfo(schedule, false));
879d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    }
880d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    long earliestEndTime = getEarliestEndTime(recordings);
881d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    if (earliestEndTime < schedule.getEndTimeMs()) {
882d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        // The schedule can starts when other recording ends even though it's
883d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        // clipped.
88495961816a768da387f0b5523cf4363ace2044089Nick Chalko                        ScheduledRecording modifiedSchedule =
88595961816a768da387f0b5523cf4363ace2044089Nick Chalko                                ScheduledRecording.buildFrom(schedule)
88695961816a768da387f0b5523cf4363ace2044089Nick Chalko                                        .setStartTimeMs(earliestEndTime)
88795961816a768da387f0b5523cf4363ace2044089Nick Chalko                                        .build();
888d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        ScheduledRecording originalSchedule =
889d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                                modified2OriginalSchedules.getOrDefault(schedule, schedule);
890d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        modified2OriginalSchedules.put(modifiedSchedule, originalSchedule);
89195961816a768da387f0b5523cf4363ace2044089Nick Chalko                        int insertPosition =
89295961816a768da387f0b5523cf4363ace2044089Nick Chalko                                Collections.binarySearch(
89395961816a768da387f0b5523cf4363ace2044089Nick Chalko                                        schedulesToCheck,
89495961816a768da387f0b5523cf4363ace2044089Nick Chalko                                        modifiedSchedule,
89595961816a768da387f0b5523cf4363ace2044089Nick Chalko                                        ScheduledRecording
89695961816a768da387f0b5523cf4363ace2044089Nick Chalko                                                .START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR);
897d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        if (insertPosition >= 0) {
898d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                            schedulesToCheck.add(insertPosition, modifiedSchedule);
899d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        } else {
900d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                            schedulesToCheck.add(-insertPosition - 1, modifiedSchedule);
901d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        }
902d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    }
90365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                }
90465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
905d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
906d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        // Returns only the schedules with the given range.
907d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        if (periods != null && !periods.isEmpty()) {
908d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            for (Iterator<ScheduledRecording> iter = conflicts.keySet().iterator();
909d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    iter.hasNext(); ) {
910d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                boolean overlapping = false;
91165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                ScheduledRecording schedule = iter.next();
912d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                for (Range<Long> period : periods) {
913d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    if (schedule.isOverLapping(period)) {
914d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        overlapping = true;
915d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                        break;
916d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    }
917d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                }
918d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                if (!overlapping) {
91965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                    iter.remove();
92065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko                }
92165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
922d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
923633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        List<ConflictInfo> result = new ArrayList<>(conflicts.values());
92495961816a768da387f0b5523cf4363ace2044089Nick Chalko        Collections.sort(
92595961816a768da387f0b5523cf4363ace2044089Nick Chalko                result,
92695961816a768da387f0b5523cf4363ace2044089Nick Chalko                new Comparator<ConflictInfo>() {
92795961816a768da387f0b5523cf4363ace2044089Nick Chalko                    @Override
92895961816a768da387f0b5523cf4363ace2044089Nick Chalko                    public int compare(ConflictInfo lhs, ConflictInfo rhs) {
92995961816a768da387f0b5523cf4363ace2044089Nick Chalko                        return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule);
93095961816a768da387f0b5523cf4363ace2044089Nick Chalko                    }
93195961816a768da387f0b5523cf4363ace2044089Nick Chalko                });
932633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        return result;
933d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
934d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
93595961816a768da387f0b5523cf4363ace2044089Nick Chalko    private static void removeFinishedRecordings(
93695961816a768da387f0b5523cf4363ace2044089Nick Chalko            List<ScheduledRecording> recordings, long currentTimeMs) {
937d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        for (Iterator<ScheduledRecording> iter = recordings.iterator(); iter.hasNext(); ) {
938d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            if (iter.next().getEndTimeMs() <= currentTimeMs) {
939d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                iter.remove();
94065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko            }
94165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        }
942d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
943d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
94495961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** @see InputTaskScheduler#getReplacableTask */
94595961816a768da387f0b5523cf4363ace2044089Nick Chalko    private static ScheduledRecording findReplaceableRecording(
94695961816a768da387f0b5523cf4363ace2044089Nick Chalko            List<ScheduledRecording> recordings, ScheduledRecording schedule) {
947d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        // Returns the recording with the following priority.
948d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        // 1. The recording with the lowest priority is returned.
949d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        // 2. If the priorities are the same, the recording which finishes early is returned.
950d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        // 3. If 1) and 2) are the same, the early created schedule is returned.
951d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        ScheduledRecording candidate = null;
952d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        for (ScheduledRecording recording : recordings) {
953d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            if (schedule.getPriority() > recording.getPriority()) {
954d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, recording) > 0) {
955d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                    candidate = recording;
956d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                }
957d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            }
958d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
959d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        return candidate;
960d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
961d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
962d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    private static long getEarliestEndTime(List<ScheduledRecording> recordings) {
963d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        long earliest = Long.MAX_VALUE;
964d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        for (ScheduledRecording recording : recordings) {
965d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            if (earliest > recording.getEndTimeMs()) {
966d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko                earliest = recording.getEndTimeMs();
967d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko            }
968d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        }
969d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        return earliest;
970d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    }
971d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko
972633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko    @VisibleForTesting
973633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko    static class ConflictInfo {
974633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        public ScheduledRecording schedule;
975633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        public boolean partialConflict;
976633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko
977633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        ConflictInfo(ScheduledRecording schedule, boolean partialConflict) {
978633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            this.schedule = schedule;
979633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko            this.partialConflict = partialConflict;
980633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko        }
981633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko    }
982633eb826b8c97731dfc5ef12c7bf78a63734275dNick Chalko
98395961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** A listener which is notified the initialization of schedule manager. */
984d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko    public interface OnInitializeListener {
98595961816a768da387f0b5523cf4363ace2044089Nick Chalko        /** Called when the schedule manager has been initialized. */
986d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko        void onInitialize();
98765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
98865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko
98995961816a768da387f0b5523cf4363ace2044089Nick Chalko    /** A listener which is notified the conflict state change of the schedules. */
99065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    public interface OnConflictStateChangeListener {
99165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        /**
99265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko         * Called when the conflicting schedules change.
99395961816a768da387f0b5523cf4363ace2044089Nick Chalko         *
99495961816a768da387f0b5523cf4363ace2044089Nick Chalko         * <p>Note that this can be called before {@link
99595961816a768da387f0b5523cf4363ace2044089Nick Chalko         * ScheduledRecordingListener#onScheduledRecordingAdded} is called.
99665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko         *
99765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko         * @param conflict {@code true} if the {@code schedules} are the new conflicts, otherwise
99895961816a768da387f0b5523cf4363ace2044089Nick Chalko         *     {@code false}.
99965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko         * @param schedules the schedules
100065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko         */
100165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko        void onConflictStateChange(boolean conflict, ScheduledRecording... schedules);
100265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko    }
100395961816a768da387f0b5523cf4363ace2044089Nick Chalko}
1004