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.app.AlarmManager;
20 import android.app.PendingIntent;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.media.tv.TvInputInfo;
24 import android.media.tv.TvInputManager.TvInputCallback;
25 import android.os.Looper;
26 import android.support.annotation.MainThread;
27 import android.support.annotation.VisibleForTesting;
28 import android.util.ArrayMap;
29 import android.util.Log;
30 import android.util.Range;
31 
32 import com.android.tv.InputSessionManager;
33 import com.android.tv.data.ChannelDataManager;
34 import com.android.tv.data.ChannelDataManager.Listener;
35 import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
36 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
37 import com.android.tv.util.Clock;
38 import com.android.tv.util.TvInputManagerHelper;
39 import com.android.tv.util.Utils;
40 
41 import java.util.Arrays;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.concurrent.TimeUnit;
45 
46 /**
47  * The core class to manage schedule and run actual recording.
48  */
49 @MainThread
50 public class Scheduler extends TvInputCallback implements ScheduledRecordingListener {
51     private static final String TAG = "Scheduler";
52     private static final boolean DEBUG = false;
53 
54     private final static long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(5);
55     @VisibleForTesting final static long MS_TO_WAKE_BEFORE_START = TimeUnit.MINUTES.toMillis(1);
56 
57     private final Looper mLooper;
58     private final InputSessionManager mSessionManager;
59     private final WritableDvrDataManager mDataManager;
60     private final DvrManager mDvrManager;
61     private final ChannelDataManager mChannelDataManager;
62     private final TvInputManagerHelper mInputManager;
63     private final Context mContext;
64     private final Clock mClock;
65     private final AlarmManager mAlarmManager;
66 
67     private final Map<String, InputTaskScheduler> mInputSchedulerMap = new ArrayMap<>();
68     private long mLastStartTimePendingMs;
69 
Scheduler(Looper looper, DvrManager dvrManager, InputSessionManager sessionManager, WritableDvrDataManager dataManager, ChannelDataManager channelDataManager, TvInputManagerHelper inputManager, Context context, Clock clock, AlarmManager alarmManager)70     public Scheduler(Looper looper, DvrManager dvrManager, InputSessionManager sessionManager,
71             WritableDvrDataManager dataManager, ChannelDataManager channelDataManager,
72             TvInputManagerHelper inputManager, Context context, Clock clock,
73             AlarmManager alarmManager) {
74         mLooper = looper;
75         mDvrManager = dvrManager;
76         mSessionManager = sessionManager;
77         mDataManager = dataManager;
78         mChannelDataManager = channelDataManager;
79         mInputManager = inputManager;
80         mContext = context;
81         mClock = clock;
82         mAlarmManager = alarmManager;
83     }
84 
85     /**
86      * Starts the scheduler.
87      */
start()88     public void start() {
89         mDataManager.addScheduledRecordingListener(this);
90         mInputManager.addCallback(this);
91         if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) {
92             updateInternal();
93         } else {
94             if (!mDataManager.isDvrScheduleLoadFinished()) {
95                 mDataManager.addDvrScheduleLoadFinishedListener(
96                         new OnDvrScheduleLoadFinishedListener() {
97                             @Override
98                             public void onDvrScheduleLoadFinished() {
99                                 mDataManager.removeDvrScheduleLoadFinishedListener(this);
100                                 updateInternal();
101                             }
102                         });
103             }
104             if (!mChannelDataManager.isDbLoadFinished()) {
105                 mChannelDataManager.addListener(new Listener() {
106                     @Override
107                     public void onLoadFinished() {
108                         mChannelDataManager.removeListener(this);
109                         updateInternal();
110                     }
111 
112                     @Override
113                     public void onChannelListUpdated() { }
114 
115                     @Override
116                     public void onChannelBrowsableChanged() { }
117                 });
118             }
119         }
120     }
121 
122     /**
123      * Stops the scheduler.
124      */
stop()125     public void stop() {
126         for (InputTaskScheduler inputTaskScheduler : mInputSchedulerMap.values()) {
127             inputTaskScheduler.stop();
128         }
129         mInputManager.removeCallback(this);
130         mDataManager.removeScheduledRecordingListener(this);
131     }
132 
updatePendingRecordings()133     private void updatePendingRecordings() {
134         List<ScheduledRecording> scheduledRecordings = mDataManager
135                 .getScheduledRecordings(new Range<>(mLastStartTimePendingMs,
136                         mClock.currentTimeMillis() + SOON_DURATION_IN_MS),
137                         ScheduledRecording.STATE_RECORDING_NOT_STARTED);
138         for (ScheduledRecording r : scheduledRecordings) {
139             scheduleRecordingSoon(r);
140         }
141     }
142 
143     /**
144      * Start recording that will happen soon, and set the next alarm time.
145      */
update()146     public void update() {
147         if (DEBUG) Log.d(TAG, "update");
148         updateInternal();
149     }
150 
updateInternal()151     private void updateInternal() {
152         if (isInitialized()) {
153             updatePendingRecordings();
154             updateNextAlarm();
155         }
156     }
157 
isInitialized()158     private boolean isInitialized() {
159         return mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished();
160     }
161 
162     @Override
onScheduledRecordingAdded(ScheduledRecording... schedules)163     public void onScheduledRecordingAdded(ScheduledRecording... schedules) {
164         if (DEBUG) Log.d(TAG, "added " + Arrays.asList(schedules));
165         if (!isInitialized()) {
166             return;
167         }
168         handleScheduleChange(schedules);
169     }
170 
171     @Override
onScheduledRecordingRemoved(ScheduledRecording... schedules)172     public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
173         if (DEBUG) Log.d(TAG, "removed " + Arrays.asList(schedules));
174         if (!isInitialized()) {
175             return;
176         }
177         boolean needToUpdateAlarm = false;
178         for (ScheduledRecording schedule : schedules) {
179             InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId());
180             if (scheduler != null) {
181                 scheduler.removeSchedule(schedule);
182                 needToUpdateAlarm = true;
183             }
184         }
185         if (needToUpdateAlarm) {
186             updateNextAlarm();
187         }
188     }
189 
190     @Override
onScheduledRecordingStatusChanged(ScheduledRecording... schedules)191     public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
192         if (DEBUG) Log.d(TAG, "state changed " + Arrays.asList(schedules));
193         if (!isInitialized()) {
194             return;
195         }
196         // Update the recordings.
197         for (ScheduledRecording schedule : schedules) {
198             InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId());
199             if (scheduler != null) {
200                 scheduler.updateSchedule(schedule);
201             }
202         }
203         handleScheduleChange(schedules);
204     }
205 
handleScheduleChange(ScheduledRecording... schedules)206     private void handleScheduleChange(ScheduledRecording... schedules) {
207         boolean needToUpdateAlarm = false;
208         for (ScheduledRecording schedule : schedules) {
209             if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
210                 if (startsWithin(schedule, SOON_DURATION_IN_MS)) {
211                     scheduleRecordingSoon(schedule);
212                 } else {
213                     needToUpdateAlarm = true;
214                 }
215             }
216         }
217         if (needToUpdateAlarm) {
218             updateNextAlarm();
219         }
220     }
221 
scheduleRecordingSoon(ScheduledRecording schedule)222     private void scheduleRecordingSoon(ScheduledRecording schedule) {
223         TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
224         if (input == null) {
225             Log.e(TAG, "Can't find input for " + schedule);
226             mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED);
227             return;
228         }
229         if (!input.canRecord() || input.getTunerCount() <= 0) {
230             Log.e(TAG, "TV input doesn't support recording: " + input);
231             mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED);
232             return;
233         }
234         InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId());
235         if (scheduler == null) {
236             scheduler = new InputTaskScheduler(mContext, input, mLooper, mChannelDataManager,
237                     mDvrManager, mDataManager, mSessionManager, mClock);
238             mInputSchedulerMap.put(input.getId(), scheduler);
239         }
240         scheduler.addSchedule(schedule);
241         if (mLastStartTimePendingMs < schedule.getStartTimeMs()) {
242             mLastStartTimePendingMs = schedule.getStartTimeMs();
243         }
244     }
245 
updateNextAlarm()246     private void updateNextAlarm() {
247         long nextStartTime = mDataManager.getNextScheduledStartTimeAfter(
248                 Math.max(mLastStartTimePendingMs, mClock.currentTimeMillis()));
249         if (nextStartTime != DvrDataManager.NEXT_START_TIME_NOT_FOUND) {
250             long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START;
251             if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt);
252             Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class);
253             PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
254             // This will cancel the previous alarm.
255             mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent);
256         } else {
257             if (DEBUG) Log.d(TAG, "No future recording, alarm not set");
258         }
259     }
260 
261     @VisibleForTesting
startsWithin(ScheduledRecording scheduledRecording, long durationInMs)262     boolean startsWithin(ScheduledRecording scheduledRecording, long durationInMs) {
263         return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs;
264     }
265 
266     // No need to remove input task scheduler when the input is removed. If the input is removed
267     // temporarily, the scheduler should keep the non-started schedules.
268     @Override
onInputUpdated(String inputId)269     public void onInputUpdated(String inputId) {
270         InputTaskScheduler scheduler = mInputSchedulerMap.get(inputId);
271         if (scheduler != null) {
272             scheduler.updateTvInputInfo(Utils.getTvInputInfoForInputId(mContext, inputId));
273         }
274     }
275 
276     @Override
onTvInputInfoUpdated(TvInputInfo input)277     public void onTvInputInfoUpdated(TvInputInfo input) {
278         InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId());
279         if (scheduler != null) {
280             scheduler.updateTvInputInfo(input);
281         }
282     }
283 }
284