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