• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.recorder;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.media.tv.TvContract;
22 import android.media.tv.TvInputManager;
23 import android.media.tv.TvRecordingClient.RecordingCallback;
24 import android.net.Uri;
25 import android.os.Build;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.os.Message;
29 import android.support.annotation.VisibleForTesting;
30 import android.support.annotation.WorkerThread;
31 import android.util.Log;
32 import android.widget.Toast;
33 
34 import com.android.tv.InputSessionManager;
35 import com.android.tv.InputSessionManager.RecordingSession;
36 import com.android.tv.R;
37 import com.android.tv.TvApplication;
38 import com.android.tv.common.SoftPreconditions;
39 import com.android.tv.data.Channel;
40 import com.android.tv.dvr.DvrManager;
41 import com.android.tv.dvr.WritableDvrDataManager;
42 import com.android.tv.dvr.data.ScheduledRecording;
43 import com.android.tv.dvr.recorder.InputTaskScheduler.HandlerWrapper;
44 import com.android.tv.util.Clock;
45 import com.android.tv.util.Utils;
46 
47 import java.util.Comparator;
48 import java.util.concurrent.TimeUnit;
49 
50 /**
51  * A Handler that actually starts and stop a recording at the right time.
52  *
53  * <p>This is run on the looper of thread named {@value DvrRecordingService#HANDLER_THREAD_NAME}.
54  * There is only one looper so messages must be handled quickly or start a separate thread.
55  */
56 @WorkerThread
57 @TargetApi(Build.VERSION_CODES.N)
58 public class RecordingTask extends RecordingCallback implements Handler.Callback,
59         DvrManager.Listener {
60     private static final String TAG = "RecordingTask";
61     private static final boolean DEBUG = false;
62 
63     /**
64      * Compares the end time in ascending order.
65      */
66     public static final Comparator<RecordingTask> END_TIME_COMPARATOR
67             = new Comparator<RecordingTask>() {
68         @Override
69         public int compare(RecordingTask lhs, RecordingTask rhs) {
70             return Long.compare(lhs.getEndTimeMs(), rhs.getEndTimeMs());
71         }
72     };
73 
74     /**
75      * Compares ID in ascending order.
76      */
77     public static final Comparator<RecordingTask> ID_COMPARATOR
78             = new Comparator<RecordingTask>() {
79         @Override
80         public int compare(RecordingTask lhs, RecordingTask rhs) {
81             return Long.compare(lhs.getScheduleId(), rhs.getScheduleId());
82         }
83     };
84 
85     /**
86      * Compares the priority in ascending order.
87      */
88     public static final Comparator<RecordingTask> PRIORITY_COMPARATOR
89             = new Comparator<RecordingTask>() {
90         @Override
91         public int compare(RecordingTask lhs, RecordingTask rhs) {
92             return Long.compare(lhs.getPriority(), rhs.getPriority());
93         }
94     };
95 
96     @VisibleForTesting
97     static final int MSG_INITIALIZE = 1;
98     @VisibleForTesting
99     static final int MSG_START_RECORDING = 2;
100     @VisibleForTesting
101     static final int MSG_STOP_RECORDING = 3;
102     /**
103      * Message to update schedule.
104      */
105     public static final int MSG_UDPATE_SCHEDULE = 4;
106 
107     /**
108      * The time when the start command will be sent before the recording starts.
109      */
110     public static final long RECORDING_EARLY_START_OFFSET_MS = TimeUnit.SECONDS.toMillis(3);
111     /**
112      * If the recording starts later than the scheduled start time or ends before the scheduled end
113      * time, it's considered as clipped.
114      */
115     private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5);
116 
117     @VisibleForTesting
118     enum State {
119         NOT_STARTED,
120         SESSION_ACQUIRED,
121         CONNECTION_PENDING,
122         CONNECTED,
123         RECORDING_STARTED,
124         RECORDING_STOP_REQUESTED,
125         FINISHED,
126         ERROR,
127         RELEASED,
128     }
129     private final InputSessionManager mSessionManager;
130     private final DvrManager mDvrManager;
131     private final Context mContext;
132 
133     private final WritableDvrDataManager mDataManager;
134     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
135     private RecordingSession mRecordingSession;
136     private Handler mHandler;
137     private ScheduledRecording mScheduledRecording;
138     private final Channel mChannel;
139     private State mState = State.NOT_STARTED;
140     private final Clock mClock;
141     private boolean mStartedWithClipping;
142     private Uri mRecordedProgramUri;
143     private boolean mCanceled;
144 
RecordingTask(Context context, ScheduledRecording scheduledRecording, Channel channel, DvrManager dvrManager, InputSessionManager sessionManager, WritableDvrDataManager dataManager, Clock clock)145     RecordingTask(Context context, ScheduledRecording scheduledRecording, Channel channel,
146             DvrManager dvrManager, InputSessionManager sessionManager,
147             WritableDvrDataManager dataManager, Clock clock) {
148         mContext = context;
149         mScheduledRecording = scheduledRecording;
150         mChannel = channel;
151         mSessionManager = sessionManager;
152         mDataManager = dataManager;
153         mClock = clock;
154         mDvrManager = dvrManager;
155 
156         if (DEBUG) Log.d(TAG, "created recording task " + mScheduledRecording);
157     }
158 
setHandler(Handler handler)159     public void setHandler(Handler handler) {
160         mHandler = handler;
161     }
162 
163     @Override
handleMessage(Message msg)164     public boolean handleMessage(Message msg) {
165         if (DEBUG) Log.d(TAG, "handleMessage " + msg);
166         SoftPreconditions.checkState(msg.what == HandlerWrapper.MESSAGE_REMOVE || mHandler != null,
167                 TAG, "Null handler trying to handle " + msg);
168         try {
169             switch (msg.what) {
170                 case MSG_INITIALIZE:
171                     handleInit();
172                     break;
173                 case MSG_START_RECORDING:
174                     handleStartRecording();
175                     break;
176                 case MSG_STOP_RECORDING:
177                     handleStopRecording();
178                     break;
179                 case MSG_UDPATE_SCHEDULE:
180                     handleUpdateSchedule((ScheduledRecording) msg.obj);
181                     break;
182                 case HandlerWrapper.MESSAGE_REMOVE:
183                     mHandler.removeCallbacksAndMessages(null);
184                     mHandler = null;
185                     release();
186                     return false;
187                 default:
188                     SoftPreconditions.checkArgument(false, TAG, "unexpected message type " + msg);
189                     break;
190             }
191             return true;
192         } catch (Exception e) {
193             Log.w(TAG, "Error processing message " + msg + "  for " + mScheduledRecording, e);
194             failAndQuit();
195         }
196         return false;
197     }
198 
199     @Override
onDisconnected(String inputId)200     public void onDisconnected(String inputId) {
201         if (DEBUG) Log.d(TAG, "onDisconnected(" + inputId + ")");
202         if (mRecordingSession != null && mState != State.FINISHED) {
203             failAndQuit();
204         }
205     }
206 
207     @Override
onConnectionFailed(String inputId)208     public void onConnectionFailed(String inputId) {
209         if (DEBUG) Log.d(TAG, "onConnectionFailed(" + inputId + ")");
210         if (mRecordingSession != null) {
211             failAndQuit();
212         }
213     }
214 
215     @Override
onTuned(Uri channelUri)216     public void onTuned(Uri channelUri) {
217         if (DEBUG) Log.d(TAG, "onTuned");
218         if (mRecordingSession == null) {
219             return;
220         }
221         mState = State.CONNECTED;
222         if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MSG_START_RECORDING,
223                 mScheduledRecording.getStartTimeMs() - RECORDING_EARLY_START_OFFSET_MS)) {
224             failAndQuit();
225         }
226     }
227 
228     @Override
onRecordingStopped(Uri recordedProgramUri)229     public void onRecordingStopped(Uri recordedProgramUri) {
230         if (DEBUG) Log.d(TAG, "onRecordingStopped");
231         if (mRecordingSession == null) {
232             return;
233         }
234         mRecordedProgramUri = recordedProgramUri;
235         mState = State.FINISHED;
236         int state = ScheduledRecording.STATE_RECORDING_FINISHED;
237         if (mStartedWithClipping || mScheduledRecording.getEndTimeMs() - CLIPPED_THRESHOLD_MS
238                 > mClock.currentTimeMillis()) {
239             state = ScheduledRecording.STATE_RECORDING_CLIPPED;
240         }
241         updateRecordingState(state);
242         sendRemove();
243         if (mCanceled) {
244             removeRecordedProgram();
245         }
246     }
247 
248     @Override
onError(int reason)249     public void onError(int reason) {
250         if (DEBUG) Log.d(TAG, "onError reason " + reason);
251         if (mRecordingSession == null) {
252             return;
253         }
254         switch (reason) {
255             case TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE:
256                 mMainThreadHandler.post(new Runnable() {
257                     @Override
258                     public void run() {
259                         if (TvApplication.getSingletons(mContext).getMainActivityWrapper()
260                                 .isResumed()) {
261                             ScheduledRecording scheduledRecording = mDataManager
262                                     .getScheduledRecording(mScheduledRecording.getId());
263                             if (scheduledRecording != null) {
264                                 Toast.makeText(mContext.getApplicationContext(),
265                                         mContext.getString(R.string
266                                         .dvr_error_insufficient_space_description_one_recording,
267                                         scheduledRecording.getProgramDisplayTitle(mContext)),
268                                         Toast.LENGTH_LONG)
269                                         .show();
270                             }
271                         } else {
272                             Utils.setRecordingFailedReason(mContext.getApplicationContext(),
273                                     TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
274                             Utils.addFailedScheduledRecordingInfo(mContext.getApplicationContext(),
275                                     mScheduledRecording.getProgramDisplayTitle(mContext));
276                         }
277                     }
278                 });
279                 // Pass through
280             default:
281                 failAndQuit();
282                 break;
283         }
284     }
285 
handleInit()286     private void handleInit() {
287         if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording);
288         if (mScheduledRecording.getEndTimeMs() < mClock.currentTimeMillis()) {
289             Log.w(TAG, "End time already past, not recording " + mScheduledRecording);
290             failAndQuit();
291             return;
292         }
293         if (mChannel == null) {
294             Log.w(TAG, "Null channel for " + mScheduledRecording);
295             failAndQuit();
296             return;
297         }
298         if (mChannel.getId() != mScheduledRecording.getChannelId()) {
299             Log.w(TAG, "Channel" + mChannel + " does not match scheduled recording "
300                     + mScheduledRecording);
301             failAndQuit();
302             return;
303         }
304 
305         String inputId = mChannel.getInputId();
306         mRecordingSession = mSessionManager.createRecordingSession(inputId,
307                 "recordingTask-" + mScheduledRecording.getId(), this,
308                 mHandler, mScheduledRecording.getEndTimeMs());
309         mState = State.SESSION_ACQUIRED;
310         mDvrManager.addListener(this, mHandler);
311         mRecordingSession.tune(inputId, mChannel.getUri());
312         mState = State.CONNECTION_PENDING;
313     }
314 
failAndQuit()315     private void failAndQuit() {
316         if (DEBUG) Log.d(TAG, "failAndQuit");
317         updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED);
318         mState = State.ERROR;
319         sendRemove();
320     }
321 
sendRemove()322     private void sendRemove() {
323         if (DEBUG) Log.d(TAG, "sendRemove");
324         if (mHandler != null) {
325             mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(
326                     HandlerWrapper.MESSAGE_REMOVE));
327         }
328     }
329 
handleStartRecording()330     private void handleStartRecording() {
331         if (DEBUG) Log.d(TAG, "handleStartRecording " + mScheduledRecording);
332         long programId = mScheduledRecording.getProgramId();
333         mRecordingSession.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null
334                 : TvContract.buildProgramUri(programId));
335         updateRecordingState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS);
336         // If it starts late, it's clipped.
337         if (mScheduledRecording.getStartTimeMs() + CLIPPED_THRESHOLD_MS
338                 < mClock.currentTimeMillis()) {
339             mStartedWithClipping = true;
340         }
341         mState = State.RECORDING_STARTED;
342 
343         if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING,
344                 mScheduledRecording.getEndTimeMs())) {
345             failAndQuit();
346         }
347     }
348 
handleStopRecording()349     private void handleStopRecording() {
350         if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording);
351         mRecordingSession.stopRecording();
352         mState = State.RECORDING_STOP_REQUESTED;
353     }
354 
handleUpdateSchedule(ScheduledRecording schedule)355     private void handleUpdateSchedule(ScheduledRecording schedule) {
356         mScheduledRecording = schedule;
357         // Check end time only. The start time is checked in InputTaskScheduler.
358         if (schedule.getEndTimeMs() != mScheduledRecording.getEndTimeMs()) {
359             if (mRecordingSession != null) {
360                 mRecordingSession.setEndTimeMs(schedule.getEndTimeMs());
361             }
362             if (mState == State.RECORDING_STARTED) {
363                 mHandler.removeMessages(MSG_STOP_RECORDING);
364                 if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) {
365                     failAndQuit();
366                 }
367             }
368         }
369     }
370 
371     @VisibleForTesting
getState()372     State getState() {
373         return mState;
374     }
375 
getScheduleId()376     private long getScheduleId() {
377         return mScheduledRecording.getId();
378     }
379 
380     /**
381      * Returns the priority.
382      */
getPriority()383     public long getPriority() {
384         return mScheduledRecording.getPriority();
385     }
386 
387     /**
388      * Returns the start time of the recording.
389      */
getStartTimeMs()390     public long getStartTimeMs() {
391         return mScheduledRecording.getStartTimeMs();
392     }
393 
394     /**
395      * Returns the end time of the recording.
396      */
getEndTimeMs()397     public long getEndTimeMs() {
398         return mScheduledRecording.getEndTimeMs();
399     }
400 
release()401     private void release() {
402         if (mRecordingSession != null) {
403             mSessionManager.releaseRecordingSession(mRecordingSession);
404             mRecordingSession = null;
405         }
406         mDvrManager.removeListener(this);
407     }
408 
sendEmptyMessageAtAbsoluteTime(int what, long when)409     private boolean sendEmptyMessageAtAbsoluteTime(int what, long when) {
410         long now = mClock.currentTimeMillis();
411         long delay = Math.max(0L, when - now);
412         if (DEBUG) {
413             Log.d(TAG, "Sending message " + what + " with a delay of " + delay / 1000
414                     + " seconds to arrive at " + Utils.toIsoDateTimeString(when));
415         }
416         return mHandler.sendEmptyMessageDelayed(what, delay);
417     }
418 
updateRecordingState(@cheduledRecording.RecordingState int state)419     private void updateRecordingState(@ScheduledRecording.RecordingState int state) {
420         if (DEBUG) Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state);
421         mScheduledRecording = ScheduledRecording.buildFrom(mScheduledRecording).setState(state)
422                 .build();
423         runOnMainThread(new Runnable() {
424             @Override
425             public void run() {
426                 ScheduledRecording schedule = mDataManager.getScheduledRecording(
427                         mScheduledRecording.getId());
428                 if (schedule == null) {
429                     // Schedule has been deleted. Delete the recorded program.
430                     removeRecordedProgram();
431                 } else  {
432                     // Update the state based on the object in DataManager in case when it has been
433                     // updated. mScheduledRecording will be updated from
434                     // onScheduledRecordingStateChanged.
435                     mDataManager.updateScheduledRecording(ScheduledRecording.buildFrom(schedule)
436                             .setState(state).build());
437                 }
438             }
439         });
440     }
441 
442     @Override
onStopRecordingRequested(ScheduledRecording recording)443     public void onStopRecordingRequested(ScheduledRecording recording) {
444         if (recording.getId() != mScheduledRecording.getId()) {
445             return;
446         }
447         stop();
448     }
449 
450     /**
451      * Starts the task.
452      */
start()453     public void start() {
454         mHandler.sendEmptyMessage(MSG_INITIALIZE);
455     }
456 
457     /**
458      * Stops the task.
459      */
stop()460     public void stop() {
461         if (DEBUG) Log.d(TAG, "stop");
462         switch (mState) {
463             case RECORDING_STARTED:
464                 mHandler.removeMessages(MSG_STOP_RECORDING);
465                 handleStopRecording();
466                 break;
467             case RECORDING_STOP_REQUESTED:
468                 // Do nothing
469                 break;
470             case NOT_STARTED:
471             case SESSION_ACQUIRED:
472             case CONNECTION_PENDING:
473             case CONNECTED:
474             case FINISHED:
475             case ERROR:
476             case RELEASED:
477             default:
478                 sendRemove();
479                 break;
480         }
481     }
482 
483     /**
484      * Cancels the task
485      */
cancel()486     public void cancel() {
487         if (DEBUG) Log.d(TAG, "cancel");
488         mCanceled = true;
489         stop();
490         removeRecordedProgram();
491     }
492 
493     /**
494      * Clean up the task.
495      */
cleanUp()496     public void cleanUp() {
497         if (mState == State.RECORDING_STARTED || mState == State.RECORDING_STOP_REQUESTED) {
498             updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED);
499         }
500         release();
501         if (mHandler != null) {
502             mHandler.removeCallbacksAndMessages(null);
503         }
504     }
505 
506     @Override
toString()507     public String toString() {
508         return getClass().getName() + "(" + mScheduledRecording + ")";
509     }
510 
removeRecordedProgram()511     private void removeRecordedProgram() {
512         runOnMainThread(new Runnable() {
513             @Override
514             public void run() {
515                 if (mRecordedProgramUri != null) {
516                     mDvrManager.removeRecordedProgram(mRecordedProgramUri);
517                 }
518             }
519         });
520     }
521 
runOnMainThread(Runnable runnable)522     private void runOnMainThread(Runnable runnable) {
523         if (Looper.myLooper() == Looper.getMainLooper()) {
524             runnable.run();
525         } else {
526             mMainThreadHandler.post(runnable);
527         }
528     }
529 }
530