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. */ 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 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 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 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 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}. */ 185 public void addSchedule(ScheduledRecording schedule) { 186 mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_SCHEDULED_RECORDING, schedule)); 187 } 188 189 @VisibleForTesting 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}. */ 201 public void removeSchedule(ScheduledRecording schedule) { 202 mHandler.sendMessage(mHandler.obtainMessage(MSG_REMOVE_SCHEDULED_RECORDING, schedule)); 203 } 204 205 @VisibleForTesting 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}. */ 220 public void updateSchedule(ScheduledRecording schedule) { 221 mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SCHEDULED_RECORDING, schedule)); 222 } 223 224 @VisibleForTesting 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. */ 248 public void updateTvInputInfo(TvInputInfo input) { 249 synchronized (mInputLock) { 250 mInput = input; 251 } 252 } 253 254 /** Stops the input task scheduler. */ 255 public void stop() { 256 mHandler.removeCallbacksAndMessages(null); 257 mHandler.sendEmptyMessage(MSG_STOP_SCHEDULE); 258 } 259 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 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(schedule, 283 ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED); 284 iter.remove(); 285 } 286 } 287 if (mWaitingSchedules.isEmpty()) { 288 return; 289 } 290 // Record the schedules which should start now. 291 List<ScheduledRecording> schedulesToStart = new ArrayList<>(); 292 for (ScheduledRecording schedule : mWaitingSchedules.values()) { 293 if (schedule.getState() != ScheduledRecording.STATE_RECORDING_CANCELED 294 && schedule.getStartTimeMs() - RecordingTask.RECORDING_EARLY_START_OFFSET_MS 295 <= currentTimeMs 296 && schedule.getEndTimeMs() > currentTimeMs) { 297 schedulesToStart.add(schedule); 298 } 299 } 300 // The schedules will be executed with the following order. 301 // 1. The schedule which starts early. It can be replaced later when the schedule with the 302 // higher priority needs to start. 303 // 2. The schedule with the higher priority. It can be replaced later when the schedule with 304 // the higher priority needs to start. 305 // 3. The schedule which was created recently. 306 Collections.sort(schedulesToStart, getRecordingOrderComparator()); 307 int tunerCount; 308 synchronized (mInputLock) { 309 tunerCount = mInput.canRecord() ? mInput.getTunerCount() : 0; 310 } 311 for (ScheduledRecording schedule : schedulesToStart) { 312 if (hasTaskWhichFinishEarlier(schedule)) { 313 // If there is a schedule which finishes earlier than the new schedule, rebuild the 314 // schedules after it finishes. 315 return; 316 } 317 if (mPendingRecordings.size() < tunerCount) { 318 // Tuners available. 319 createRecordingTask(schedule).start(); 320 mWaitingSchedules.remove(schedule.getId()); 321 } else { 322 // No available tuners. 323 RecordingTask task = getReplacableTask(schedule); 324 if (task != null) { 325 task.stop(); 326 // Just return. The schedules will be rebuilt after the task is stopped. 327 return; 328 } 329 } 330 } 331 if (mWaitingSchedules.isEmpty()) { 332 return; 333 } 334 // Set next scheduling. 335 long earliest = Long.MAX_VALUE; 336 for (ScheduledRecording schedule : mWaitingSchedules.values()) { 337 // The conflicting schedules will be removed if they end before conflicting resolved. 338 if (schedulesToStart.contains(schedule)) { 339 if (earliest > schedule.getEndTimeMs()) { 340 earliest = schedule.getEndTimeMs(); 341 } 342 } else { 343 if (earliest 344 > schedule.getStartTimeMs() 345 - RecordingTask.RECORDING_EARLY_START_OFFSET_MS) { 346 earliest = 347 schedule.getStartTimeMs() 348 - RecordingTask.RECORDING_EARLY_START_OFFSET_MS; 349 } 350 } 351 } 352 mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest - currentTimeMs); 353 } 354 355 private RecordingTask createRecordingTask(ScheduledRecording schedule) { 356 Channel channel = mChannelDataManager.getChannel(schedule.getChannelId()); 357 RecordingTask recordingTask = 358 mRecordingTaskFactory.createRecordingTask( 359 schedule, channel, mDvrManager, mSessionManager, mDataManager, mClock); 360 HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, schedule, recordingTask); 361 mPendingRecordings.put(schedule.getId(), handlerWrapper); 362 return recordingTask; 363 } 364 365 private boolean hasTaskWhichFinishEarlier(ScheduledRecording schedule) { 366 int size = mPendingRecordings.size(); 367 for (int i = 0; i < size; ++i) { 368 RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; 369 if (task.getEndTimeMs() <= schedule.getStartTimeMs()) { 370 return true; 371 } 372 } 373 return false; 374 } 375 376 private RecordingTask getReplacableTask(ScheduledRecording schedule) { 377 // Returns the recording with the following priority. 378 // 1. The recording with the lowest priority is returned. 379 // 2. If the priorities are the same, the recording which finishes early is returned. 380 // 3. If 1) and 2) are the same, the early created schedule is returned. 381 int size = mPendingRecordings.size(); 382 RecordingTask candidate = null; 383 for (int i = 0; i < size; ++i) { 384 RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; 385 if (schedule.getPriority() > task.getPriority()) { 386 if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, task) > 0) { 387 candidate = task; 388 } 389 } 390 } 391 return candidate; 392 } 393 394 private void fail(ScheduledRecording schedule, int reason) { 395 // It's called when the scheduling has been failed without creating RecordingTask. 396 runOnMainHandler( 397 new Runnable() { 398 @Override 399 public void run() { 400 ScheduledRecording scheduleInManager = 401 mDataManager.getScheduledRecording(schedule.getId()); 402 if (scheduleInManager != null) { 403 // The schedule should be updated based on the object from DataManager 404 // in case when it has been updated. 405 mDataManager.changeState( 406 scheduleInManager, 407 ScheduledRecording.STATE_RECORDING_FAILED, 408 reason); 409 } 410 } 411 }); 412 } 413 414 private void runOnMainHandler(Runnable runnable) { 415 if (Looper.myLooper() == mMainThreadHandler.getLooper()) { 416 runnable.run(); 417 } else { 418 mMainThreadHandler.post(runnable); 419 } 420 } 421 422 @VisibleForTesting 423 interface RecordingTaskFactory { 424 RecordingTask createRecordingTask( 425 ScheduledRecording scheduledRecording, 426 Channel channel, 427 DvrManager dvrManager, 428 InputSessionManager sessionManager, 429 WritableDvrDataManager dataManager, 430 Clock clock); 431 } 432 433 private class WorkerThreadHandler extends Handler { 434 public WorkerThreadHandler(Looper looper) { 435 super(looper); 436 } 437 438 @Override 439 public void handleMessage(Message msg) { 440 switch (msg.what) { 441 case MSG_ADD_SCHEDULED_RECORDING: 442 handleAddSchedule((ScheduledRecording) msg.obj); 443 break; 444 case MSG_REMOVE_SCHEDULED_RECORDING: 445 handleRemoveSchedule((ScheduledRecording) msg.obj); 446 break; 447 case MSG_UPDATE_SCHEDULED_RECORDING: 448 handleUpdateSchedule((ScheduledRecording) msg.obj); 449 break; 450 case MSG_BUILD_SCHEDULE: 451 handleBuildSchedule(); 452 break; 453 case MSG_STOP_SCHEDULE: 454 handleStopSchedule(); 455 break; 456 } 457 } 458 } 459 } 460