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