/* * 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. * *

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 + ")"; } }