diff options
Diffstat (limited to 'src/com/android/tv/dvr/recorder/RecordingTask.java')
-rw-r--r-- | src/com/android/tv/dvr/recorder/RecordingTask.java | 530 |
1 files changed, 530 insertions, 0 deletions
diff --git a/src/com/android/tv/dvr/recorder/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java new file mode 100644 index 00000000..c3314dde --- /dev/null +++ b/src/com/android/tv/dvr/recorder/RecordingTask.java @@ -0,0 +1,530 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.recorder; + +import android.annotation.TargetApi; +import android.content.Context; +import android.media.tv.TvContract; +import android.media.tv.TvInputManager; +import android.media.tv.TvRecordingClient.RecordingCallback; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.util.Log; +import android.widget.Toast; + +import com.android.tv.InputSessionManager; +import com.android.tv.InputSessionManager.RecordingSession; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.Channel; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.WritableDvrDataManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.recorder.InputTaskScheduler.HandlerWrapper; +import com.android.tv.util.Clock; +import com.android.tv.util.Utils; + +import java.util.Comparator; +import java.util.concurrent.TimeUnit; + +/** + * A Handler that actually starts and stop a recording at the right time. + * + * <p>This is run on the looper of thread named {@value DvrRecordingService#HANDLER_THREAD_NAME}. + * There is only one looper so messages must be handled quickly or start a separate thread. + */ +@WorkerThread +@VisibleForTesting +@TargetApi(Build.VERSION_CODES.N) +public class RecordingTask extends RecordingCallback implements Handler.Callback, + DvrManager.Listener { + private static final String TAG = "RecordingTask"; + private static final boolean DEBUG = false; + + /** + * Compares the end time in ascending order. + */ + public static final Comparator<RecordingTask> END_TIME_COMPARATOR + = new Comparator<RecordingTask>() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getEndTimeMs(), rhs.getEndTimeMs()); + } + }; + + /** + * Compares ID in ascending order. + */ + public static final Comparator<RecordingTask> ID_COMPARATOR + = new Comparator<RecordingTask>() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getScheduleId(), rhs.getScheduleId()); + } + }; + + /** + * Compares the priority in ascending order. + */ + public static final Comparator<RecordingTask> PRIORITY_COMPARATOR + = new Comparator<RecordingTask>() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getPriority(), rhs.getPriority()); + } + }; + + @VisibleForTesting + static final int MSG_INITIALIZE = 1; + @VisibleForTesting + static final int MSG_START_RECORDING = 2; + @VisibleForTesting + static final int MSG_STOP_RECORDING = 3; + /** + * Message to update schedule. + */ + public static final int MSG_UDPATE_SCHEDULE = 4; + + /** + * The time when the start command will be sent before the recording starts. + */ + public static final long RECORDING_EARLY_START_OFFSET_MS = TimeUnit.SECONDS.toMillis(3); + /** + * If the recording starts later than the scheduled start time or ends before the scheduled end + * time, it's considered as clipped. + */ + private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5); + + @VisibleForTesting + enum State { + NOT_STARTED, + SESSION_ACQUIRED, + CONNECTION_PENDING, + CONNECTED, + RECORDING_STARTED, + RECORDING_STOP_REQUESTED, + FINISHED, + ERROR, + RELEASED, + } + private final InputSessionManager mSessionManager; + private final DvrManager mDvrManager; + private final Context mContext; + + private final WritableDvrDataManager mDataManager; + private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + private RecordingSession mRecordingSession; + private Handler mHandler; + private ScheduledRecording mScheduledRecording; + private final Channel mChannel; + private State mState = State.NOT_STARTED; + private final Clock mClock; + private boolean mStartedWithClipping; + private Uri mRecordedProgramUri; + private boolean mCanceled; + + RecordingTask(Context context, ScheduledRecording scheduledRecording, Channel channel, + DvrManager dvrManager, InputSessionManager sessionManager, + WritableDvrDataManager dataManager, Clock clock) { + mContext = context; + mScheduledRecording = scheduledRecording; + mChannel = channel; + mSessionManager = sessionManager; + mDataManager = dataManager; + mClock = clock; + mDvrManager = dvrManager; + + if (DEBUG) Log.d(TAG, "created recording task " + mScheduledRecording); + } + + public void setHandler(Handler handler) { + mHandler = handler; + } + + @Override + public boolean handleMessage(Message msg) { + if (DEBUG) Log.d(TAG, "handleMessage " + msg); + SoftPreconditions.checkState(msg.what == HandlerWrapper.MESSAGE_REMOVE || mHandler != null, + TAG, "Null handler trying to handle " + msg); + try { + switch (msg.what) { + case MSG_INITIALIZE: + handleInit(); + break; + case MSG_START_RECORDING: + handleStartRecording(); + break; + case MSG_STOP_RECORDING: + handleStopRecording(); + break; + case MSG_UDPATE_SCHEDULE: + handleUpdateSchedule((ScheduledRecording) msg.obj); + break; + case HandlerWrapper.MESSAGE_REMOVE: + mHandler.removeCallbacksAndMessages(null); + mHandler = null; + release(); + return false; + default: + SoftPreconditions.checkArgument(false, TAG, "unexpected message type " + msg); + break; + } + return true; + } catch (Exception e) { + Log.w(TAG, "Error processing message " + msg + " for " + mScheduledRecording, e); + failAndQuit(); + } + return false; + } + + @Override + public void onDisconnected(String inputId) { + if (DEBUG) Log.d(TAG, "onDisconnected(" + inputId + ")"); + if (mRecordingSession != null && mState != State.FINISHED) { + failAndQuit(); + } + } + + @Override + public void onConnectionFailed(String inputId) { + if (DEBUG) Log.d(TAG, "onConnectionFailed(" + inputId + ")"); + if (mRecordingSession != null) { + failAndQuit(); + } + } + + @Override + public void onTuned(Uri channelUri) { + if (DEBUG) Log.d(TAG, "onTuned"); + if (mRecordingSession == null) { + return; + } + mState = State.CONNECTED; + if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MSG_START_RECORDING, + mScheduledRecording.getStartTimeMs() - RECORDING_EARLY_START_OFFSET_MS)) { + failAndQuit(); + } + } + + @Override + public void onRecordingStopped(Uri recordedProgramUri) { + if (DEBUG) Log.d(TAG, "onRecordingStopped"); + if (mRecordingSession == null) { + return; + } + mRecordedProgramUri = recordedProgramUri; + mState = State.FINISHED; + int state = ScheduledRecording.STATE_RECORDING_FINISHED; + if (mStartedWithClipping || mScheduledRecording.getEndTimeMs() - CLIPPED_THRESHOLD_MS + > mClock.currentTimeMillis()) { + state = ScheduledRecording.STATE_RECORDING_CLIPPED; + } + updateRecordingState(state); + sendRemove(); + if (mCanceled) { + removeRecordedProgram(); + } + } + + @Override + public void onError(int reason) { + if (DEBUG) Log.d(TAG, "onError reason " + reason); + if (mRecordingSession == null) { + return; + } + switch (reason) { + case TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE: + mMainThreadHandler.post(new Runnable() { + @Override + public void run() { + if (TvApplication.getSingletons(mContext).getMainActivityWrapper() + .isResumed()) { + ScheduledRecording scheduledRecording = mDataManager + .getScheduledRecording(mScheduledRecording.getId()); + if (scheduledRecording != null) { + Toast.makeText(mContext.getApplicationContext(), + mContext.getString(R.string + .dvr_error_insufficient_space_description_one_recording, + scheduledRecording.getProgramDisplayTitle(mContext)), + Toast.LENGTH_LONG) + .show(); + } + } else { + Utils.setRecordingFailedReason(mContext.getApplicationContext(), + TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); + Utils.addFailedScheduledRecordingInfo(mContext.getApplicationContext(), + mScheduledRecording.getProgramDisplayTitle(mContext)); + } + } + }); + // Pass through + default: + failAndQuit(); + break; + } + } + + private void handleInit() { + if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording); + if (mScheduledRecording.getEndTimeMs() < mClock.currentTimeMillis()) { + Log.w(TAG, "End time already past, not recording " + mScheduledRecording); + failAndQuit(); + return; + } + if (mChannel == null) { + Log.w(TAG, "Null channel for " + mScheduledRecording); + failAndQuit(); + return; + } + if (mChannel.getId() != mScheduledRecording.getChannelId()) { + Log.w(TAG, "Channel" + mChannel + " does not match scheduled recording " + + mScheduledRecording); + failAndQuit(); + return; + } + + String inputId = mChannel.getInputId(); + mRecordingSession = mSessionManager.createRecordingSession(inputId, + "recordingTask-" + mScheduledRecording.getId(), this, + mHandler, mScheduledRecording.getEndTimeMs()); + mState = State.SESSION_ACQUIRED; + mDvrManager.addListener(this, mHandler); + mRecordingSession.tune(inputId, mChannel.getUri()); + mState = State.CONNECTION_PENDING; + } + + private void failAndQuit() { + if (DEBUG) Log.d(TAG, "failAndQuit"); + updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); + mState = State.ERROR; + sendRemove(); + } + + private void sendRemove() { + if (DEBUG) Log.d(TAG, "sendRemove"); + if (mHandler != null) { + mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage( + HandlerWrapper.MESSAGE_REMOVE)); + } + } + + private void handleStartRecording() { + if (DEBUG) Log.d(TAG, "handleStartRecording " + mScheduledRecording); + long programId = mScheduledRecording.getProgramId(); + mRecordingSession.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null + : TvContract.buildProgramUri(programId)); + updateRecordingState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS); + // If it starts late, it's clipped. + if (mScheduledRecording.getStartTimeMs() + CLIPPED_THRESHOLD_MS + < mClock.currentTimeMillis()) { + mStartedWithClipping = true; + } + mState = State.RECORDING_STARTED; + + if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, + mScheduledRecording.getEndTimeMs())) { + failAndQuit(); + } + } + + private void handleStopRecording() { + if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording); + mRecordingSession.stopRecording(); + mState = State.RECORDING_STOP_REQUESTED; + } + + private void handleUpdateSchedule(ScheduledRecording schedule) { + mScheduledRecording = schedule; + // Check end time only. The start time is checked in InputTaskScheduler. + if (schedule.getEndTimeMs() != mScheduledRecording.getEndTimeMs()) { + if (mRecordingSession != null) { + mRecordingSession.setEndTimeMs(schedule.getEndTimeMs()); + } + if (mState == State.RECORDING_STARTED) { + mHandler.removeMessages(MSG_STOP_RECORDING); + if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) { + failAndQuit(); + } + } + } + } + + @VisibleForTesting + State getState() { + return mState; + } + + private long getScheduleId() { + return mScheduledRecording.getId(); + } + + /** + * Returns the priority. + */ + public long getPriority() { + return mScheduledRecording.getPriority(); + } + + /** + * Returns the start time of the recording. + */ + public long getStartTimeMs() { + return mScheduledRecording.getStartTimeMs(); + } + + /** + * Returns the end time of the recording. + */ + public long getEndTimeMs() { + return mScheduledRecording.getEndTimeMs(); + } + + private void release() { + if (mRecordingSession != null) { + mSessionManager.releaseRecordingSession(mRecordingSession); + mRecordingSession = null; + } + mDvrManager.removeListener(this); + } + + private boolean sendEmptyMessageAtAbsoluteTime(int what, long when) { + long now = mClock.currentTimeMillis(); + long delay = Math.max(0L, when - now); + if (DEBUG) { + Log.d(TAG, "Sending message " + what + " with a delay of " + delay / 1000 + + " seconds to arrive at " + Utils.toIsoDateTimeString(when)); + } + return mHandler.sendEmptyMessageDelayed(what, delay); + } + + private void updateRecordingState(@ScheduledRecording.RecordingState int state) { + if (DEBUG) Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state); + mScheduledRecording = ScheduledRecording.buildFrom(mScheduledRecording).setState(state) + .build(); + runOnMainThread(new Runnable() { + @Override + public void run() { + ScheduledRecording schedule = mDataManager.getScheduledRecording( + mScheduledRecording.getId()); + if (schedule == null) { + // Schedule has been deleted. Delete the recorded program. + removeRecordedProgram(); + } else { + // Update the state based on the object in DataManager in case when it has been + // updated. mScheduledRecording will be updated from + // onScheduledRecordingStateChanged. + mDataManager.updateScheduledRecording(ScheduledRecording.buildFrom(schedule) + .setState(state).build()); + } + } + }); + } + + @Override + public void onStopRecordingRequested(ScheduledRecording recording) { + if (recording.getId() != mScheduledRecording.getId()) { + return; + } + stop(); + } + + /** + * Starts the task. + */ + public void start() { + mHandler.sendEmptyMessage(MSG_INITIALIZE); + } + + /** + * Stops the task. + */ + public void stop() { + if (DEBUG) Log.d(TAG, "stop"); + switch (mState) { + case RECORDING_STARTED: + mHandler.removeMessages(MSG_STOP_RECORDING); + handleStopRecording(); + break; + case RECORDING_STOP_REQUESTED: + // Do nothing + break; + case NOT_STARTED: + case SESSION_ACQUIRED: + case CONNECTION_PENDING: + case CONNECTED: + case FINISHED: + case ERROR: + case RELEASED: + default: + sendRemove(); + break; + } + } + + /** + * Cancels the task + */ + public void cancel() { + if (DEBUG) Log.d(TAG, "cancel"); + mCanceled = true; + stop(); + removeRecordedProgram(); + } + + /** + * Clean up the task. + */ + public void cleanUp() { + if (mState == State.RECORDING_STARTED || mState == State.RECORDING_STOP_REQUESTED) { + updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); + } + release(); + if (mHandler != null) { + mHandler.removeCallbacksAndMessages(null); + } + } + + @Override + public String toString() { + return getClass().getName() + "(" + mScheduledRecording + ")"; + } + + private void removeRecordedProgram() { + runOnMainThread(new Runnable() { + @Override + public void run() { + if (mRecordedProgramUri != null) { + mDvrManager.removeRecordedProgram(mRecordedProgramUri); + } + } + }); + } + + private void runOnMainThread(Runnable runnable) { + if (Looper.myLooper() == Looper.getMainLooper()) { + runnable.run(); + } else { + mMainThreadHandler.post(runnable); + } + } +} |