blob: 804485b3f99cf68b3f900b158731b0c665a32302 [file] [log] [blame]
/*
* 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;
import android.media.tv.TvContract;
import android.media.tv.TvRecordingClient;
import android.net.Uri;
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 com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.util.Clock;
import com.android.tv.util.Utils;
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
class RecordingTask extends TvRecordingClient.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;
@VisibleForTesting
static final int MESSAGE_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);
@VisibleForTesting
enum State {
NOT_STARTED,
SESSION_ACQUIRED,
CONNECTION_PENDING,
CONNECTED,
RECORDING_START_REQUESTED,
RECORDING_STARTED,
RECORDING_STOP_REQUESTED,
ERROR,
RELEASED,
}
private final DvrSessionManager mSessionManager;
private final DvrManager mDvrManager;
private final WritableDvrDataManager mDataManager;
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
private TvRecordingClient mTvRecordingClient;
private Handler mHandler;
private ScheduledRecording mScheduledRecording;
private final Channel mChannel;
private State mState = State.NOT_STARTED;
private final Clock mClock;
RecordingTask(ScheduledRecording scheduledRecording, Channel channel,
DvrManager dvrManager, DvrSessionManager sessionManager,
WritableDvrDataManager dataManager, Clock clock) {
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 == Scheduler.HandlerWrapper.MESSAGE_REMOVE || mHandler != null,
TAG, "Null handler trying to handle " + msg);
try {
switch (msg.what) {
case MESSAGE_INIT:
handleInit();
break;
case MESSAGE_START_RECORDING:
handleStartRecording();
break;
case MESSAGE_STOP_RECORDING:
handleStopRecording();
break;
case Scheduler.HandlerWrapper.MESSAGE_REMOVE:
// Clear the handler
mHandler = null;
release();
return false;
default:
SoftPreconditions.checkArgument(false, TAG, "unexpected message type " + msg);
}
return true;
} catch (Exception e) {
Log.w(TAG, "Error processing message " + msg + " for " + mScheduledRecording, e);
failAndQuit();
}
return false;
}
@Override
public void onTuned(Uri channelUri) {
if (DEBUG) {
Log.d(TAG, "onTuned");
}
super.onTuned(channelUri);
mState = State.CONNECTED;
if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_START_RECORDING,
mScheduledRecording.getStartTimeMs() - MS_BEFORE_START)) {
mState = State.ERROR;
return;
}
}
@Override
public void onRecordingStopped(Uri recordedProgramUri) {
super.onRecordingStopped(recordedProgramUri);
mState = State.CONNECTED;
updateRecording(ScheduledRecording.buildFrom(mScheduledRecording)
.setState(ScheduledRecording.STATE_RECORDING_FINISHED).build());
sendRemove();
}
@Override
public void onError(int reason) {
if (DEBUG) Log.d(TAG, "onError reason " + reason);
super.onError(reason);
// TODO(dvr) handle success
switch (reason) {
default:
updateRecording(ScheduledRecording.buildFrom(mScheduledRecording)
.setState(ScheduledRecording.STATE_RECORDING_FAILED)
.build());
}
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();
return;
}
if (mChannel.getId() != mScheduledRecording.getChannelId()) {
Log.w(TAG, "Channel" + mChannel + " does not match scheduled recording "
+ mScheduledRecording);
failAndQuit();
return;
}
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;
}
mDvrManager.addListener(this, mHandler);
mTvRecordingClient.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.sendEmptyMessage(Scheduler.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
: TvContract.buildProgramUri(programId));
updateRecording(ScheduledRecording.buildFrom(mScheduledRecording)
.setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS).build());
mState = State.RECORDING_STARTED;
if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_STOP_RECORDING,
mScheduledRecording.getEndTimeMs() + MS_AFTER_END)) {
mState = State.ERROR;
return;
}
}
private void handleStopRecording() {
if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording);
mTvRecordingClient.stopRecording();
mState = State.RECORDING_STOP_REQUESTED;
}
@VisibleForTesting
State getState() {
return mState;
}
private void release() {
if (mTvRecordingClient != null) {
mSessionManager.releaseTvRecordingClient(mTvRecordingClient);
}
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) {
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;
mMainThreadHandler.post(new Runnable() {
@Override
public void run() {
mDataManager.updateScheduledRecording(mScheduledRecording);
}
});
}
@Override
public void onStopRecordingRequested(ScheduledRecording recording) {
if (recording.getId() != mScheduledRecording.getId()) {
return;
}
switch (mState) {
case RECORDING_STARTED:
mHandler.removeMessages(MESSAGE_STOP_RECORDING);
handleStopRecording();
break;
case RECORDING_STOP_REQUESTED:
// Do nothing
break;
case NOT_STARTED:
case SESSION_ACQUIRED:
case CONNECTION_PENDING:
case CONNECTED:
case RECORDING_START_REQUESTED:
case ERROR:
case RELEASED:
default:
sendRemove();
break;
}
}
@Override
public String toString() {
return getClass().getName() + "(" + mScheduledRecording + ")";
}
}