diff options
author | Nick Chalko <nchalko@google.com> | 2016-08-31 16:00:31 -0700 |
---|---|---|
committer | Nick Chalko <nchalko@google.com> | 2016-09-07 05:38:33 -0700 |
commit | 65fda1eaa94968bb55d5ded10dcb0b3f37fb05f2 (patch) | |
tree | ffc8e4c5a71c130d3782bf03e674f9d77ca77f72 /src/com/android/tv/dvr/RecordingTask.java | |
parent | ad819718f80e796cf039f96537b5c8cd127c042b (diff) | |
download | TV-65fda1eaa94968bb55d5ded10dcb0b3f37fb05f2.tar.gz |
Sync to ub-tv-dev at http://ag/1415258
Bug: 30970843
Change-Id: I0aa43094d103de28956a3d9b56a594ea46a20543
Diffstat (limited to 'src/com/android/tv/dvr/RecordingTask.java')
-rw-r--r-- | src/com/android/tv/dvr/RecordingTask.java | 302 |
1 files changed, 218 insertions, 84 deletions
diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/RecordingTask.java index 804485b3..2373f15c 100644 --- a/src/com/android/tv/dvr/RecordingTask.java +++ b/src/com/android/tv/dvr/RecordingTask.java @@ -16,18 +16,28 @@ package com.android.tv.dvr; +import android.annotation.TargetApi; +import android.content.Context; import android.media.tv.TvContract; -import android.media.tv.TvRecordingClient; +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.InputTaskScheduler.HandlerWrapper; import com.android.tv.util.Clock; import com.android.tv.util.Utils; @@ -40,22 +50,33 @@ import java.util.concurrent.TimeUnit; * There is only one looper so messages must be handled quickly or start a separate thread. */ @WorkerThread -class RecordingTask extends TvRecordingClient.RecordingCallback - implements Handler.Callback, DvrManager.Listener { +@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; @VisibleForTesting - static final int MESSAGE_INIT = 1; + static final int MSG_INITIALIZE = 1; @VisibleForTesting - static final int MESSAGE_START_RECORDING = 2; + static final int MSG_START_RECORDING = 2; @VisibleForTesting - static final int MESSAGE_STOP_RECORDING = 3; - - @VisibleForTesting - static final long MS_BEFORE_START = TimeUnit.SECONDS.toMillis(5); - @VisibleForTesting - static final long MS_AFTER_END = TimeUnit.SECONDS.toMillis(5); + 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 { @@ -63,27 +84,32 @@ class RecordingTask extends TvRecordingClient.RecordingCallback SESSION_ACQUIRED, CONNECTION_PENDING, CONNECTED, - RECORDING_START_REQUESTED, RECORDING_STARTED, RECORDING_STOP_REQUESTED, + FINISHED, ERROR, RELEASED, } - private final DvrSessionManager mSessionManager; + 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 TvRecordingClient mTvRecordingClient; + 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(ScheduledRecording scheduledRecording, Channel channel, - DvrManager dvrManager, DvrSessionManager sessionManager, + RecordingTask(Context context, ScheduledRecording scheduledRecording, Channel channel, + DvrManager dvrManager, InputSessionManager sessionManager, WritableDvrDataManager dataManager, Clock clock) { + mContext = context; mScheduledRecording = scheduledRecording; mChannel = channel; mSessionManager = sessionManager; @@ -101,27 +127,30 @@ class RecordingTask extends TvRecordingClient.RecordingCallback @Override public boolean handleMessage(Message msg) { if (DEBUG) Log.d(TAG, "handleMessage " + msg); - SoftPreconditions - .checkState(msg.what == Scheduler.HandlerWrapper.MESSAGE_REMOVE || mHandler != null, - TAG, "Null handler trying to handle " + msg); + SoftPreconditions.checkState(msg.what == HandlerWrapper.MESSAGE_REMOVE || mHandler != null, + TAG, "Null handler trying to handle " + msg); try { switch (msg.what) { - case MESSAGE_INIT: + case MSG_INITIALIZE: handleInit(); break; - case MESSAGE_START_RECORDING: + case MSG_START_RECORDING: handleStartRecording(); break; - case MESSAGE_STOP_RECORDING: + case MSG_STOP_RECORDING: handleStopRecording(); break; - case Scheduler.HandlerWrapper.MESSAGE_REMOVE: - // Clear the handler + 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) { @@ -132,54 +161,83 @@ class RecordingTask extends TvRecordingClient.RecordingCallback } @Override + public void onDisconnected(String inputId) { + if (DEBUG) Log.d(TAG, "onDisconnected(" + inputId + ")"); + if (mRecordingSession != null && mState != State.FINISHED) { + failAndQuit(); + } + } + + @Override public void onTuned(Uri channelUri) { - if (DEBUG) { - Log.d(TAG, "onTuned"); + if (DEBUG) Log.d(TAG, "onTuned"); + if (mRecordingSession == null) { + return; } - super.onTuned(channelUri); mState = State.CONNECTED; - if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_START_RECORDING, - mScheduledRecording.getStartTimeMs() - MS_BEFORE_START)) { - mState = State.ERROR; - return; + if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MSG_START_RECORDING, + mScheduledRecording.getStartTimeMs() - RECORDING_EARLY_START_OFFSET_MS)) { + failAndQuit(); } } - @Override public void onRecordingStopped(Uri recordedProgramUri) { - super.onRecordingStopped(recordedProgramUri); - mState = State.CONNECTED; - updateRecording(ScheduledRecording.buildFrom(mScheduledRecording) - .setState(ScheduledRecording.STATE_RECORDING_FINISHED).build()); + 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); - super.onError(reason); - // TODO(dvr) handle success + 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()) { + Toast.makeText(mContext.getApplicationContext(), + R.string.dvr_error_insufficient_space_description, + Toast.LENGTH_LONG) + .show(); + } else { + Utils.setRecordingFailedReason(mContext.getApplicationContext(), + TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); + } + } + }); + // Pass through default: - updateRecording(ScheduledRecording.buildFrom(mScheduledRecording) - .setState(ScheduledRecording.STATE_RECORDING_FAILED) - .build()); + failAndQuit(); + break; } - release(); - sendRemove(); } private void handleInit() { if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording); - //TODO check recording preconditions - 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(); @@ -193,18 +251,11 @@ class RecordingTask extends TvRecordingClient.RecordingCallback } String inputId = mChannel.getInputId(); - if (mSessionManager.canAcquireDvrSession(inputId, mChannel)) { - mTvRecordingClient = mSessionManager - .createTvRecordingClient("recordingTask-" + mScheduledRecording.getId(), this, - mHandler); - mState = State.SESSION_ACQUIRED; - } else { - Log.w(TAG, "Unable to acquire a session for " + mScheduledRecording); - failAndQuit(); - return; - } + mRecordingSession = mSessionManager.createRecordingSession(inputId, + "recordingTask-" + mScheduledRecording.getId(), this, mHandler); + mState = State.SESSION_ACQUIRED; mDvrManager.addListener(this, mHandler); - mTvRecordingClient.tune(inputId, mChannel.getUri()); + mRecordingSession.tune(inputId, mChannel.getUri()); mState = State.CONNECTION_PENDING; } @@ -218,41 +269,78 @@ class RecordingTask extends TvRecordingClient.RecordingCallback private void sendRemove() { if (DEBUG) Log.d(TAG, "sendRemove"); if (mHandler != null) { - mHandler.sendEmptyMessage(Scheduler.HandlerWrapper.MESSAGE_REMOVE); + mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage( + HandlerWrapper.MESSAGE_REMOVE)); } } private void handleStartRecording() { if (DEBUG) Log.d(TAG, "handleStartRecording " + mScheduledRecording); - // TODO(DVR) handle errors long programId = mScheduledRecording.getProgramId(); - mTvRecordingClient.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null + mRecordingSession.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null : TvContract.buildProgramUri(programId)); - updateRecording(ScheduledRecording.buildFrom(mScheduledRecording) - .setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS).build()); + 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 (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_STOP_RECORDING, - mScheduledRecording.getEndTimeMs() + MS_AFTER_END)) { - mState = State.ERROR; - return; + if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, + mScheduledRecording.getEndTimeMs())) { + failAndQuit(); } } private void handleStopRecording() { if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording); - mTvRecordingClient.stopRecording(); + 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() + && mState == State.RECORDING_STARTED) { + mHandler.removeMessages(MSG_STOP_RECORDING); + if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) { + failAndQuit(); + } + } + } + @VisibleForTesting State getState() { return mState; } + /** + * 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 (mTvRecordingClient != null) { - mSessionManager.releaseTvRecordingClient(mTvRecordingClient); + if (mRecordingSession != null) { + mSessionManager.releaseRecordingSession(mRecordingSession); + mRecordingSession = null; } mDvrManager.removeListener(this); } @@ -268,22 +356,24 @@ class RecordingTask extends TvRecordingClient.RecordingCallback } private void updateRecordingState(@ScheduledRecording.RecordingState int state) { - updateRecording(ScheduledRecording.buildFrom(mScheduledRecording).setState(state).build()); - } - - @VisibleForTesting - static Uri getIdAsMediaUri(ScheduledRecording scheduledRecording) { - // TODO define the URI format - return new Uri.Builder().appendPath(String.valueOf(scheduledRecording.getId())).build(); - } - - private void updateRecording(ScheduledRecording updatedScheduledRecording) { - if (DEBUG) Log.d(TAG, "updateScheduledRecording " + updatedScheduledRecording); - mScheduledRecording = updatedScheduledRecording; + if (DEBUG) Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state); + mScheduledRecording = ScheduledRecording.buildFrom(mScheduledRecording).setState(state) + .build(); mMainThreadHandler.post(new Runnable() { @Override public void run() { - mDataManager.updateScheduledRecording(mScheduledRecording); + 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()); + } } }); } @@ -293,9 +383,24 @@ class RecordingTask extends TvRecordingClient.RecordingCallback 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(MESSAGE_STOP_RECORDING); + mHandler.removeMessages(MSG_STOP_RECORDING); handleStopRecording(); break; case RECORDING_STOP_REQUESTED: @@ -305,7 +410,7 @@ class RecordingTask extends TvRecordingClient.RecordingCallback case SESSION_ACQUIRED: case CONNECTION_PENDING: case CONNECTED: - case RECORDING_START_REQUESTED: + case FINISHED: case ERROR: case RELEASED: default: @@ -314,8 +419,37 @@ class RecordingTask extends TvRecordingClient.RecordingCallback } } + /** + * Cancels the task + */ + public void cancel() { + if (DEBUG) Log.d(TAG, "cancel"); + mCanceled = true; + stop(); + removeRecordedProgram(); + } + @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); + } + } } |