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