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