1 /* 2 * Copyright (C) 2015 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.annotation.TargetApi; 20 import android.content.Context; 21 import android.media.tv.TvContract; 22 import android.media.tv.TvInputManager; 23 import android.media.tv.TvRecordingClient.RecordingCallback; 24 import android.net.Uri; 25 import android.os.Build; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.os.Message; 29 import android.support.annotation.VisibleForTesting; 30 import android.support.annotation.WorkerThread; 31 import android.util.Log; 32 import android.widget.Toast; 33 34 import com.android.tv.InputSessionManager; 35 import com.android.tv.InputSessionManager.RecordingSession; 36 import com.android.tv.R; 37 import com.android.tv.TvApplication; 38 import com.android.tv.common.SoftPreconditions; 39 import com.android.tv.data.Channel; 40 import com.android.tv.dvr.DvrManager; 41 import com.android.tv.dvr.WritableDvrDataManager; 42 import com.android.tv.dvr.data.ScheduledRecording; 43 import com.android.tv.dvr.recorder.InputTaskScheduler.HandlerWrapper; 44 import com.android.tv.util.Clock; 45 import com.android.tv.util.Utils; 46 47 import java.util.Comparator; 48 import java.util.concurrent.TimeUnit; 49 50 /** 51 * A Handler that actually starts and stop a recording at the right time. 52 * 53 * <p>This is run on the looper of thread named {@value DvrRecordingService#HANDLER_THREAD_NAME}. 54 * There is only one looper so messages must be handled quickly or start a separate thread. 55 */ 56 @WorkerThread 57 @TargetApi(Build.VERSION_CODES.N) 58 public class RecordingTask extends RecordingCallback implements Handler.Callback, 59 DvrManager.Listener { 60 private static final String TAG = "RecordingTask"; 61 private static final boolean DEBUG = false; 62 63 /** 64 * Compares the end time in ascending order. 65 */ 66 public static final Comparator<RecordingTask> END_TIME_COMPARATOR 67 = new Comparator<RecordingTask>() { 68 @Override 69 public int compare(RecordingTask lhs, RecordingTask rhs) { 70 return Long.compare(lhs.getEndTimeMs(), rhs.getEndTimeMs()); 71 } 72 }; 73 74 /** 75 * Compares ID in ascending order. 76 */ 77 public static final Comparator<RecordingTask> ID_COMPARATOR 78 = new Comparator<RecordingTask>() { 79 @Override 80 public int compare(RecordingTask lhs, RecordingTask rhs) { 81 return Long.compare(lhs.getScheduleId(), rhs.getScheduleId()); 82 } 83 }; 84 85 /** 86 * Compares the priority in ascending order. 87 */ 88 public static final Comparator<RecordingTask> PRIORITY_COMPARATOR 89 = new Comparator<RecordingTask>() { 90 @Override 91 public int compare(RecordingTask lhs, RecordingTask rhs) { 92 return Long.compare(lhs.getPriority(), rhs.getPriority()); 93 } 94 }; 95 96 @VisibleForTesting 97 static final int MSG_INITIALIZE = 1; 98 @VisibleForTesting 99 static final int MSG_START_RECORDING = 2; 100 @VisibleForTesting 101 static final int MSG_STOP_RECORDING = 3; 102 /** 103 * Message to update schedule. 104 */ 105 public static final int MSG_UDPATE_SCHEDULE = 4; 106 107 /** 108 * The time when the start command will be sent before the recording starts. 109 */ 110 public static final long RECORDING_EARLY_START_OFFSET_MS = TimeUnit.SECONDS.toMillis(3); 111 /** 112 * If the recording starts later than the scheduled start time or ends before the scheduled end 113 * time, it's considered as clipped. 114 */ 115 private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5); 116 117 @VisibleForTesting 118 enum State { 119 NOT_STARTED, 120 SESSION_ACQUIRED, 121 CONNECTION_PENDING, 122 CONNECTED, 123 RECORDING_STARTED, 124 RECORDING_STOP_REQUESTED, 125 FINISHED, 126 ERROR, 127 RELEASED, 128 } 129 private final InputSessionManager mSessionManager; 130 private final DvrManager mDvrManager; 131 private final Context mContext; 132 133 private final WritableDvrDataManager mDataManager; 134 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 135 private RecordingSession mRecordingSession; 136 private Handler mHandler; 137 private ScheduledRecording mScheduledRecording; 138 private final Channel mChannel; 139 private State mState = State.NOT_STARTED; 140 private final Clock mClock; 141 private boolean mStartedWithClipping; 142 private Uri mRecordedProgramUri; 143 private boolean mCanceled; 144 RecordingTask(Context context, ScheduledRecording scheduledRecording, Channel channel, DvrManager dvrManager, InputSessionManager sessionManager, WritableDvrDataManager dataManager, Clock clock)145 RecordingTask(Context context, ScheduledRecording scheduledRecording, Channel channel, 146 DvrManager dvrManager, InputSessionManager sessionManager, 147 WritableDvrDataManager dataManager, Clock clock) { 148 mContext = context; 149 mScheduledRecording = scheduledRecording; 150 mChannel = channel; 151 mSessionManager = sessionManager; 152 mDataManager = dataManager; 153 mClock = clock; 154 mDvrManager = dvrManager; 155 156 if (DEBUG) Log.d(TAG, "created recording task " + mScheduledRecording); 157 } 158 setHandler(Handler handler)159 public void setHandler(Handler handler) { 160 mHandler = handler; 161 } 162 163 @Override handleMessage(Message msg)164 public boolean handleMessage(Message msg) { 165 if (DEBUG) Log.d(TAG, "handleMessage " + msg); 166 SoftPreconditions.checkState(msg.what == HandlerWrapper.MESSAGE_REMOVE || mHandler != null, 167 TAG, "Null handler trying to handle " + msg); 168 try { 169 switch (msg.what) { 170 case MSG_INITIALIZE: 171 handleInit(); 172 break; 173 case MSG_START_RECORDING: 174 handleStartRecording(); 175 break; 176 case MSG_STOP_RECORDING: 177 handleStopRecording(); 178 break; 179 case MSG_UDPATE_SCHEDULE: 180 handleUpdateSchedule((ScheduledRecording) msg.obj); 181 break; 182 case HandlerWrapper.MESSAGE_REMOVE: 183 mHandler.removeCallbacksAndMessages(null); 184 mHandler = null; 185 release(); 186 return false; 187 default: 188 SoftPreconditions.checkArgument(false, TAG, "unexpected message type " + msg); 189 break; 190 } 191 return true; 192 } catch (Exception e) { 193 Log.w(TAG, "Error processing message " + msg + " for " + mScheduledRecording, e); 194 failAndQuit(); 195 } 196 return false; 197 } 198 199 @Override onDisconnected(String inputId)200 public void onDisconnected(String inputId) { 201 if (DEBUG) Log.d(TAG, "onDisconnected(" + inputId + ")"); 202 if (mRecordingSession != null && mState != State.FINISHED) { 203 failAndQuit(); 204 } 205 } 206 207 @Override onConnectionFailed(String inputId)208 public void onConnectionFailed(String inputId) { 209 if (DEBUG) Log.d(TAG, "onConnectionFailed(" + inputId + ")"); 210 if (mRecordingSession != null) { 211 failAndQuit(); 212 } 213 } 214 215 @Override onTuned(Uri channelUri)216 public void onTuned(Uri channelUri) { 217 if (DEBUG) Log.d(TAG, "onTuned"); 218 if (mRecordingSession == null) { 219 return; 220 } 221 mState = State.CONNECTED; 222 if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MSG_START_RECORDING, 223 mScheduledRecording.getStartTimeMs() - RECORDING_EARLY_START_OFFSET_MS)) { 224 failAndQuit(); 225 } 226 } 227 228 @Override onRecordingStopped(Uri recordedProgramUri)229 public void onRecordingStopped(Uri recordedProgramUri) { 230 if (DEBUG) Log.d(TAG, "onRecordingStopped"); 231 if (mRecordingSession == null) { 232 return; 233 } 234 mRecordedProgramUri = recordedProgramUri; 235 mState = State.FINISHED; 236 int state = ScheduledRecording.STATE_RECORDING_FINISHED; 237 if (mStartedWithClipping || mScheduledRecording.getEndTimeMs() - CLIPPED_THRESHOLD_MS 238 > mClock.currentTimeMillis()) { 239 state = ScheduledRecording.STATE_RECORDING_CLIPPED; 240 } 241 updateRecordingState(state); 242 sendRemove(); 243 if (mCanceled) { 244 removeRecordedProgram(); 245 } 246 } 247 248 @Override onError(int reason)249 public void onError(int reason) { 250 if (DEBUG) Log.d(TAG, "onError reason " + reason); 251 if (mRecordingSession == null) { 252 return; 253 } 254 switch (reason) { 255 case TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE: 256 mMainThreadHandler.post(new Runnable() { 257 @Override 258 public void run() { 259 if (TvApplication.getSingletons(mContext).getMainActivityWrapper() 260 .isResumed()) { 261 ScheduledRecording scheduledRecording = mDataManager 262 .getScheduledRecording(mScheduledRecording.getId()); 263 if (scheduledRecording != null) { 264 Toast.makeText(mContext.getApplicationContext(), 265 mContext.getString(R.string 266 .dvr_error_insufficient_space_description_one_recording, 267 scheduledRecording.getProgramDisplayTitle(mContext)), 268 Toast.LENGTH_LONG) 269 .show(); 270 } 271 } else { 272 Utils.setRecordingFailedReason(mContext.getApplicationContext(), 273 TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); 274 Utils.addFailedScheduledRecordingInfo(mContext.getApplicationContext(), 275 mScheduledRecording.getProgramDisplayTitle(mContext)); 276 } 277 } 278 }); 279 // Pass through 280 default: 281 failAndQuit(); 282 break; 283 } 284 } 285 handleInit()286 private void handleInit() { 287 if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording); 288 if (mScheduledRecording.getEndTimeMs() < mClock.currentTimeMillis()) { 289 Log.w(TAG, "End time already past, not recording " + mScheduledRecording); 290 failAndQuit(); 291 return; 292 } 293 if (mChannel == null) { 294 Log.w(TAG, "Null channel for " + mScheduledRecording); 295 failAndQuit(); 296 return; 297 } 298 if (mChannel.getId() != mScheduledRecording.getChannelId()) { 299 Log.w(TAG, "Channel" + mChannel + " does not match scheduled recording " 300 + mScheduledRecording); 301 failAndQuit(); 302 return; 303 } 304 305 String inputId = mChannel.getInputId(); 306 mRecordingSession = mSessionManager.createRecordingSession(inputId, 307 "recordingTask-" + mScheduledRecording.getId(), this, 308 mHandler, mScheduledRecording.getEndTimeMs()); 309 mState = State.SESSION_ACQUIRED; 310 mDvrManager.addListener(this, mHandler); 311 mRecordingSession.tune(inputId, mChannel.getUri()); 312 mState = State.CONNECTION_PENDING; 313 } 314 failAndQuit()315 private void failAndQuit() { 316 if (DEBUG) Log.d(TAG, "failAndQuit"); 317 updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); 318 mState = State.ERROR; 319 sendRemove(); 320 } 321 sendRemove()322 private void sendRemove() { 323 if (DEBUG) Log.d(TAG, "sendRemove"); 324 if (mHandler != null) { 325 mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage( 326 HandlerWrapper.MESSAGE_REMOVE)); 327 } 328 } 329 handleStartRecording()330 private void handleStartRecording() { 331 if (DEBUG) Log.d(TAG, "handleStartRecording " + mScheduledRecording); 332 long programId = mScheduledRecording.getProgramId(); 333 mRecordingSession.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null 334 : TvContract.buildProgramUri(programId)); 335 updateRecordingState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS); 336 // If it starts late, it's clipped. 337 if (mScheduledRecording.getStartTimeMs() + CLIPPED_THRESHOLD_MS 338 < mClock.currentTimeMillis()) { 339 mStartedWithClipping = true; 340 } 341 mState = State.RECORDING_STARTED; 342 343 if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, 344 mScheduledRecording.getEndTimeMs())) { 345 failAndQuit(); 346 } 347 } 348 handleStopRecording()349 private void handleStopRecording() { 350 if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording); 351 mRecordingSession.stopRecording(); 352 mState = State.RECORDING_STOP_REQUESTED; 353 } 354 handleUpdateSchedule(ScheduledRecording schedule)355 private void handleUpdateSchedule(ScheduledRecording schedule) { 356 mScheduledRecording = schedule; 357 // Check end time only. The start time is checked in InputTaskScheduler. 358 if (schedule.getEndTimeMs() != mScheduledRecording.getEndTimeMs()) { 359 if (mRecordingSession != null) { 360 mRecordingSession.setEndTimeMs(schedule.getEndTimeMs()); 361 } 362 if (mState == State.RECORDING_STARTED) { 363 mHandler.removeMessages(MSG_STOP_RECORDING); 364 if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) { 365 failAndQuit(); 366 } 367 } 368 } 369 } 370 371 @VisibleForTesting getState()372 State getState() { 373 return mState; 374 } 375 getScheduleId()376 private long getScheduleId() { 377 return mScheduledRecording.getId(); 378 } 379 380 /** 381 * Returns the priority. 382 */ getPriority()383 public long getPriority() { 384 return mScheduledRecording.getPriority(); 385 } 386 387 /** 388 * Returns the start time of the recording. 389 */ getStartTimeMs()390 public long getStartTimeMs() { 391 return mScheduledRecording.getStartTimeMs(); 392 } 393 394 /** 395 * Returns the end time of the recording. 396 */ getEndTimeMs()397 public long getEndTimeMs() { 398 return mScheduledRecording.getEndTimeMs(); 399 } 400 release()401 private void release() { 402 if (mRecordingSession != null) { 403 mSessionManager.releaseRecordingSession(mRecordingSession); 404 mRecordingSession = null; 405 } 406 mDvrManager.removeListener(this); 407 } 408 sendEmptyMessageAtAbsoluteTime(int what, long when)409 private boolean sendEmptyMessageAtAbsoluteTime(int what, long when) { 410 long now = mClock.currentTimeMillis(); 411 long delay = Math.max(0L, when - now); 412 if (DEBUG) { 413 Log.d(TAG, "Sending message " + what + " with a delay of " + delay / 1000 414 + " seconds to arrive at " + Utils.toIsoDateTimeString(when)); 415 } 416 return mHandler.sendEmptyMessageDelayed(what, delay); 417 } 418 updateRecordingState(@cheduledRecording.RecordingState int state)419 private void updateRecordingState(@ScheduledRecording.RecordingState int state) { 420 if (DEBUG) Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state); 421 mScheduledRecording = ScheduledRecording.buildFrom(mScheduledRecording).setState(state) 422 .build(); 423 runOnMainThread(new Runnable() { 424 @Override 425 public void run() { 426 ScheduledRecording schedule = mDataManager.getScheduledRecording( 427 mScheduledRecording.getId()); 428 if (schedule == null) { 429 // Schedule has been deleted. Delete the recorded program. 430 removeRecordedProgram(); 431 } else { 432 // Update the state based on the object in DataManager in case when it has been 433 // updated. mScheduledRecording will be updated from 434 // onScheduledRecordingStateChanged. 435 mDataManager.updateScheduledRecording(ScheduledRecording.buildFrom(schedule) 436 .setState(state).build()); 437 } 438 } 439 }); 440 } 441 442 @Override onStopRecordingRequested(ScheduledRecording recording)443 public void onStopRecordingRequested(ScheduledRecording recording) { 444 if (recording.getId() != mScheduledRecording.getId()) { 445 return; 446 } 447 stop(); 448 } 449 450 /** 451 * Starts the task. 452 */ start()453 public void start() { 454 mHandler.sendEmptyMessage(MSG_INITIALIZE); 455 } 456 457 /** 458 * Stops the task. 459 */ stop()460 public void stop() { 461 if (DEBUG) Log.d(TAG, "stop"); 462 switch (mState) { 463 case RECORDING_STARTED: 464 mHandler.removeMessages(MSG_STOP_RECORDING); 465 handleStopRecording(); 466 break; 467 case RECORDING_STOP_REQUESTED: 468 // Do nothing 469 break; 470 case NOT_STARTED: 471 case SESSION_ACQUIRED: 472 case CONNECTION_PENDING: 473 case CONNECTED: 474 case FINISHED: 475 case ERROR: 476 case RELEASED: 477 default: 478 sendRemove(); 479 break; 480 } 481 } 482 483 /** 484 * Cancels the task 485 */ cancel()486 public void cancel() { 487 if (DEBUG) Log.d(TAG, "cancel"); 488 mCanceled = true; 489 stop(); 490 removeRecordedProgram(); 491 } 492 493 /** 494 * Clean up the task. 495 */ cleanUp()496 public void cleanUp() { 497 if (mState == State.RECORDING_STARTED || mState == State.RECORDING_STOP_REQUESTED) { 498 updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); 499 } 500 release(); 501 if (mHandler != null) { 502 mHandler.removeCallbacksAndMessages(null); 503 } 504 } 505 506 @Override toString()507 public String toString() { 508 return getClass().getName() + "(" + mScheduledRecording + ")"; 509 } 510 removeRecordedProgram()511 private void removeRecordedProgram() { 512 runOnMainThread(new Runnable() { 513 @Override 514 public void run() { 515 if (mRecordedProgramUri != null) { 516 mDvrManager.removeRecordedProgram(mRecordedProgramUri); 517 } 518 } 519 }); 520 } 521 runOnMainThread(Runnable runnable)522 private void runOnMainThread(Runnable runnable) { 523 if (Looper.myLooper() == Looper.getMainLooper()) { 524 runnable.run(); 525 } else { 526 mMainThreadHandler.post(runnable); 527 } 528 } 529 } 530