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
17package com.android.tv.dvr;
18
19import android.media.tv.TvContract;
20import android.media.tv.TvRecordingClient;
21import android.net.Uri;
22import android.os.Handler;
23import android.os.Looper;
24import android.os.Message;
25import android.support.annotation.VisibleForTesting;
26import android.support.annotation.WorkerThread;
27import android.util.Log;
28
29import com.android.tv.common.SoftPreconditions;
30import com.android.tv.data.Channel;
31import com.android.tv.util.Clock;
32import com.android.tv.util.Utils;
33
34import java.util.concurrent.TimeUnit;
35
36/**
37 * A Handler that actually starts and stop a recording at the right time.
38 *
39 * <p>This is run on the looper of thread named {@value DvrRecordingService#HANDLER_THREAD_NAME}.
40 * There is only one looper so messages must be handled quickly or start a separate thread.
41 */
42@WorkerThread
43class RecordingTask extends TvRecordingClient.RecordingCallback
44        implements Handler.Callback, DvrManager.Listener {
45    private static final String TAG = "RecordingTask";
46    private static final boolean DEBUG = false;
47
48    @VisibleForTesting
49    static final int MESSAGE_INIT = 1;
50    @VisibleForTesting
51    static final int MESSAGE_START_RECORDING = 2;
52    @VisibleForTesting
53    static final int MESSAGE_STOP_RECORDING = 3;
54
55    @VisibleForTesting
56    static final long MS_BEFORE_START = TimeUnit.SECONDS.toMillis(5);
57    @VisibleForTesting
58    static final long MS_AFTER_END = TimeUnit.SECONDS.toMillis(5);
59
60    @VisibleForTesting
61    enum State {
62        NOT_STARTED,
63        SESSION_ACQUIRED,
64        CONNECTION_PENDING,
65        CONNECTED,
66        RECORDING_START_REQUESTED,
67        RECORDING_STARTED,
68        RECORDING_STOP_REQUESTED,
69        ERROR,
70        RELEASED,
71    }
72    private final DvrSessionManager mSessionManager;
73    private final DvrManager mDvrManager;
74
75    private final WritableDvrDataManager mDataManager;
76    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
77    private TvRecordingClient mTvRecordingClient;
78    private Handler mHandler;
79    private ScheduledRecording mScheduledRecording;
80    private final Channel mChannel;
81    private State mState = State.NOT_STARTED;
82    private final Clock mClock;
83
84    RecordingTask(ScheduledRecording scheduledRecording, Channel channel,
85            DvrManager dvrManager, DvrSessionManager sessionManager,
86            WritableDvrDataManager dataManager, Clock clock) {
87        mScheduledRecording = scheduledRecording;
88        mChannel = channel;
89        mSessionManager = sessionManager;
90        mDataManager = dataManager;
91        mClock = clock;
92        mDvrManager = dvrManager;
93
94        if (DEBUG) Log.d(TAG, "created recording task " + mScheduledRecording);
95    }
96
97    public void setHandler(Handler handler) {
98        mHandler = handler;
99    }
100
101    @Override
102    public boolean handleMessage(Message msg) {
103        if (DEBUG) Log.d(TAG, "handleMessage " + msg);
104        SoftPreconditions
105                .checkState(msg.what == Scheduler.HandlerWrapper.MESSAGE_REMOVE || mHandler != null,
106                        TAG, "Null handler trying to handle " + msg);
107        try {
108            switch (msg.what) {
109                case MESSAGE_INIT:
110                    handleInit();
111                    break;
112                case MESSAGE_START_RECORDING:
113                    handleStartRecording();
114                    break;
115                case MESSAGE_STOP_RECORDING:
116                    handleStopRecording();
117                    break;
118                case Scheduler.HandlerWrapper.MESSAGE_REMOVE:
119                    // Clear the handler
120                    mHandler = null;
121                    release();
122                    return false;
123                default:
124                    SoftPreconditions.checkArgument(false, TAG, "unexpected message type " + msg);
125            }
126            return true;
127        } catch (Exception e) {
128            Log.w(TAG, "Error processing message " + msg + "  for " + mScheduledRecording, e);
129            failAndQuit();
130        }
131        return false;
132    }
133
134    @Override
135    public void onTuned(Uri channelUri) {
136        if (DEBUG) {
137            Log.d(TAG, "onTuned");
138        }
139        super.onTuned(channelUri);
140        mState = State.CONNECTED;
141        if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_START_RECORDING,
142                mScheduledRecording.getStartTimeMs() - MS_BEFORE_START)) {
143            mState = State.ERROR;
144            return;
145        }
146    }
147
148
149    @Override
150    public void onRecordingStopped(Uri recordedProgramUri) {
151        super.onRecordingStopped(recordedProgramUri);
152        mState = State.CONNECTED;
153        updateRecording(ScheduledRecording.buildFrom(mScheduledRecording)
154                .setState(ScheduledRecording.STATE_RECORDING_FINISHED).build());
155        sendRemove();
156    }
157
158    @Override
159    public void onError(int reason) {
160        if (DEBUG) Log.d(TAG, "onError reason " + reason);
161        super.onError(reason);
162        // TODO(dvr) handle success
163        switch (reason) {
164            default:
165                updateRecording(ScheduledRecording.buildFrom(mScheduledRecording)
166                        .setState(ScheduledRecording.STATE_RECORDING_FAILED)
167                        .build());
168        }
169        release();
170        sendRemove();
171    }
172
173    private void handleInit() {
174        if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording);
175        //TODO check recording preconditions
176
177        if (mScheduledRecording.getEndTimeMs() < mClock.currentTimeMillis()) {
178            Log.w(TAG, "End time already past, not recording " + mScheduledRecording);
179            failAndQuit();
180            return;
181        }
182
183        if (mChannel == null) {
184            Log.w(TAG, "Null channel for " + mScheduledRecording);
185            failAndQuit();
186            return;
187        }
188        if (mChannel.getId() != mScheduledRecording.getChannelId()) {
189            Log.w(TAG, "Channel" + mChannel + " does not match scheduled recording "
190                    + mScheduledRecording);
191            failAndQuit();
192            return;
193        }
194
195        String inputId = mChannel.getInputId();
196        if (mSessionManager.canAcquireDvrSession(inputId, mChannel)) {
197            mTvRecordingClient = mSessionManager
198                    .createTvRecordingClient("recordingTask-" + mScheduledRecording.getId(), this,
199                            mHandler);
200            mState = State.SESSION_ACQUIRED;
201        } else {
202            Log.w(TAG, "Unable to acquire a session for " + mScheduledRecording);
203            failAndQuit();
204            return;
205        }
206        mDvrManager.addListener(this, mHandler);
207        mTvRecordingClient.tune(inputId, mChannel.getUri());
208        mState = State.CONNECTION_PENDING;
209    }
210
211    private void failAndQuit() {
212        if (DEBUG) Log.d(TAG, "failAndQuit");
213        updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED);
214        mState = State.ERROR;
215        sendRemove();
216    }
217
218    private void sendRemove() {
219        if (DEBUG) Log.d(TAG, "sendRemove");
220        if (mHandler != null) {
221            mHandler.sendEmptyMessage(Scheduler.HandlerWrapper.MESSAGE_REMOVE);
222        }
223    }
224
225    private void handleStartRecording() {
226        if (DEBUG) Log.d(TAG, "handleStartRecording " + mScheduledRecording);
227        // TODO(DVR) handle errors
228        long programId = mScheduledRecording.getProgramId();
229        mTvRecordingClient.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null
230                : TvContract.buildProgramUri(programId));
231        updateRecording(ScheduledRecording.buildFrom(mScheduledRecording)
232                .setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS).build());
233        mState = State.RECORDING_STARTED;
234
235        if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_STOP_RECORDING,
236                mScheduledRecording.getEndTimeMs() + MS_AFTER_END)) {
237            mState = State.ERROR;
238            return;
239        }
240    }
241
242    private void handleStopRecording() {
243        if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording);
244        mTvRecordingClient.stopRecording();
245        mState = State.RECORDING_STOP_REQUESTED;
246    }
247
248    @VisibleForTesting
249    State getState() {
250        return mState;
251    }
252
253    private void release() {
254        if (mTvRecordingClient != null) {
255           mSessionManager.releaseTvRecordingClient(mTvRecordingClient);
256        }
257        mDvrManager.removeListener(this);
258    }
259
260    private boolean sendEmptyMessageAtAbsoluteTime(int what, long when) {
261        long now = mClock.currentTimeMillis();
262        long delay = Math.max(0L, when - now);
263        if (DEBUG) {
264            Log.d(TAG, "Sending message " + what + " with a delay of " + delay / 1000
265                    + " seconds to arrive at " + Utils.toIsoDateTimeString(when));
266        }
267        return mHandler.sendEmptyMessageDelayed(what, delay);
268    }
269
270    private void updateRecordingState(@ScheduledRecording.RecordingState int state) {
271        updateRecording(ScheduledRecording.buildFrom(mScheduledRecording).setState(state).build());
272    }
273
274    @VisibleForTesting
275    static Uri getIdAsMediaUri(ScheduledRecording scheduledRecording) {
276            // TODO define the URI format
277        return new Uri.Builder().appendPath(String.valueOf(scheduledRecording.getId())).build();
278    }
279
280    private void updateRecording(ScheduledRecording updatedScheduledRecording) {
281        if (DEBUG) Log.d(TAG, "updateScheduledRecording " + updatedScheduledRecording);
282        mScheduledRecording = updatedScheduledRecording;
283        mMainThreadHandler.post(new Runnable() {
284            @Override
285            public void run() {
286                mDataManager.updateScheduledRecording(mScheduledRecording);
287            }
288        });
289    }
290
291    @Override
292    public void onStopRecordingRequested(ScheduledRecording recording) {
293        if (recording.getId() != mScheduledRecording.getId()) {
294            return;
295        }
296        switch (mState) {
297            case RECORDING_STARTED:
298                mHandler.removeMessages(MESSAGE_STOP_RECORDING);
299                handleStopRecording();
300                break;
301            case RECORDING_STOP_REQUESTED:
302                // Do nothing
303                break;
304            case NOT_STARTED:
305            case SESSION_ACQUIRED:
306            case CONNECTION_PENDING:
307            case CONNECTED:
308            case RECORDING_START_REQUESTED:
309            case ERROR:
310            case RELEASED:
311            default:
312                sendRemove();
313                break;
314        }
315    }
316
317    @Override
318    public String toString() {
319        return getClass().getName() + "(" + mScheduledRecording + ")";
320    }
321}
322