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 
17 package com.android.tv.dvr;
18 
19 import android.media.tv.TvContract;
20 import android.media.tv.TvRecordingClient;
21 import android.net.Uri;
22 import android.os.Handler;
23 import android.os.Looper;
24 import android.os.Message;
25 import android.support.annotation.VisibleForTesting;
26 import android.support.annotation.WorkerThread;
27 import android.util.Log;
28 
29 import com.android.tv.common.SoftPreconditions;
30 import com.android.tv.data.Channel;
31 import com.android.tv.util.Clock;
32 import com.android.tv.util.Utils;
33 
34 import 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
43 class 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 
RecordingTask(ScheduledRecording scheduledRecording, Channel channel, DvrManager dvrManager, DvrSessionManager sessionManager, WritableDvrDataManager dataManager, Clock clock)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 
setHandler(Handler handler)97     public void setHandler(Handler handler) {
98         mHandler = handler;
99     }
100 
101     @Override
handleMessage(Message msg)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
onTuned(Uri channelUri)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
onRecordingStopped(Uri recordedProgramUri)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
onError(int reason)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 
handleInit()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 
failAndQuit()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 
sendRemove()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 
handleStartRecording()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 
handleStopRecording()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
getState()249     State getState() {
250         return mState;
251     }
252 
release()253     private void release() {
254         if (mTvRecordingClient != null) {
255            mSessionManager.releaseTvRecordingClient(mTvRecordingClient);
256         }
257         mDvrManager.removeListener(this);
258     }
259 
sendEmptyMessageAtAbsoluteTime(int what, long when)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 
updateRecordingState(@cheduledRecording.RecordingState int state)270     private void updateRecordingState(@ScheduledRecording.RecordingState int state) {
271         updateRecording(ScheduledRecording.buildFrom(mScheduledRecording).setState(state).build());
272     }
273 
274     @VisibleForTesting
getIdAsMediaUri(ScheduledRecording scheduledRecording)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 
updateRecording(ScheduledRecording updatedScheduledRecording)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
onStopRecordingRequested(ScheduledRecording recording)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
toString()318     public String toString() {
319         return getClass().getName() + "(" + mScheduledRecording + ")";
320     }
321 }
322