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