1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.dvr.recorder;
18 
19 import android.annotation.TargetApi;
20 import android.content.ContentUris;
21 import android.media.tv.TvContract;
22 import android.net.Uri;
23 import android.os.Build;
24 import android.os.Message;
25 import android.support.annotation.MainThread;
26 import android.support.annotation.NonNull;
27 import android.support.annotation.Nullable;
28 import android.util.ArraySet;
29 import android.util.Log;
30 import com.android.tv.InputSessionManager;
31 import com.android.tv.InputSessionManager.OnTvViewChannelChangeListener;
32 import com.android.tv.MainActivity;
33 import com.android.tv.TvSingletons;
34 import com.android.tv.common.WeakHandler;
35 import com.android.tv.data.ChannelDataManager;
36 import com.android.tv.data.api.Channel;
37 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
38 import com.android.tv.dvr.DvrScheduleManager;
39 import com.android.tv.dvr.data.ScheduledRecording;
40 import com.android.tv.dvr.ui.DvrUiHelper;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.HashMap;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Set;
47 import java.util.concurrent.TimeUnit;
48 
49 /**
50  * Checking the runtime conflict of DVR recording.
51  *
52  * <p>This class runs only while the {@link MainActivity} is resumed and holds the upcoming
53  * conflicts.
54  */
55 @TargetApi(Build.VERSION_CODES.N)
56 @MainThread
57 public class ConflictChecker {
58     private static final String TAG = "ConflictChecker";
59     private static final boolean DEBUG = false;
60 
61     private static final int MSG_CHECK_CONFLICT = 1;
62 
63     private static final long CHECK_RETRY_PERIOD_MS = TimeUnit.SECONDS.toMillis(30);
64 
65     /**
66      * To show watch conflict dialog, the start time of the earliest conflicting schedule should be
67      * less than or equal to this time.
68      */
69     private static final long MAX_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.MINUTES.toMillis(5);
70     /**
71      * To show watch conflict dialog, the start time of the earliest conflicting schedule should be
72      * greater than or equal to this time.
73      */
74     private static final long MIN_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.SECONDS.toMillis(30);
75 
76     private final MainActivity mMainActivity;
77     private final ChannelDataManager mChannelDataManager;
78     private final DvrScheduleManager mScheduleManager;
79     private final InputSessionManager mSessionManager;
80     private final ConflictCheckerHandler mHandler = new ConflictCheckerHandler(this);
81 
82     private final List<ScheduledRecording> mUpcomingConflicts = new ArrayList<>();
83     private final Set<OnUpcomingConflictChangeListener> mOnUpcomingConflictChangeListeners =
84             new ArraySet<>();
85     private final Map<Long, List<ScheduledRecording>> mCheckedConflictsMap = new HashMap<>();
86 
87     private final ScheduledRecordingListener mScheduledRecordingListener =
88             new ScheduledRecordingListener() {
89                 @Override
90                 public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
91                     if (DEBUG) {
92                         Log.d(
93                                 TAG,
94                                 "onScheduledRecordingAdded: "
95                                         + Arrays.toString(scheduledRecordings));
96                     }
97                     mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
98                 }
99 
100                 @Override
101                 public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
102                     if (DEBUG) {
103                         Log.d(
104                                 TAG,
105                                 "onScheduledRecordingRemoved: "
106                                         + Arrays.toString(scheduledRecordings));
107                     }
108                     mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
109                 }
110 
111                 @Override
112                 public void onScheduledRecordingStatusChanged(
113                         ScheduledRecording... scheduledRecordings) {
114                     if (DEBUG) {
115                         Log.d(
116                                 TAG,
117                                 "onScheduledRecordingStatusChanged: "
118                                         + Arrays.toString(scheduledRecordings));
119                     }
120                     mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
121                 }
122             };
123 
124     private final OnTvViewChannelChangeListener mOnTvViewChannelChangeListener =
125             new OnTvViewChannelChangeListener() {
126                 @Override
127                 public void onTvViewChannelChange(@Nullable Uri channelUri) {
128                     mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
129                 }
130             };
131 
132     private boolean mStarted;
133 
ConflictChecker(MainActivity mainActivity)134     public ConflictChecker(MainActivity mainActivity) {
135         mMainActivity = mainActivity;
136         TvSingletons tvSingletons = TvSingletons.getSingletons(mainActivity);
137         mChannelDataManager = tvSingletons.getChannelDataManager();
138         mScheduleManager = tvSingletons.getDvrScheduleManager();
139         mSessionManager = tvSingletons.getInputSessionManager();
140     }
141 
142     /** Starts checking the conflict. */
start()143     public void start() {
144         if (mStarted) {
145             return;
146         }
147         mStarted = true;
148         mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
149         mScheduleManager.addScheduledRecordingListener(mScheduledRecordingListener);
150         mSessionManager.addOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener);
151     }
152 
153     /** Stops checking the conflict. */
stop()154     public void stop() {
155         if (!mStarted) {
156             return;
157         }
158         mStarted = false;
159         mSessionManager.removeOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener);
160         mScheduleManager.removeScheduledRecordingListener(mScheduledRecordingListener);
161         mHandler.removeCallbacksAndMessages(null);
162     }
163 
164     /** Returns the upcoming conflicts. */
getUpcomingConflicts()165     public List<ScheduledRecording> getUpcomingConflicts() {
166         return new ArrayList<>(mUpcomingConflicts);
167     }
168 
169     /** Adds a {@link OnUpcomingConflictChangeListener}. */
addOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener)170     public void addOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) {
171         mOnUpcomingConflictChangeListeners.add(listener);
172     }
173 
174     /** Removes the {@link OnUpcomingConflictChangeListener}. */
removeOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener)175     public void removeOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) {
176         mOnUpcomingConflictChangeListeners.remove(listener);
177     }
178 
notifyUpcomingConflictChanged()179     private void notifyUpcomingConflictChanged() {
180         for (OnUpcomingConflictChangeListener l : mOnUpcomingConflictChangeListeners) {
181             l.onUpcomingConflictChange();
182         }
183     }
184 
185     /** Remembers the user's decision to record while watching the channel. */
setCheckedConflictsForChannel(long mChannelId, List<ScheduledRecording> conflicts)186     public void setCheckedConflictsForChannel(long mChannelId, List<ScheduledRecording> conflicts) {
187         mCheckedConflictsMap.put(mChannelId, new ArrayList<>(conflicts));
188     }
189 
onCheckConflict()190     void onCheckConflict() {
191         // Checks the conflicting schedules and setup the next re-check time.
192         // If there are upcoming conflicts soon, it opens the conflict dialog.
193         if (DEBUG) Log.d(TAG, "Handling MSG_CHECK_CONFLICT");
194         mHandler.removeMessages(MSG_CHECK_CONFLICT);
195         mUpcomingConflicts.clear();
196         if (!mScheduleManager.isInitialized() || !mChannelDataManager.isDbLoadFinished()) {
197             mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, CHECK_RETRY_PERIOD_MS);
198             notifyUpcomingConflictChanged();
199             return;
200         }
201         if (mSessionManager.getCurrentTvViewChannelUri() == null) {
202             // As MainActivity is not using a tuner, no need to check the conflict.
203             notifyUpcomingConflictChanged();
204             return;
205         }
206         Uri channelUri = mSessionManager.getCurrentTvViewChannelUri();
207         if (TvContract.isChannelUriForPassthroughInput(channelUri)) {
208             notifyUpcomingConflictChanged();
209             return;
210         }
211         long channelId = ContentUris.parseId(channelUri);
212         Channel channel = mChannelDataManager.getChannel(channelId);
213         // The conflicts caused by watching the channel.
214         List<ScheduledRecording> conflicts =
215                 mScheduleManager.getConflictingSchedulesForWatching(channel.getId());
216         long earliestToCheck = Long.MAX_VALUE;
217         long currentTimeMs = System.currentTimeMillis();
218         for (ScheduledRecording schedule : conflicts) {
219             long startTimeMs = schedule.getStartTimeMs();
220             if (startTimeMs < currentTimeMs + MIN_WATCH_CONFLICT_CHECK_TIME_MS) {
221                 // The start time of the upcoming conflict remains less than the minimum
222                 // check time.
223                 continue;
224             }
225             if (startTimeMs > currentTimeMs + MAX_WATCH_CONFLICT_CHECK_TIME_MS) {
226                 // The start time of the upcoming conflict remains greater than the
227                 // maximum check time. Setup the next re-check time.
228                 long nextCheckTimeMs = startTimeMs - MAX_WATCH_CONFLICT_CHECK_TIME_MS;
229                 if (earliestToCheck > nextCheckTimeMs) {
230                     earliestToCheck = nextCheckTimeMs;
231                 }
232             } else {
233                 // Found upcoming conflicts which will start soon.
234                 mUpcomingConflicts.add(schedule);
235                 // The schedule will be removed from the "upcoming conflict" when the
236                 // recording is almost started.
237                 long nextCheckTimeMs = startTimeMs - MIN_WATCH_CONFLICT_CHECK_TIME_MS;
238                 if (earliestToCheck > nextCheckTimeMs) {
239                     earliestToCheck = nextCheckTimeMs;
240                 }
241             }
242         }
243         if (earliestToCheck != Long.MAX_VALUE) {
244             mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, earliestToCheck - currentTimeMs);
245         }
246         if (DEBUG) Log.d(TAG, "upcoming conflicts: " + mUpcomingConflicts);
247         notifyUpcomingConflictChanged();
248         if (!mUpcomingConflicts.isEmpty()
249                 && !DvrUiHelper.isChannelWatchConflictDialogShown(mMainActivity)) {
250             // Don't show the conflict dialog if the user already knows.
251             List<ScheduledRecording> checkedConflicts = mCheckedConflictsMap.get(channel.getId());
252             if (checkedConflicts == null || !checkedConflicts.containsAll(mUpcomingConflicts)) {
253                 DvrUiHelper.showChannelWatchConflictDialog(mMainActivity, channel);
254             }
255         }
256     }
257 
258     private static class ConflictCheckerHandler extends WeakHandler<ConflictChecker> {
ConflictCheckerHandler(ConflictChecker conflictChecker)259         ConflictCheckerHandler(ConflictChecker conflictChecker) {
260             super(conflictChecker);
261         }
262 
263         @Override
handleMessage(Message msg, @NonNull ConflictChecker conflictChecker)264         protected void handleMessage(Message msg, @NonNull ConflictChecker conflictChecker) {
265             switch (msg.what) {
266                 case MSG_CHECK_CONFLICT:
267                     conflictChecker.onCheckConflict();
268                     break;
269             }
270         }
271     }
272 
273     /** A listener for the change of upcoming conflicts. */
274     public interface OnUpcomingConflictChangeListener {
onUpcomingConflictChange()275         void onUpcomingConflictChange();
276     }
277 }
278