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