/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.dvr.recorder; import android.content.Context; import android.media.tv.TvInputInfo; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.support.annotation.VisibleForTesting; import android.util.ArrayMap; import android.util.Log; import android.util.LongSparseArray; import com.android.tv.InputSessionManager; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.WritableDvrDataManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Clock; import com.android.tv.util.CompositeComparator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; /** * The scheduler for a TV input. */ public class InputTaskScheduler { private static final String TAG = "InputTaskScheduler"; private static final boolean DEBUG = false; private static final int MSG_ADD_SCHEDULED_RECORDING = 1; private static final int MSG_REMOVE_SCHEDULED_RECORDING = 2; private static final int MSG_UPDATE_SCHEDULED_RECORDING = 3; private static final int MSG_BUILD_SCHEDULE = 4; private static final int MSG_STOP_SCHEDULE = 5; private static final float MIN_REMAIN_DURATION_PERCENT = 0.05f; // The candidate comparator should be the consistent with // DvrScheduleManager#CANDIDATE_COMPARATOR. private static final Comparator CANDIDATE_COMPARATOR = new CompositeComparator<>( RecordingTask.PRIORITY_COMPARATOR, RecordingTask.END_TIME_COMPARATOR, RecordingTask.ID_COMPARATOR); /** * Returns the comparator which the schedules are sorted with when executed. */ public static Comparator getRecordingOrderComparator() { return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR; } /** * Wraps a {@link RecordingTask} removing it from {@link #mPendingRecordings} when it is done. */ public final class HandlerWrapper extends Handler { public static final int MESSAGE_REMOVE = 999; private final long mId; private final RecordingTask mTask; HandlerWrapper(Looper looper, ScheduledRecording scheduledRecording, RecordingTask recordingTask) { super(looper, recordingTask); mId = scheduledRecording.getId(); mTask = recordingTask; mTask.setHandler(this); } @Override public void handleMessage(Message msg) { // The RecordingTask gets a chance first. // It must return false to pass this message to here. if (msg.what == MESSAGE_REMOVE) { if (DEBUG) Log.d(TAG, "done " + mId); mPendingRecordings.remove(mId); } removeCallbacksAndMessages(null); mHandler.removeMessages(MSG_BUILD_SCHEDULE); mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); super.handleMessage(msg); } } private TvInputInfo mInput; private final Looper mLooper; private final ChannelDataManager mChannelDataManager; private final DvrManager mDvrManager; private final WritableDvrDataManager mDataManager; private final InputSessionManager mSessionManager; private final Clock mClock; private final Context mContext; private final LongSparseArray mPendingRecordings = new LongSparseArray<>(); private final Map mWaitingSchedules = new ArrayMap<>(); private final Handler mMainThreadHandler; private final Handler mHandler; private final Object mInputLock = new Object(); private final RecordingTaskFactory mRecordingTaskFactory; public InputTaskScheduler(Context context, TvInputInfo input, Looper looper, ChannelDataManager channelDataManager, DvrManager dvrManager, DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock) { this(context, input, looper, channelDataManager, dvrManager, dataManager, sessionManager, clock, null); } @VisibleForTesting InputTaskScheduler(Context context, TvInputInfo input, Looper looper, ChannelDataManager channelDataManager, DvrManager dvrManager, DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock, RecordingTaskFactory recordingTaskFactory) { if (DEBUG) Log.d(TAG, "Creating scheduler for " + input); mContext = context; mInput = input; mLooper = looper; mChannelDataManager = channelDataManager; mDvrManager = dvrManager; mDataManager = (WritableDvrDataManager) dataManager; mSessionManager = sessionManager; mClock = clock; mMainThreadHandler = new Handler(Looper.getMainLooper()); mRecordingTaskFactory = recordingTaskFactory != null ? recordingTaskFactory : new RecordingTaskFactory() { @Override public RecordingTask createRecordingTask(ScheduledRecording schedule, Channel channel, DvrManager dvrManager, InputSessionManager sessionManager, WritableDvrDataManager dataManager, Clock clock) { return new RecordingTask(mContext, schedule, channel, mDvrManager, mSessionManager, mDataManager, mClock); } }; mHandler = new WorkerThreadHandler(looper); } /** * Adds a {@link ScheduledRecording}. */ public void addSchedule(ScheduledRecording schedule) { mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_SCHEDULED_RECORDING, schedule)); } @VisibleForTesting void handleAddSchedule(ScheduledRecording schedule) { if (mPendingRecordings.get(schedule.getId()) != null || mWaitingSchedules.containsKey(schedule.getId())) { return; } mWaitingSchedules.put(schedule.getId(), schedule); mHandler.removeMessages(MSG_BUILD_SCHEDULE); mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); } /** * Removes the {@link ScheduledRecording}. */ public void removeSchedule(ScheduledRecording schedule) { mHandler.sendMessage(mHandler.obtainMessage(MSG_REMOVE_SCHEDULED_RECORDING, schedule)); } @VisibleForTesting void handleRemoveSchedule(ScheduledRecording schedule) { HandlerWrapper wrapper = mPendingRecordings.get(schedule.getId()); if (wrapper != null) { wrapper.mTask.cancel(); return; } if (mWaitingSchedules.containsKey(schedule.getId())) { mWaitingSchedules.remove(schedule.getId()); mHandler.removeMessages(MSG_BUILD_SCHEDULE); mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); } } /** * Updates the {@link ScheduledRecording}. */ public void updateSchedule(ScheduledRecording schedule) { mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SCHEDULED_RECORDING, schedule)); } @VisibleForTesting void handleUpdateSchedule(ScheduledRecording schedule) { HandlerWrapper wrapper = mPendingRecordings.get(schedule.getId()); if (wrapper != null) { if (schedule.getStartTimeMs() > mClock.currentTimeMillis() && schedule.getStartTimeMs() > wrapper.mTask.getStartTimeMs()) { // It shouldn't have started. Cancel and put to the waiting list. // The schedules will be rebuilt when the task is removed. // The reschedule is called in RecordingScheduler. wrapper.mTask.cancel(); mWaitingSchedules.put(schedule.getId(), schedule); return; } wrapper.sendMessage(wrapper.obtainMessage(RecordingTask.MSG_UDPATE_SCHEDULE, schedule)); return; } if (mWaitingSchedules.containsKey(schedule.getId())) { mWaitingSchedules.put(schedule.getId(), schedule); mHandler.removeMessages(MSG_BUILD_SCHEDULE); mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); } } /** * Updates the TV input. */ public void updateTvInputInfo(TvInputInfo input) { synchronized (mInputLock) { mInput = input; } } /** * Stops the input task scheduler. */ public void stop() { mHandler.removeCallbacksAndMessages(null); mHandler.sendEmptyMessage(MSG_STOP_SCHEDULE); } private void handleStopSchedule() { mWaitingSchedules.clear(); int size = mPendingRecordings.size(); for (int i = 0; i < size; ++i) { RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; task.cleanUp(); } } @VisibleForTesting void handleBuildSchedule() { if (mWaitingSchedules.isEmpty()) { return; } long currentTimeMs = mClock.currentTimeMillis(); // Remove past schedules. for (Iterator iter = mWaitingSchedules.values().iterator(); iter.hasNext(); ) { ScheduledRecording schedule = iter.next(); if (schedule.getEndTimeMs() - currentTimeMs <= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) { fail(schedule); iter.remove(); } } if (mWaitingSchedules.isEmpty()) { return; } // Record the schedules which should start now. List schedulesToStart = new ArrayList<>(); for (ScheduledRecording schedule : mWaitingSchedules.values()) { if (schedule.getState() != ScheduledRecording.STATE_RECORDING_CANCELED && schedule.getStartTimeMs() - RecordingTask.RECORDING_EARLY_START_OFFSET_MS <= currentTimeMs && schedule.getEndTimeMs() > currentTimeMs) { schedulesToStart.add(schedule); } } // The schedules will be executed with the following order. // 1. The schedule which starts early. It can be replaced later when the schedule with the // higher priority needs to start. // 2. The schedule with the higher priority. It can be replaced later when the schedule with // the higher priority needs to start. // 3. The schedule which was created recently. Collections.sort(schedulesToStart, getRecordingOrderComparator()); int tunerCount; synchronized (mInputLock) { tunerCount = mInput.canRecord() ? mInput.getTunerCount() : 0; } for (ScheduledRecording schedule : schedulesToStart) { if (hasTaskWhichFinishEarlier(schedule)) { // If there is a schedule which finishes earlier than the new schedule, rebuild the // schedules after it finishes. return; } if (mPendingRecordings.size() < tunerCount) { // Tuners available. createRecordingTask(schedule).start(); mWaitingSchedules.remove(schedule.getId()); } else { // No available tuners. RecordingTask task = getReplacableTask(schedule); if (task != null) { task.stop(); // Just return. The schedules will be rebuilt after the task is stopped. return; } } } if (mWaitingSchedules.isEmpty()) { return; } // Set next scheduling. long earliest = Long.MAX_VALUE; for (ScheduledRecording schedule : mWaitingSchedules.values()) { // The conflicting schedules will be removed if they end before conflicting resolved. if (schedulesToStart.contains(schedule)) { if (earliest > schedule.getEndTimeMs()) { earliest = schedule.getEndTimeMs(); } } else { if (earliest > schedule.getStartTimeMs() - RecordingTask.RECORDING_EARLY_START_OFFSET_MS) { earliest = schedule.getStartTimeMs() - RecordingTask.RECORDING_EARLY_START_OFFSET_MS; } } } mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest - currentTimeMs); } private RecordingTask createRecordingTask(ScheduledRecording schedule) { Channel channel = mChannelDataManager.getChannel(schedule.getChannelId()); RecordingTask recordingTask = mRecordingTaskFactory.createRecordingTask(schedule, channel, mDvrManager, mSessionManager, mDataManager, mClock); HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, schedule, recordingTask); mPendingRecordings.put(schedule.getId(), handlerWrapper); return recordingTask; } private boolean hasTaskWhichFinishEarlier(ScheduledRecording schedule) { int size = mPendingRecordings.size(); for (int i = 0; i < size; ++i) { RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; if (task.getEndTimeMs() <= schedule.getStartTimeMs()) { return true; } } return false; } private RecordingTask getReplacableTask(ScheduledRecording schedule) { // Returns the recording with the following priority. // 1. The recording with the lowest priority is returned. // 2. If the priorities are the same, the recording which finishes early is returned. // 3. If 1) and 2) are the same, the early created schedule is returned. int size = mPendingRecordings.size(); RecordingTask candidate = null; for (int i = 0; i < size; ++i) { RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; if (schedule.getPriority() > task.getPriority()) { if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, task) > 0) { candidate = task; } } } return candidate; } private void fail(ScheduledRecording schedule) { // It's called when the scheduling has been failed without creating RecordingTask. runOnMainHandler(new Runnable() { @Override public void run() { ScheduledRecording scheduleInManager = mDataManager.getScheduledRecording(schedule.getId()); if (scheduleInManager != null) { // The schedule should be updated based on the object from DataManager in case // when it has been updated. mDataManager.changeState(scheduleInManager, ScheduledRecording.STATE_RECORDING_FAILED); } } }); } private void runOnMainHandler(Runnable runnable) { if (Looper.myLooper() == mMainThreadHandler.getLooper()) { runnable.run(); } else { mMainThreadHandler.post(runnable); } } @VisibleForTesting interface RecordingTaskFactory { RecordingTask createRecordingTask(ScheduledRecording scheduledRecording, Channel channel, DvrManager dvrManager, InputSessionManager sessionManager, WritableDvrDataManager dataManager, Clock clock); } private class WorkerThreadHandler extends Handler { public WorkerThreadHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_ADD_SCHEDULED_RECORDING: handleAddSchedule((ScheduledRecording) msg.obj); break; case MSG_REMOVE_SCHEDULED_RECORDING: handleRemoveSchedule((ScheduledRecording) msg.obj); break; case MSG_UPDATE_SCHEDULED_RECORDING: handleUpdateSchedule((ScheduledRecording) msg.obj); case MSG_BUILD_SCHEDULE: handleBuildSchedule(); break; case MSG_STOP_SCHEDULE: handleStopSchedule(); break; } } } }