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