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