| /* |
| * 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<RecordingTask> 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<ScheduledRecording> 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<HandlerWrapper> mPendingRecordings = new LongSparseArray<>(); |
| private final Map<Long, ScheduledRecording> 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<ScheduledRecording> 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<ScheduledRecording> 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; |
| } |
| } |
| } |
| } |