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.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.Build;
26 import android.os.HandlerThread;
27 import android.os.Looper;
28 import android.support.annotation.MainThread;
29 import android.support.annotation.RequiresApi;
30 import android.support.annotation.VisibleForTesting;
31 import android.util.ArrayMap;
32 import android.util.Log;
33 import android.util.Range;
34 import com.android.tv.InputSessionManager;
35 import com.android.tv.TvSingletons;
36 import com.android.tv.common.SoftPreconditions;
37 import com.android.tv.common.util.Clock;
38 import com.android.tv.data.ChannelDataManager;
39 import com.android.tv.data.ChannelDataManager.Listener;
40 import com.android.tv.dvr.DvrDataManager;
41 import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
42 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
43 import com.android.tv.dvr.DvrManager;
44 import com.android.tv.dvr.WritableDvrDataManager;
45 import com.android.tv.dvr.data.ScheduledRecording;
46 import com.android.tv.util.TvInputManagerHelper;
47 import com.android.tv.util.Utils;
48 import java.util.Arrays;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.concurrent.TimeUnit;
52 
53 /**
54  * The core class to manage DVR schedule and run recording task. *
55  *
56  * <p>This class is responsible for:
57  *
58  * <ul>
59  *   <li>Sending record commands to TV inputs
60  *   <li>Resolving conflicting schedules, handling overlapping recording time durations, etc.
61  * </ul>
62  *
63  * <p>This should be a singleton associated with application's main process.
64  */
65 @RequiresApi(Build.VERSION_CODES.N)
66 @MainThread
67 public class RecordingScheduler extends TvInputCallback implements ScheduledRecordingListener {
68     private static final String TAG = "RecordingScheduler";
69     private static final boolean DEBUG = false;
70 
71     private static final String HANDLER_THREAD_NAME = "RecordingScheduler";
72     private static final long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(1);
73     @VisibleForTesting static final long MS_TO_WAKE_BEFORE_START = TimeUnit.SECONDS.toMillis(30);
74 
75     private final Looper mLooper;
76     private final InputSessionManager mSessionManager;
77     private final WritableDvrDataManager mDataManager;
78     private final DvrManager mDvrManager;
79     private final ChannelDataManager mChannelDataManager;
80     private final TvInputManagerHelper mInputManager;
81     private final Context mContext;
82     private final Clock mClock;
83     private final AlarmManager mAlarmManager;
84 
85     private final Map<String, InputTaskScheduler> mInputSchedulerMap = new ArrayMap<>();
86     private long mLastStartTimePendingMs;
87 
88     private OnDvrScheduleLoadFinishedListener mDvrScheduleLoadListener =
89             new OnDvrScheduleLoadFinishedListener() {
90                 @Override
91                 public void onDvrScheduleLoadFinished() {
92                     mDataManager.removeDvrScheduleLoadFinishedListener(this);
93                     if (isDbLoaded()) {
94                         updateInternal();
95                     }
96                 }
97             };
98 
99     private Listener mChannelDataLoadListener =
100             new Listener() {
101                 @Override
102                 public void onLoadFinished() {
103                     mChannelDataManager.removeListener(this);
104                     if (isDbLoaded()) {
105                         updateInternal();
106                     }
107                 }
108 
109                 @Override
110                 public void onChannelListUpdated() {}
111 
112                 @Override
113                 public void onChannelBrowsableChanged() {}
114             };
115 
116     /**
117      * Creates a scheduler to schedule alarms for scheduled recordings and create recording tasks.
118      * This method should be only called once in the life-cycle of the application.
119      */
createScheduler(Context context)120     public static RecordingScheduler createScheduler(Context context) {
121         SoftPreconditions.checkState(
122                 TvSingletons.getSingletons(context).getRecordingScheduler() == null);
123         HandlerThread handlerThread = new HandlerThread(HANDLER_THREAD_NAME);
124         handlerThread.start();
125         TvSingletons singletons = TvSingletons.getSingletons(context);
126         return new RecordingScheduler(
127                 handlerThread.getLooper(),
128                 singletons.getDvrManager(),
129                 singletons.getInputSessionManager(),
130                 (WritableDvrDataManager) singletons.getDvrDataManager(),
131                 singletons.getChannelDataManager(),
132                 singletons.getTvInputManagerHelper(),
133                 context,
134                 Clock.SYSTEM,
135                 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE));
136     }
137 
138     @VisibleForTesting
RecordingScheduler( Looper looper, DvrManager dvrManager, InputSessionManager sessionManager, WritableDvrDataManager dataManager, ChannelDataManager channelDataManager, TvInputManagerHelper inputManager, Context context, Clock clock, AlarmManager alarmManager)139     RecordingScheduler(
140             Looper looper,
141             DvrManager dvrManager,
142             InputSessionManager sessionManager,
143             WritableDvrDataManager dataManager,
144             ChannelDataManager channelDataManager,
145             TvInputManagerHelper inputManager,
146             Context context,
147             Clock clock,
148             AlarmManager alarmManager) {
149         mLooper = looper;
150         mDvrManager = dvrManager;
151         mSessionManager = sessionManager;
152         mDataManager = dataManager;
153         mChannelDataManager = channelDataManager;
154         mInputManager = inputManager;
155         mContext = context;
156         mClock = clock;
157         mAlarmManager = alarmManager;
158         mDataManager.addScheduledRecordingListener(this);
159         mInputManager.addCallback(this);
160         if (isDbLoaded()) {
161             updateInternal();
162         } else {
163             if (!mDataManager.isDvrScheduleLoadFinished()) {
164                 mDataManager.addDvrScheduleLoadFinishedListener(mDvrScheduleLoadListener);
165             }
166             if (!mChannelDataManager.isDbLoadFinished()) {
167                 mChannelDataManager.addListener(mChannelDataLoadListener);
168             }
169         }
170     }
171 
172     /** Start recording that will happen soon, and set the next alarm time. */
updateAndStartServiceIfNeeded()173     public void updateAndStartServiceIfNeeded() {
174         if (DEBUG) Log.d(TAG, "update and start service if needed");
175         if (isDbLoaded()) {
176             updateInternal();
177         } else {
178             // updateInternal will be called when DB is loaded. Start DvrRecordingService to
179             // prevent process being killed before that.
180             DvrRecordingService.startForegroundService(mContext, false);
181         }
182     }
183 
updateInternal()184     private void updateInternal() {
185         boolean recordingSoon = updatePendingRecordings();
186         updateNextAlarm();
187         if (recordingSoon) {
188             // Start DvrRecordingService to protect upcoming recording task from being killed.
189             DvrRecordingService.startForegroundService(mContext, true);
190         } else {
191             DvrRecordingService.stopForegroundIfNotRecording();
192         }
193     }
194 
updatePendingRecordings()195     private boolean updatePendingRecordings() {
196         List<ScheduledRecording> scheduledRecordings =
197                 mDataManager.getScheduledRecordings(
198                         new Range<>(
199                                 mLastStartTimePendingMs,
200                                 mClock.currentTimeMillis() + SOON_DURATION_IN_MS),
201                         ScheduledRecording.STATE_RECORDING_NOT_STARTED);
202         for (ScheduledRecording r : scheduledRecordings) {
203             scheduleRecordingSoon(r);
204         }
205         // update() may be called multiple times, under this situation, pending recordings may be
206         // already updated thus scheduledRecordings may have a size of 0. Therefore we also have to
207         // check mLastStartTimePendingMs to check if we have upcoming recordings and prevent the
208         // recording service being wrongly pushed back to background in updateInternal().
209         return scheduledRecordings.size() > 0
210                 || (mLastStartTimePendingMs > mClock.currentTimeMillis()
211                         && mLastStartTimePendingMs
212                                 < mClock.currentTimeMillis() + SOON_DURATION_IN_MS);
213     }
214 
isDbLoaded()215     private boolean isDbLoaded() {
216         return mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished();
217     }
218 
219     @Override
onScheduledRecordingAdded(ScheduledRecording... schedules)220     public void onScheduledRecordingAdded(ScheduledRecording... schedules) {
221         if (DEBUG) Log.d(TAG, "added " + Arrays.asList(schedules));
222         if (!isDbLoaded()) {
223             return;
224         }
225         handleScheduleChange(schedules);
226     }
227 
228     @Override
onScheduledRecordingRemoved(ScheduledRecording... schedules)229     public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
230         if (DEBUG) Log.d(TAG, "removed " + Arrays.asList(schedules));
231         if (!isDbLoaded()) {
232             return;
233         }
234         boolean needToUpdateAlarm = false;
235         for (ScheduledRecording schedule : schedules) {
236             InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(schedule.getInputId());
237             if (inputTaskScheduler != null) {
238                 inputTaskScheduler.removeSchedule(schedule);
239                 needToUpdateAlarm = true;
240             }
241         }
242         if (needToUpdateAlarm) {
243             updateNextAlarm();
244         }
245     }
246 
247     @Override
onScheduledRecordingStatusChanged(ScheduledRecording... schedules)248     public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
249         if (DEBUG) Log.d(TAG, "state changed " + Arrays.asList(schedules));
250         if (!isDbLoaded()) {
251             return;
252         }
253         // Update the recordings.
254         for (ScheduledRecording schedule : schedules) {
255             InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(schedule.getInputId());
256             if (inputTaskScheduler != null) {
257                 inputTaskScheduler.updateSchedule(schedule);
258             }
259         }
260         handleScheduleChange(schedules);
261     }
262 
handleScheduleChange(ScheduledRecording... schedules)263     private void handleScheduleChange(ScheduledRecording... schedules) {
264         boolean needToUpdateAlarm = false;
265         for (ScheduledRecording schedule : schedules) {
266             if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
267                 if (startsWithin(schedule, SOON_DURATION_IN_MS)) {
268                     scheduleRecordingSoon(schedule);
269                 } else {
270                     needToUpdateAlarm = true;
271                 }
272             }
273         }
274         if (needToUpdateAlarm) {
275             updateNextAlarm();
276         }
277     }
278 
scheduleRecordingSoon(ScheduledRecording schedule)279     private void scheduleRecordingSoon(ScheduledRecording schedule) {
280         TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
281         if (input == null) {
282             Log.e(TAG, "Can't find input for " + schedule);
283             mDataManager.changeState(
284                     schedule,
285                     ScheduledRecording.STATE_RECORDING_FAILED,
286                     ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE);
287             return;
288         }
289         if (!input.canRecord() || input.getTunerCount() <= 0) {
290             Log.e(TAG, "TV input doesn't support recording: " + input);
291             mDataManager.changeState(
292                     schedule,
293                     ScheduledRecording.STATE_RECORDING_FAILED,
294                     ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED);
295             return;
296         }
297         InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(input.getId());
298         if (inputTaskScheduler == null) {
299             inputTaskScheduler =
300                     new InputTaskScheduler(
301                             mContext,
302                             input,
303                             mLooper,
304                             mChannelDataManager,
305                             mDvrManager,
306                             mDataManager,
307                             mSessionManager,
308                             mClock);
309             mInputSchedulerMap.put(input.getId(), inputTaskScheduler);
310         }
311         inputTaskScheduler.addSchedule(schedule);
312         if (mLastStartTimePendingMs < schedule.getStartTimeMs()) {
313             mLastStartTimePendingMs = schedule.getStartTimeMs();
314         }
315     }
316 
updateNextAlarm()317     private void updateNextAlarm() {
318         long nextStartTime =
319                 mDataManager.getNextScheduledStartTimeAfter(
320                         Math.max(mLastStartTimePendingMs, mClock.currentTimeMillis()));
321         if (nextStartTime != DvrDataManager.NEXT_START_TIME_NOT_FOUND) {
322             long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START;
323             if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt);
324             Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class);
325             PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
326             // This will cancel the previous alarm.
327             mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent);
328         } else {
329             if (DEBUG) Log.d(TAG, "No future recording, alarm not set");
330         }
331     }
332 
333     @VisibleForTesting
startsWithin(ScheduledRecording scheduledRecording, long durationInMs)334     boolean startsWithin(ScheduledRecording scheduledRecording, long durationInMs) {
335         return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs;
336     }
337 
338     // No need to remove input task schedule worker when the input is removed. If the input is
339     // removed temporarily, the scheduler should keep the non-started schedules.
340     @Override
onInputUpdated(String inputId)341     public void onInputUpdated(String inputId) {
342         InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(inputId);
343         if (inputTaskScheduler != null) {
344             inputTaskScheduler.updateTvInputInfo(Utils.getTvInputInfoForInputId(mContext, inputId));
345         }
346     }
347 
348     @Override
onTvInputInfoUpdated(TvInputInfo input)349     public void onTvInputInfoUpdated(TvInputInfo input) {
350         InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(input.getId());
351         if (inputTaskScheduler != null) {
352             inputTaskScheduler.updateTvInputInfo(input);
353         }
354     }
355 }
356