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