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