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 17package com.android.tv.dvr.recorder; 18 19import android.content.Context; 20import android.media.tv.TvInputInfo; 21import android.os.Handler; 22import android.os.Looper; 23import android.os.Message; 24import android.support.annotation.VisibleForTesting; 25import android.util.ArrayMap; 26import android.util.Log; 27import android.util.LongSparseArray; 28 29import com.android.tv.InputSessionManager; 30import com.android.tv.data.Channel; 31import com.android.tv.data.ChannelDataManager; 32import com.android.tv.dvr.DvrDataManager; 33import com.android.tv.dvr.DvrManager; 34import com.android.tv.dvr.WritableDvrDataManager; 35import com.android.tv.dvr.data.ScheduledRecording; 36import com.android.tv.util.Clock; 37import com.android.tv.util.CompositeComparator; 38 39import java.util.ArrayList; 40import java.util.Collections; 41import java.util.Comparator; 42import java.util.Iterator; 43import java.util.List; 44import java.util.Map; 45 46/** 47 * The scheduler for a TV input. 48 */ 49public 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 */ 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 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 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 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 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 */ 161 public void addSchedule(ScheduledRecording schedule) { 162 mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_SCHEDULED_RECORDING, schedule)); 163 } 164 165 @VisibleForTesting 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 */ 179 public void removeSchedule(ScheduledRecording schedule) { 180 mHandler.sendMessage(mHandler.obtainMessage(MSG_REMOVE_SCHEDULED_RECORDING, schedule)); 181 } 182 183 @VisibleForTesting 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 */ 200 public void updateSchedule(ScheduledRecording schedule) { 201 mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SCHEDULED_RECORDING, schedule)); 202 } 203 204 @VisibleForTesting 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 */ 230 public void updateTvInputInfo(TvInputInfo input) { 231 synchronized (mInputLock) { 232 mInput = input; 233 } 234 } 235 236 /** 237 * Stops the input task scheduler. 238 */ 239 public void stop() { 240 mHandler.removeCallbacksAndMessages(null); 241 mHandler.sendEmptyMessage(MSG_STOP_SCHEDULE); 242 } 243 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 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 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 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 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 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 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 { 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 { 405 public WorkerThreadHandler(Looper looper) { 406 super(looper); 407 } 408 409 @Override 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