• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.content.Context;
20 import android.media.tv.TvInputInfo;
21 import android.os.Handler;
22 import android.os.Looper;
23 import android.os.Message;
24 import android.support.annotation.VisibleForTesting;
25 import android.util.ArrayMap;
26 import android.util.Log;
27 import android.util.LongSparseArray;
28 import com.android.tv.InputSessionManager;
29 import com.android.tv.common.util.Clock;
30 import com.android.tv.data.ChannelDataManager;
31 import com.android.tv.data.api.Channel;
32 import com.android.tv.dvr.DvrDataManager;
33 import com.android.tv.dvr.DvrManager;
34 import com.android.tv.dvr.WritableDvrDataManager;
35 import com.android.tv.dvr.data.ScheduledRecording;
36 import com.android.tv.util.CompositeComparator;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.Comparator;
40 import java.util.Iterator;
41 import java.util.List;
42 import java.util.Map;
43 
44 /** The scheduler for a TV input. */
45 public class InputTaskScheduler {
46     private static final String TAG = "InputTaskScheduler";
47     private static final boolean DEBUG = false;
48 
49     private static final int MSG_ADD_SCHEDULED_RECORDING = 1;
50     private static final int MSG_REMOVE_SCHEDULED_RECORDING = 2;
51     private static final int MSG_UPDATE_SCHEDULED_RECORDING = 3;
52     private static final int MSG_BUILD_SCHEDULE = 4;
53     private static final int MSG_STOP_SCHEDULE = 5;
54 
55     private static final float MIN_REMAIN_DURATION_PERCENT = 0.05f;
56 
57     // The candidate comparator should be the consistent with
58     // DvrScheduleManager#CANDIDATE_COMPARATOR.
59     private static final Comparator<RecordingTask> CANDIDATE_COMPARATOR =
60             new CompositeComparator<>(
61                     RecordingTask.PRIORITY_COMPARATOR,
62                     RecordingTask.END_TIME_COMPARATOR,
63                     RecordingTask.ID_COMPARATOR);
64 
65     /** Returns the comparator which the schedules are sorted with when executed. */
getRecordingOrderComparator()66     public static Comparator<ScheduledRecording> getRecordingOrderComparator() {
67         return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR;
68     }
69 
70     /**
71      * Wraps a {@link RecordingTask} removing it from {@link #mPendingRecordings} when it is done.
72      */
73     public final class HandlerWrapper extends Handler {
74         public static final int MESSAGE_REMOVE = 999;
75         private final long mId;
76         private final RecordingTask mTask;
77 
HandlerWrapper( Looper looper, ScheduledRecording scheduledRecording, RecordingTask recordingTask)78         HandlerWrapper(
79                 Looper looper, ScheduledRecording scheduledRecording, RecordingTask recordingTask) {
80             super(looper, recordingTask);
81             mId = scheduledRecording.getId();
82             mTask = recordingTask;
83             mTask.setHandler(this);
84         }
85 
86         @Override
handleMessage(Message msg)87         public void handleMessage(Message msg) {
88             // The RecordingTask gets a chance first.
89             // It must return false to pass this message to here.
90             if (msg.what == MESSAGE_REMOVE) {
91                 if (DEBUG) Log.d(TAG, "done " + mId);
92                 mPendingRecordings.remove(mId);
93             }
94             removeCallbacksAndMessages(null);
95             mHandler.removeMessages(MSG_BUILD_SCHEDULE);
96             mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE);
97             super.handleMessage(msg);
98         }
99     }
100 
101     private TvInputInfo mInput;
102     private final Looper mLooper;
103     private final ChannelDataManager mChannelDataManager;
104     private final DvrManager mDvrManager;
105     private final WritableDvrDataManager mDataManager;
106     private final InputSessionManager mSessionManager;
107     private final Clock mClock;
108     private final Context mContext;
109 
110     private final LongSparseArray<HandlerWrapper> mPendingRecordings = new LongSparseArray<>();
111     private final Map<Long, ScheduledRecording> mWaitingSchedules = new ArrayMap<>();
112     private final Handler mMainThreadHandler;
113     private final Handler mHandler;
114     private final Object mInputLock = new Object();
115     private final RecordingTaskFactory mRecordingTaskFactory;
116 
InputTaskScheduler( Context context, TvInputInfo input, Looper looper, ChannelDataManager channelDataManager, DvrManager dvrManager, DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock)117     public InputTaskScheduler(
118             Context context,
119             TvInputInfo input,
120             Looper looper,
121             ChannelDataManager channelDataManager,
122             DvrManager dvrManager,
123             DvrDataManager dataManager,
124             InputSessionManager sessionManager,
125             Clock clock) {
126         this(
127                 context,
128                 input,
129                 looper,
130                 channelDataManager,
131                 dvrManager,
132                 dataManager,
133                 sessionManager,
134                 clock,
135                 null);
136     }
137 
138     @VisibleForTesting
InputTaskScheduler( Context context, TvInputInfo input, Looper looper, ChannelDataManager channelDataManager, DvrManager dvrManager, DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock, RecordingTaskFactory recordingTaskFactory)139     InputTaskScheduler(
140             Context context,
141             TvInputInfo input,
142             Looper looper,
143             ChannelDataManager channelDataManager,
144             DvrManager dvrManager,
145             DvrDataManager dataManager,
146             InputSessionManager sessionManager,
147             Clock clock,
148             RecordingTaskFactory recordingTaskFactory) {
149         if (DEBUG) Log.d(TAG, "Creating scheduler for " + input);
150         mContext = context;
151         mInput = input;
152         mLooper = looper;
153         mChannelDataManager = channelDataManager;
154         mDvrManager = dvrManager;
155         mDataManager = (WritableDvrDataManager) dataManager;
156         mSessionManager = sessionManager;
157         mClock = clock;
158         mMainThreadHandler = new Handler(Looper.getMainLooper());
159         mRecordingTaskFactory =
160                 recordingTaskFactory != null
161                         ? recordingTaskFactory
162                         : new RecordingTaskFactory() {
163                             @Override
164                             public RecordingTask createRecordingTask(
165                                     ScheduledRecording schedule,
166                                     Channel channel,
167                                     DvrManager dvrManager,
168                                     InputSessionManager sessionManager,
169                                     WritableDvrDataManager dataManager,
170                                     Clock clock) {
171                                 return new RecordingTask(
172                                         mContext,
173                                         schedule,
174                                         channel,
175                                         mDvrManager,
176                                         mSessionManager,
177                                         mDataManager,
178                                         mClock);
179                             }
180                         };
181         mHandler = new WorkerThreadHandler(looper);
182     }
183 
184     /** Adds a {@link ScheduledRecording}. */
addSchedule(ScheduledRecording schedule)185     public void addSchedule(ScheduledRecording schedule) {
186         mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_SCHEDULED_RECORDING, schedule));
187     }
188 
189     @VisibleForTesting
handleAddSchedule(ScheduledRecording schedule)190     void handleAddSchedule(ScheduledRecording schedule) {
191         if (mPendingRecordings.get(schedule.getId()) != null
192                 || mWaitingSchedules.containsKey(schedule.getId())) {
193             return;
194         }
195         mWaitingSchedules.put(schedule.getId(), schedule);
196         mHandler.removeMessages(MSG_BUILD_SCHEDULE);
197         mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE);
198     }
199 
200     /** Removes the {@link ScheduledRecording}. */
removeSchedule(ScheduledRecording schedule)201     public void removeSchedule(ScheduledRecording schedule) {
202         mHandler.sendMessage(mHandler.obtainMessage(MSG_REMOVE_SCHEDULED_RECORDING, schedule));
203     }
204 
205     @VisibleForTesting
handleRemoveSchedule(ScheduledRecording schedule)206     void handleRemoveSchedule(ScheduledRecording schedule) {
207         HandlerWrapper wrapper = mPendingRecordings.get(schedule.getId());
208         if (wrapper != null) {
209             wrapper.mTask.cancel();
210             return;
211         }
212         if (mWaitingSchedules.containsKey(schedule.getId())) {
213             mWaitingSchedules.remove(schedule.getId());
214             mHandler.removeMessages(MSG_BUILD_SCHEDULE);
215             mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE);
216         }
217     }
218 
219     /** Updates the {@link ScheduledRecording}. */
updateSchedule(ScheduledRecording schedule)220     public void updateSchedule(ScheduledRecording schedule) {
221         mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SCHEDULED_RECORDING, schedule));
222     }
223 
224     @VisibleForTesting
handleUpdateSchedule(ScheduledRecording schedule)225     void handleUpdateSchedule(ScheduledRecording schedule) {
226         HandlerWrapper wrapper = mPendingRecordings.get(schedule.getId());
227         if (wrapper != null) {
228             if (schedule.getStartTimeMs() > mClock.currentTimeMillis()
229                     && schedule.getStartTimeMs() > wrapper.mTask.getStartTimeMs()) {
230                 // It shouldn't have started. Cancel and put to the waiting list.
231                 // The schedules will be rebuilt when the task is removed.
232                 // The reschedule is called in RecordingScheduler.
233                 wrapper.mTask.cancel();
234                 mWaitingSchedules.put(schedule.getId(), schedule);
235                 return;
236             }
237             wrapper.sendMessage(wrapper.obtainMessage(RecordingTask.MSG_UDPATE_SCHEDULE, schedule));
238             return;
239         }
240         if (mWaitingSchedules.containsKey(schedule.getId())) {
241             mWaitingSchedules.put(schedule.getId(), schedule);
242             mHandler.removeMessages(MSG_BUILD_SCHEDULE);
243             mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE);
244         }
245     }
246 
247     /** Updates the TV input. */
updateTvInputInfo(TvInputInfo input)248     public void updateTvInputInfo(TvInputInfo input) {
249         synchronized (mInputLock) {
250             mInput = input;
251         }
252     }
253 
254     /** Stops the input task scheduler. */
stop()255     public void stop() {
256         mHandler.removeCallbacksAndMessages(null);
257         mHandler.sendEmptyMessage(MSG_STOP_SCHEDULE);
258     }
259 
handleStopSchedule()260     private void handleStopSchedule() {
261         mWaitingSchedules.clear();
262         int size = mPendingRecordings.size();
263         for (int i = 0; i < size; ++i) {
264             RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask;
265             task.cleanUp();
266         }
267     }
268 
269     @VisibleForTesting
handleBuildSchedule()270     void handleBuildSchedule() {
271         if (mWaitingSchedules.isEmpty()) {
272             return;
273         }
274         long currentTimeMs = mClock.currentTimeMillis();
275         // Remove past schedules.
276         for (Iterator<ScheduledRecording> iter = mWaitingSchedules.values().iterator();
277                 iter.hasNext(); ) {
278             ScheduledRecording schedule = iter.next();
279             if (schedule.getEndTimeMs() - currentTimeMs
280                     <= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) {
281                 Log.e(TAG, "Error! Program ended before recording started:" + schedule);
282                 fail(
283                         schedule,
284                         ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED);
285                 iter.remove();
286             }
287         }
288         if (mWaitingSchedules.isEmpty()) {
289             return;
290         }
291         // Record the schedules which should start now.
292         List<ScheduledRecording> schedulesToStart = new ArrayList<>();
293         for (ScheduledRecording schedule : mWaitingSchedules.values()) {
294             if (schedule.getState() != ScheduledRecording.STATE_RECORDING_CANCELED
295                     && schedule.getStartTimeMs() - RecordingTask.RECORDING_EARLY_START_OFFSET_MS
296                             <= currentTimeMs
297                     && schedule.getEndTimeMs() > currentTimeMs) {
298                 schedulesToStart.add(schedule);
299             }
300         }
301         // The schedules will be executed with the following order.
302         // 1. The schedule which starts early. It can be replaced later when the schedule with the
303         //    higher priority needs to start.
304         // 2. The schedule with the higher priority. It can be replaced later when the schedule with
305         //    the higher priority needs to start.
306         // 3. The schedule which was created recently.
307         Collections.sort(schedulesToStart, getRecordingOrderComparator());
308         int tunerCount;
309         synchronized (mInputLock) {
310             tunerCount = mInput.canRecord() ? mInput.getTunerCount() : 0;
311         }
312         for (ScheduledRecording schedule : schedulesToStart) {
313             if (hasTaskWhichFinishEarlier(schedule)) {
314                 // If there is a schedule which finishes earlier than the new schedule, rebuild the
315                 // schedules after it finishes.
316                 return;
317             }
318             if (mPendingRecordings.size() < tunerCount) {
319                 // Tuners available.
320                 createRecordingTask(schedule).start();
321                 mWaitingSchedules.remove(schedule.getId());
322             } else {
323                 // No available tuners.
324                 RecordingTask task = getReplacableTask(schedule);
325                 if (task != null) {
326                     task.stop();
327                     // Just return. The schedules will be rebuilt after the task is stopped.
328                     return;
329                 }
330             }
331         }
332         if (mWaitingSchedules.isEmpty()) {
333             return;
334         }
335         // Set next scheduling.
336         long earliest = Long.MAX_VALUE;
337         for (ScheduledRecording schedule : mWaitingSchedules.values()) {
338             // The conflicting schedules will be removed if they end before conflicting resolved.
339             if (schedulesToStart.contains(schedule)) {
340                 if (earliest > schedule.getEndTimeMs()) {
341                     earliest = schedule.getEndTimeMs();
342                 }
343             } else {
344                 if (earliest
345                         > schedule.getStartTimeMs()
346                                 - RecordingTask.RECORDING_EARLY_START_OFFSET_MS) {
347                     earliest =
348                             schedule.getStartTimeMs()
349                                     - RecordingTask.RECORDING_EARLY_START_OFFSET_MS;
350                 }
351             }
352         }
353         mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest - currentTimeMs);
354     }
355 
createRecordingTask(ScheduledRecording schedule)356     private RecordingTask createRecordingTask(ScheduledRecording schedule) {
357         Channel channel = mChannelDataManager.getChannel(schedule.getChannelId());
358         RecordingTask recordingTask =
359                 mRecordingTaskFactory.createRecordingTask(
360                         schedule, channel, mDvrManager, mSessionManager, mDataManager, mClock);
361         HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, schedule, recordingTask);
362         mPendingRecordings.put(schedule.getId(), handlerWrapper);
363         return recordingTask;
364     }
365 
hasTaskWhichFinishEarlier(ScheduledRecording schedule)366     private boolean hasTaskWhichFinishEarlier(ScheduledRecording schedule) {
367         int size = mPendingRecordings.size();
368         for (int i = 0; i < size; ++i) {
369             RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask;
370             if (task.getEndTimeMs() <= schedule.getStartTimeMs()) {
371                 return true;
372             }
373         }
374         return false;
375     }
376 
getReplacableTask(ScheduledRecording schedule)377     private RecordingTask getReplacableTask(ScheduledRecording schedule) {
378         // Returns the recording with the following priority.
379         // 1. The recording with the lowest priority is returned.
380         // 2. If the priorities are the same, the recording which finishes early is returned.
381         // 3. If 1) and 2) are the same, the early created schedule is returned.
382         int size = mPendingRecordings.size();
383         RecordingTask candidate = null;
384         for (int i = 0; i < size; ++i) {
385             RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask;
386             if (schedule.getPriority() > task.getPriority()) {
387                 if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, task) > 0) {
388                     candidate = task;
389                 }
390             }
391         }
392         return candidate;
393     }
394 
fail(ScheduledRecording schedule, int reason)395     private void fail(ScheduledRecording schedule, int reason) {
396         // It's called when the scheduling has been failed without creating RecordingTask.
397         runOnMainHandler(
398                 () -> {
399                     ScheduledRecording scheduleInManager =
400                             mDataManager.getScheduledRecording(schedule.getId());
401                     if (scheduleInManager != null) {
402                         // The schedule should be updated based on the object from DataManager
403                         // in case when it has been updated.
404                         mDataManager.changeState(
405                                 scheduleInManager,
406                                 ScheduledRecording.STATE_RECORDING_FAILED,
407                                 reason);
408                     }
409                 });
410     }
411 
runOnMainHandler(Runnable runnable)412     private void runOnMainHandler(Runnable runnable) {
413         if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
414             runnable.run();
415         } else {
416             mMainThreadHandler.post(runnable);
417         }
418     }
419 
420     @VisibleForTesting
421     interface RecordingTaskFactory {
createRecordingTask( ScheduledRecording scheduledRecording, Channel channel, DvrManager dvrManager, InputSessionManager sessionManager, WritableDvrDataManager dataManager, Clock clock)422         RecordingTask createRecordingTask(
423                 ScheduledRecording scheduledRecording,
424                 Channel channel,
425                 DvrManager dvrManager,
426                 InputSessionManager sessionManager,
427                 WritableDvrDataManager dataManager,
428                 Clock clock);
429     }
430 
431     private class WorkerThreadHandler extends Handler {
WorkerThreadHandler(Looper looper)432         public WorkerThreadHandler(Looper looper) {
433             super(looper);
434         }
435 
436         @Override
handleMessage(Message msg)437         public void handleMessage(Message msg) {
438             switch (msg.what) {
439                 case MSG_ADD_SCHEDULED_RECORDING:
440                     handleAddSchedule((ScheduledRecording) msg.obj);
441                     break;
442                 case MSG_REMOVE_SCHEDULED_RECORDING:
443                     handleRemoveSchedule((ScheduledRecording) msg.obj);
444                     break;
445                 case MSG_UPDATE_SCHEDULED_RECORDING:
446                     handleUpdateSchedule((ScheduledRecording) msg.obj);
447                     break;
448                 case MSG_BUILD_SCHEDULE:
449                     handleBuildSchedule();
450                     break;
451                 case MSG_STOP_SCHEDULE:
452                     handleStopSchedule();
453                     break;
454             }
455         }
456     }
457 }
458