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.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 
    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 
    159     public void setHandler(Handler handler) {
    160         mHandler = handler;
    161     }
    162 
    163     @Override
    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
    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
    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
    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
    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
    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 
    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 
    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 
    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 
    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 
    349     private void handleStopRecording() {
    350         if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording);
    351         mRecordingSession.stopRecording();
    352         mState = State.RECORDING_STOP_REQUESTED;
    353     }
    354 
    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
    372     State getState() {
    373         return mState;
    374     }
    375 
    376     private long getScheduleId() {
    377         return mScheduledRecording.getId();
    378     }
    379 
    380     /**
    381      * Returns the priority.
    382      */
    383     public long getPriority() {
    384         return mScheduledRecording.getPriority();
    385     }
    386 
    387     /**
    388      * Returns the start time of the recording.
    389      */
    390     public long getStartTimeMs() {
    391         return mScheduledRecording.getStartTimeMs();
    392     }
    393 
    394     /**
    395      * Returns the end time of the recording.
    396      */
    397     public long getEndTimeMs() {
    398         return mScheduledRecording.getEndTimeMs();
    399     }
    400 
    401     private void release() {
    402         if (mRecordingSession != null) {
    403             mSessionManager.releaseRecordingSession(mRecordingSession);
    404             mRecordingSession = null;
    405         }
    406         mDvrManager.removeListener(this);
    407     }
    408 
    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 
    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
    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      */
    453     public void start() {
    454         mHandler.sendEmptyMessage(MSG_INITIALIZE);
    455     }
    456 
    457     /**
    458      * Stops the task.
    459      */
    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      */
    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      */
    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
    507     public String toString() {
    508         return getClass().getName() + "(" + mScheduledRecording + ")";
    509     }
    510 
    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 
    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