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