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
17package com.android.tv.dvr.recorder;
18
19import android.content.Context;
20import android.media.tv.TvInputInfo;
21import android.os.Handler;
22import android.os.Looper;
23import android.os.Message;
24import android.support.annotation.VisibleForTesting;
25import android.util.ArrayMap;
26import android.util.Log;
27import android.util.LongSparseArray;
28
29import com.android.tv.InputSessionManager;
30import com.android.tv.data.Channel;
31import com.android.tv.data.ChannelDataManager;
32import com.android.tv.dvr.DvrDataManager;
33import com.android.tv.dvr.DvrManager;
34import com.android.tv.dvr.WritableDvrDataManager;
35import com.android.tv.dvr.data.ScheduledRecording;
36import com.android.tv.util.Clock;
37import com.android.tv.util.CompositeComparator;
38
39import java.util.ArrayList;
40import java.util.Collections;
41import java.util.Comparator;
42import java.util.Iterator;
43import java.util.List;
44import java.util.Map;
45
46/**
47 * The scheduler for a TV input.
48 */
49public 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     */
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
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
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
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
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     */
161    public void addSchedule(ScheduledRecording schedule) {
162        mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_SCHEDULED_RECORDING, schedule));
163    }
164
165    @VisibleForTesting
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     */
179    public void removeSchedule(ScheduledRecording schedule) {
180        mHandler.sendMessage(mHandler.obtainMessage(MSG_REMOVE_SCHEDULED_RECORDING, schedule));
181    }
182
183    @VisibleForTesting
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     */
200    public void updateSchedule(ScheduledRecording schedule) {
201        mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SCHEDULED_RECORDING, schedule));
202    }
203
204    @VisibleForTesting
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     */
230    public void updateTvInputInfo(TvInputInfo input) {
231        synchronized (mInputLock) {
232            mInput = input;
233        }
234    }
235
236    /**
237     * Stops the input task scheduler.
238     */
239    public void stop() {
240        mHandler.removeCallbacksAndMessages(null);
241        mHandler.sendEmptyMessage(MSG_STOP_SCHEDULE);
242    }
243
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
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
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
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
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
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
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 {
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 {
405        public WorkerThreadHandler(Looper looper) {
406            super(looper);
407        }
408
409        @Override
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