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 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 159 public void setHandler(Handler handler) { 160 mHandler = handler; 161 } 162 163 @Override 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 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 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 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 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 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 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 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 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 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 349 private void handleStopRecording() { 350 if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording); 351 mRecordingSession.stopRecording(); 352 mState = State.RECORDING_STOP_REQUESTED; 353 } 354 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 372 State getState() { 373 return mState; 374 } 375 376 private long getScheduleId() { 377 return mScheduledRecording.getId(); 378 } 379 380 /** 381 * Returns the priority. 382 */ 383 public long getPriority() { 384 return mScheduledRecording.getPriority(); 385 } 386 387 /** 388 * Returns the start time of the recording. 389 */ 390 public long getStartTimeMs() { 391 return mScheduledRecording.getStartTimeMs(); 392 } 393 394 /** 395 * Returns the end time of the recording. 396 */ 397 public long getEndTimeMs() { 398 return mScheduledRecording.getEndTimeMs(); 399 } 400 401 private void release() { 402 if (mRecordingSession != null) { 403 mSessionManager.releaseRecordingSession(mRecordingSession); 404 mRecordingSession = null; 405 } 406 mDvrManager.removeListener(this); 407 } 408 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 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 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 */ 453 public void start() { 454 mHandler.sendEmptyMessage(MSG_INITIALIZE); 455 } 456 457 /** 458 * Stops the task. 459 */ 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 */ 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 */ 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 507 public String toString() { 508 return getClass().getName() + "(" + mScheduledRecording + ")"; 509 } 510 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 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