1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.tv.dvr; 18 19 import android.media.tv.TvContract; 20 import android.media.tv.TvRecordingClient; 21 import android.net.Uri; 22 import android.os.Handler; 23 import android.os.Looper; 24 import android.os.Message; 25 import android.support.annotation.VisibleForTesting; 26 import android.support.annotation.WorkerThread; 27 import android.util.Log; 28 29 import com.android.tv.common.SoftPreconditions; 30 import com.android.tv.data.Channel; 31 import com.android.tv.util.Clock; 32 import com.android.tv.util.Utils; 33 34 import java.util.concurrent.TimeUnit; 35 36 /** 37 * A Handler that actually starts and stop a recording at the right time. 38 * 39 * <p>This is run on the looper of thread named {@value DvrRecordingService#HANDLER_THREAD_NAME}. 40 * There is only one looper so messages must be handled quickly or start a separate thread. 41 */ 42 @WorkerThread 43 class RecordingTask extends TvRecordingClient.RecordingCallback 44 implements Handler.Callback, DvrManager.Listener { 45 private static final String TAG = "RecordingTask"; 46 private static final boolean DEBUG = false; 47 48 @VisibleForTesting 49 static final int MESSAGE_INIT = 1; 50 @VisibleForTesting 51 static final int MESSAGE_START_RECORDING = 2; 52 @VisibleForTesting 53 static final int MESSAGE_STOP_RECORDING = 3; 54 55 @VisibleForTesting 56 static final long MS_BEFORE_START = TimeUnit.SECONDS.toMillis(5); 57 @VisibleForTesting 58 static final long MS_AFTER_END = TimeUnit.SECONDS.toMillis(5); 59 60 @VisibleForTesting 61 enum State { 62 NOT_STARTED, 63 SESSION_ACQUIRED, 64 CONNECTION_PENDING, 65 CONNECTED, 66 RECORDING_START_REQUESTED, 67 RECORDING_STARTED, 68 RECORDING_STOP_REQUESTED, 69 ERROR, 70 RELEASED, 71 } 72 private final DvrSessionManager mSessionManager; 73 private final DvrManager mDvrManager; 74 75 private final WritableDvrDataManager mDataManager; 76 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 77 private TvRecordingClient mTvRecordingClient; 78 private Handler mHandler; 79 private ScheduledRecording mScheduledRecording; 80 private final Channel mChannel; 81 private State mState = State.NOT_STARTED; 82 private final Clock mClock; 83 RecordingTask(ScheduledRecording scheduledRecording, Channel channel, DvrManager dvrManager, DvrSessionManager sessionManager, WritableDvrDataManager dataManager, Clock clock)84 RecordingTask(ScheduledRecording scheduledRecording, Channel channel, 85 DvrManager dvrManager, DvrSessionManager sessionManager, 86 WritableDvrDataManager dataManager, Clock clock) { 87 mScheduledRecording = scheduledRecording; 88 mChannel = channel; 89 mSessionManager = sessionManager; 90 mDataManager = dataManager; 91 mClock = clock; 92 mDvrManager = dvrManager; 93 94 if (DEBUG) Log.d(TAG, "created recording task " + mScheduledRecording); 95 } 96 setHandler(Handler handler)97 public void setHandler(Handler handler) { 98 mHandler = handler; 99 } 100 101 @Override handleMessage(Message msg)102 public boolean handleMessage(Message msg) { 103 if (DEBUG) Log.d(TAG, "handleMessage " + msg); 104 SoftPreconditions 105 .checkState(msg.what == Scheduler.HandlerWrapper.MESSAGE_REMOVE || mHandler != null, 106 TAG, "Null handler trying to handle " + msg); 107 try { 108 switch (msg.what) { 109 case MESSAGE_INIT: 110 handleInit(); 111 break; 112 case MESSAGE_START_RECORDING: 113 handleStartRecording(); 114 break; 115 case MESSAGE_STOP_RECORDING: 116 handleStopRecording(); 117 break; 118 case Scheduler.HandlerWrapper.MESSAGE_REMOVE: 119 // Clear the handler 120 mHandler = null; 121 release(); 122 return false; 123 default: 124 SoftPreconditions.checkArgument(false, TAG, "unexpected message type " + msg); 125 } 126 return true; 127 } catch (Exception e) { 128 Log.w(TAG, "Error processing message " + msg + " for " + mScheduledRecording, e); 129 failAndQuit(); 130 } 131 return false; 132 } 133 134 @Override onTuned(Uri channelUri)135 public void onTuned(Uri channelUri) { 136 if (DEBUG) { 137 Log.d(TAG, "onTuned"); 138 } 139 super.onTuned(channelUri); 140 mState = State.CONNECTED; 141 if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_START_RECORDING, 142 mScheduledRecording.getStartTimeMs() - MS_BEFORE_START)) { 143 mState = State.ERROR; 144 return; 145 } 146 } 147 148 149 @Override onRecordingStopped(Uri recordedProgramUri)150 public void onRecordingStopped(Uri recordedProgramUri) { 151 super.onRecordingStopped(recordedProgramUri); 152 mState = State.CONNECTED; 153 updateRecording(ScheduledRecording.buildFrom(mScheduledRecording) 154 .setState(ScheduledRecording.STATE_RECORDING_FINISHED).build()); 155 sendRemove(); 156 } 157 158 @Override onError(int reason)159 public void onError(int reason) { 160 if (DEBUG) Log.d(TAG, "onError reason " + reason); 161 super.onError(reason); 162 // TODO(dvr) handle success 163 switch (reason) { 164 default: 165 updateRecording(ScheduledRecording.buildFrom(mScheduledRecording) 166 .setState(ScheduledRecording.STATE_RECORDING_FAILED) 167 .build()); 168 } 169 release(); 170 sendRemove(); 171 } 172 handleInit()173 private void handleInit() { 174 if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording); 175 //TODO check recording preconditions 176 177 if (mScheduledRecording.getEndTimeMs() < mClock.currentTimeMillis()) { 178 Log.w(TAG, "End time already past, not recording " + mScheduledRecording); 179 failAndQuit(); 180 return; 181 } 182 183 if (mChannel == null) { 184 Log.w(TAG, "Null channel for " + mScheduledRecording); 185 failAndQuit(); 186 return; 187 } 188 if (mChannel.getId() != mScheduledRecording.getChannelId()) { 189 Log.w(TAG, "Channel" + mChannel + " does not match scheduled recording " 190 + mScheduledRecording); 191 failAndQuit(); 192 return; 193 } 194 195 String inputId = mChannel.getInputId(); 196 if (mSessionManager.canAcquireDvrSession(inputId, mChannel)) { 197 mTvRecordingClient = mSessionManager 198 .createTvRecordingClient("recordingTask-" + mScheduledRecording.getId(), this, 199 mHandler); 200 mState = State.SESSION_ACQUIRED; 201 } else { 202 Log.w(TAG, "Unable to acquire a session for " + mScheduledRecording); 203 failAndQuit(); 204 return; 205 } 206 mDvrManager.addListener(this, mHandler); 207 mTvRecordingClient.tune(inputId, mChannel.getUri()); 208 mState = State.CONNECTION_PENDING; 209 } 210 failAndQuit()211 private void failAndQuit() { 212 if (DEBUG) Log.d(TAG, "failAndQuit"); 213 updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); 214 mState = State.ERROR; 215 sendRemove(); 216 } 217 sendRemove()218 private void sendRemove() { 219 if (DEBUG) Log.d(TAG, "sendRemove"); 220 if (mHandler != null) { 221 mHandler.sendEmptyMessage(Scheduler.HandlerWrapper.MESSAGE_REMOVE); 222 } 223 } 224 handleStartRecording()225 private void handleStartRecording() { 226 if (DEBUG) Log.d(TAG, "handleStartRecording " + mScheduledRecording); 227 // TODO(DVR) handle errors 228 long programId = mScheduledRecording.getProgramId(); 229 mTvRecordingClient.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null 230 : TvContract.buildProgramUri(programId)); 231 updateRecording(ScheduledRecording.buildFrom(mScheduledRecording) 232 .setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS).build()); 233 mState = State.RECORDING_STARTED; 234 235 if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_STOP_RECORDING, 236 mScheduledRecording.getEndTimeMs() + MS_AFTER_END)) { 237 mState = State.ERROR; 238 return; 239 } 240 } 241 handleStopRecording()242 private void handleStopRecording() { 243 if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording); 244 mTvRecordingClient.stopRecording(); 245 mState = State.RECORDING_STOP_REQUESTED; 246 } 247 248 @VisibleForTesting getState()249 State getState() { 250 return mState; 251 } 252 release()253 private void release() { 254 if (mTvRecordingClient != null) { 255 mSessionManager.releaseTvRecordingClient(mTvRecordingClient); 256 } 257 mDvrManager.removeListener(this); 258 } 259 sendEmptyMessageAtAbsoluteTime(int what, long when)260 private boolean sendEmptyMessageAtAbsoluteTime(int what, long when) { 261 long now = mClock.currentTimeMillis(); 262 long delay = Math.max(0L, when - now); 263 if (DEBUG) { 264 Log.d(TAG, "Sending message " + what + " with a delay of " + delay / 1000 265 + " seconds to arrive at " + Utils.toIsoDateTimeString(when)); 266 } 267 return mHandler.sendEmptyMessageDelayed(what, delay); 268 } 269 updateRecordingState(@cheduledRecording.RecordingState int state)270 private void updateRecordingState(@ScheduledRecording.RecordingState int state) { 271 updateRecording(ScheduledRecording.buildFrom(mScheduledRecording).setState(state).build()); 272 } 273 274 @VisibleForTesting getIdAsMediaUri(ScheduledRecording scheduledRecording)275 static Uri getIdAsMediaUri(ScheduledRecording scheduledRecording) { 276 // TODO define the URI format 277 return new Uri.Builder().appendPath(String.valueOf(scheduledRecording.getId())).build(); 278 } 279 updateRecording(ScheduledRecording updatedScheduledRecording)280 private void updateRecording(ScheduledRecording updatedScheduledRecording) { 281 if (DEBUG) Log.d(TAG, "updateScheduledRecording " + updatedScheduledRecording); 282 mScheduledRecording = updatedScheduledRecording; 283 mMainThreadHandler.post(new Runnable() { 284 @Override 285 public void run() { 286 mDataManager.updateScheduledRecording(mScheduledRecording); 287 } 288 }); 289 } 290 291 @Override onStopRecordingRequested(ScheduledRecording recording)292 public void onStopRecordingRequested(ScheduledRecording recording) { 293 if (recording.getId() != mScheduledRecording.getId()) { 294 return; 295 } 296 switch (mState) { 297 case RECORDING_STARTED: 298 mHandler.removeMessages(MESSAGE_STOP_RECORDING); 299 handleStopRecording(); 300 break; 301 case RECORDING_STOP_REQUESTED: 302 // Do nothing 303 break; 304 case NOT_STARTED: 305 case SESSION_ACQUIRED: 306 case CONNECTION_PENDING: 307 case CONNECTED: 308 case RECORDING_START_REQUESTED: 309 case ERROR: 310 case RELEASED: 311 default: 312 sendRemove(); 313 break; 314 } 315 } 316 317 @Override toString()318 public String toString() { 319 return getClass().getName() + "(" + mScheduledRecording + ")"; 320 } 321 } 322