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 17package com.android.usbtuner.tvinput; 18 19import android.content.ContentResolver; 20import android.content.ContentUris; 21import android.content.ContentValues; 22import android.content.Context; 23import android.database.Cursor; 24import android.media.MediaDataSource; 25import android.media.tv.TvContract; 26import android.media.tv.TvInputManager; 27import android.net.Uri; 28import android.os.Handler; 29import android.os.HandlerThread; 30import android.os.Looper; 31import android.os.Message; 32import android.support.annotation.IntDef; 33import android.support.annotation.Nullable; 34import android.util.Log; 35import android.widget.Toast; 36 37import com.google.android.exoplayer.util.Assertions; 38import com.android.tv.common.recording.RecordedProgram; 39import com.android.tv.common.recording.RecordingCapability; 40import com.android.usbtuner.DvbDeviceAccessor; 41import com.android.usbtuner.TunerHal; 42import com.android.usbtuner.UsbTunerDataSource; 43import com.android.usbtuner.data.PsipData; 44import com.android.usbtuner.data.TunerChannel; 45import com.android.usbtuner.exoplayer.Recorder; 46import com.android.usbtuner.exoplayer.cache.CacheManager; 47import com.android.usbtuner.exoplayer.cache.DvrStorageManager; 48 49import java.io.File; 50import java.io.IOException; 51import java.lang.annotation.Retention; 52import java.lang.annotation.RetentionPolicy; 53import java.util.List; 54import java.util.Locale; 55import java.util.Random; 56 57/** 58 * Implements a DVR feature. 59 */ 60public class TunerRecordingSessionWorker implements PlaybackCacheListener, 61 EventDetector.EventListener, Recorder.RecordListener, 62 Handler.Callback { 63 private static String TAG = "TunerRecordingSessionWorker"; 64 private static final boolean DEBUG = false; 65 66 private static final String SORT_BY_TIME = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS 67 + ", " + TvContract.Programs.COLUMN_CHANNEL_ID + ", " 68 + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS; 69 private static final int MSG_CONNECT = 1; 70 private static final int MSG_DISCONNECT = 2; 71 private static final int MSG_START_RECORDING = 3; 72 private static final int MSG_STOP_RECORDING = 4; 73 private static final int MSG_RECORDING_RESULT = 5; 74 private static final int MSG_DELETE_RECORDING = 6; 75 private static final int MSG_RELEASE = 7; 76 private RecordingCapability mCapabilities; 77 78 public RecordingCapability getCapabilities() { 79 return mCapabilities; 80 } 81 82 @IntDef({STATE_IDLE, STATE_CONNECTED, STATE_RECORDING}) 83 @Retention(RetentionPolicy.SOURCE) 84 public @interface DvrSessionState {} 85 private static final int STATE_IDLE = 1; 86 private static final int STATE_CONNECTED = 2; 87 private static final int STATE_RECORDING = 3; 88 89 private static final long CHANNEL_ID_NONE = -1; 90 91 private final Context mContext; 92 private final ChannelDataManager mChannelDataManager; 93 private final Handler mHandler; 94 private final Random mRandom = new Random(); 95 96 private TunerHal mTunerHal; 97 private UsbTunerDataSource mTunerSource; 98 private TunerChannel mChannel; 99 private File mStorageDir; 100 private long mRecordStartTime; 101 private long mRecordEndTime; 102 private CacheManager mCacheManager; 103 private Recorder mRecorder; 104 private final TunerRecordingSession mSession; 105 @DvrSessionState private int mSessionState = STATE_IDLE; 106 private final String mInputId; 107 108 public TunerRecordingSessionWorker(Context context, String inputId, 109 ChannelDataManager dataManager, TunerRecordingSession session) { 110 mRandom.setSeed(System.nanoTime()); 111 mContext = context; 112 HandlerThread handlerThread = new HandlerThread(TAG); 113 handlerThread.start(); 114 mHandler = new Handler(handlerThread.getLooper(), this); 115 mChannelDataManager = dataManager; 116 mChannelDataManager.checkDataVersion(context); 117 mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId); 118 mInputId = inputId; 119 if (DEBUG) Log.d(TAG, mCapabilities.toString()); 120 mSession = session; 121 } 122 123 // PlaybackCacheListener 124 @Override 125 public void onCacheStartTimeChanged(long startTimeMs) { 126 } 127 128 @Override 129 public void onCacheStateChanged(boolean available) { 130 } 131 132 @Override 133 public void onDiskTooSlow() { 134 } 135 136 // EventDetector.EventListener 137 @Override 138 public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { 139 if (mChannel == null || mChannel.compareTo(channel) != 0) { 140 return; 141 } 142 mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); 143 } 144 145 @Override 146 public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) { 147 if (mChannel == null || mChannel.compareTo(channel) != 0) { 148 return; 149 } 150 mChannelDataManager.notifyEventDetected(channel, items); 151 } 152 153 public void connect(Uri channelUri) { 154 mHandler.removeCallbacksAndMessages(null); 155 mHandler.obtainMessage(MSG_CONNECT, channelUri).sendToTarget(); 156 } 157 158 public void disconnect() { 159 mHandler.sendEmptyMessage(MSG_DISCONNECT); 160 } 161 162 public void startRecording() { 163 mHandler.sendEmptyMessage(MSG_START_RECORDING); 164 } 165 166 public void stopRecording() { 167 mHandler.sendEmptyMessage(MSG_STOP_RECORDING); 168 } 169 170 public void notifyRecordingFinished(boolean success) { 171 mHandler.obtainMessage(MSG_RECORDING_RESULT, success).sendToTarget(); 172 } 173 174 public void deleteRecording(Uri mediaUri) { 175 mHandler.obtainMessage(MSG_DELETE_RECORDING, mediaUri).sendToTarget(); 176 } 177 178 public void release() { 179 mHandler.removeCallbacksAndMessages(null); 180 mHandler.sendEmptyMessage(MSG_RELEASE); 181 } 182 183 @Override 184 public boolean handleMessage(Message msg) { 185 // TODO: Add RecordStopped status 186 switch (msg.what) { 187 case MSG_CONNECT: { 188 Uri channelUri = (Uri) msg.obj; 189 if (onConnect(channelUri)) { 190 mSession.onTuned(channelUri); 191 } else { 192 Log.w(TAG, "Recording session connect failed"); 193 mSession.onConnectFailed(); 194 } 195 return true; 196 } 197 case MSG_START_RECORDING: { 198 if(onStartRecording()) { 199 Toast.makeText(mContext, "USB TV tuner: Recording started", 200 Toast.LENGTH_SHORT).show(); 201 } 202 else { 203 mSession.onRecordUnexpectedlyStopped(TvInputManager.RECORDING_ERROR_UNKNOWN); 204 } 205 return true; 206 } 207 case MSG_DISCONNECT: { 208 return true; 209 } 210 case MSG_STOP_RECORDING: { 211 onStopRecording(); 212 new Handler(Looper.getMainLooper()).post(new Runnable() { 213 @Override 214 public void run() { 215 Toast.makeText(mContext, "USB TV tuner: Recording stopped", 216 Toast.LENGTH_SHORT).show(); 217 } 218 }); 219 return true; 220 } 221 case MSG_RECORDING_RESULT: { 222 onRecordingResult((Boolean) msg.obj); 223 return true; 224 } 225 case MSG_DELETE_RECORDING: { 226 Uri toDelete = (Uri) msg.obj; 227 onDeleteRecording(toDelete); 228 return true; 229 } 230 case MSG_RELEASE: { 231 onRelease(); 232 return true; 233 } 234 } 235 return false; 236 } 237 238 @Nullable 239 private TunerChannel getChannel(Uri channelUri) { 240 if (channelUri == null) { 241 return null; 242 } 243 long channelId; 244 try { 245 channelId = ContentUris.parseId(channelUri); 246 } catch (UnsupportedOperationException | NumberFormatException e) { 247 channelId = CHANNEL_ID_NONE; 248 } 249 return (channelId == CHANNEL_ID_NONE) ? null : mChannelDataManager.getChannel(channelId); 250 } 251 252 private String getStorageKey() { 253 long prefix = System.currentTimeMillis(); 254 int suffix = mRandom.nextInt(); 255 return String.format(Locale.ENGLISH, "%016x_%016x", prefix, suffix); 256 } 257 258 private File getMediaDir(String storageKey) { 259 return new File(mContext.getCacheDir().getAbsolutePath() + "/recording/" + storageKey); 260 } 261 262 private File getMediaDir(Uri mediaUri) { 263 String mediaPath = mediaUri.getPath(); 264 if (mediaPath == null || mediaPath.length() == 0) { 265 return null; 266 } 267 return new File(mContext.getCacheDir().getAbsolutePath() + "/recording" + 268 mediaUri.getPath()); 269 } 270 271 private void reset() { 272 if (mRecorder != null) { 273 mRecorder.release(); 274 mRecorder = null; 275 } 276 if (mCacheManager != null) { 277 mCacheManager.close(); 278 mCacheManager = null; 279 } 280 if (mTunerSource != null) { 281 mTunerSource.stopStream(); 282 mTunerSource = null; 283 } 284 if (mTunerHal != null) { 285 try { 286 mTunerHal.close(); 287 } catch (Exception ex) { 288 Log.e(TAG, "Error on closing tuner HAL.", ex); 289 } 290 mTunerHal = null; 291 } 292 mSessionState = STATE_IDLE; 293 } 294 295 private void resetRecorder() { 296 Assertions.checkArgument(mSessionState != STATE_IDLE); 297 if (mRecorder != null) { 298 mRecorder.release(); 299 mRecorder = null; 300 } 301 if (mCacheManager != null) { 302 mCacheManager.close(); 303 mCacheManager = null; 304 } 305 if (mTunerSource != null) { 306 mTunerSource.stopStream(); 307 mTunerSource = null; 308 } 309 mSessionState = STATE_CONNECTED; 310 } 311 312 private boolean onConnect(Uri channelUri) { 313 if (mSessionState == STATE_RECORDING) { 314 return false; 315 } 316 mChannel = getChannel(channelUri); 317 if (mChannel == null) { 318 Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel); 319 return false; 320 } 321 if (mSessionState == STATE_CONNECTED) { 322 return true; 323 } 324 mTunerHal = TunerHal.createInstance(mContext); 325 if (mTunerHal == null) { 326 Log.w(TAG, "Failed to start recording. Couldn't open a DVB device"); 327 reset(); 328 return false; 329 } 330 mSessionState = STATE_CONNECTED; 331 return true; 332 } 333 334 private boolean onStartRecording() { 335 if (mSessionState != STATE_CONNECTED) { 336 return false; 337 } 338 mStorageDir = getMediaDir(getStorageKey()); 339 mTunerSource = new UsbTunerDataSource(mTunerHal, this); 340 if (!mTunerSource.tuneToChannel(mChannel)) { 341 Log.w(TAG, "Failed to start recording. Couldn't tune to the channel for " + 342 mChannel.toString()); 343 resetRecorder(); 344 return false; 345 } 346 mCacheManager = new CacheManager(new DvrStorageManager(mStorageDir, true)); 347 mTunerSource.startStream(); 348 mRecordStartTime = System.currentTimeMillis(); 349 mRecorder = new Recorder((MediaDataSource) mTunerSource, 350 mCacheManager, this, this); 351 try { 352 mRecorder.prepare(); 353 } catch (IOException e) { 354 Log.w(TAG, "Failed to start recording. Couldn't prepare a extractor"); 355 resetRecorder(); 356 return false; 357 } 358 mSessionState = STATE_RECORDING; 359 return true; 360 } 361 362 private void onStopRecording() { 363 if (mSessionState != STATE_RECORDING) { 364 return; 365 } 366 // Do not change session status. 367 if (mRecorder != null) { 368 mRecorder.release(); 369 mRecordEndTime = System.currentTimeMillis(); 370 mRecorder = null; 371 } 372 } 373 374 private static class Program { 375 private long mChannelId; 376 private String mTitle; 377 private String mEpisodeTitle; 378 private int mSeasonNumber; 379 private int mEpisodeNumber; 380 private String mDescription; 381 private String mPosterArtUri; 382 private String mThumbnailUri; 383 private String mCanonicalGenres; 384 private String mContentRatings; 385 private long mStartTimeUtcMillis; 386 private long mEndTimeUtcMillis; 387 private long mVideoWidth; 388 private long mVideoHeight; 389 390 private static final String[] PROJECTION = { 391 TvContract.Programs.COLUMN_CHANNEL_ID, 392 TvContract.Programs.COLUMN_TITLE, 393 TvContract.Programs.COLUMN_EPISODE_TITLE, 394 TvContract.Programs.COLUMN_SEASON_NUMBER, 395 TvContract.Programs.COLUMN_EPISODE_NUMBER, 396 TvContract.Programs.COLUMN_SHORT_DESCRIPTION, 397 TvContract.Programs.COLUMN_POSTER_ART_URI, 398 TvContract.Programs.COLUMN_THUMBNAIL_URI, 399 TvContract.Programs.COLUMN_CANONICAL_GENRE, 400 TvContract.Programs.COLUMN_CONTENT_RATING, 401 TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, 402 TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, 403 TvContract.Programs.COLUMN_VIDEO_WIDTH, 404 TvContract.Programs.COLUMN_VIDEO_HEIGHT 405 }; 406 407 public Program(Cursor cursor) { 408 int index = 0; 409 mChannelId = cursor.getLong(index++); 410 mTitle = cursor.getString(index++); 411 mEpisodeTitle = cursor.getString(index++); 412 mSeasonNumber = cursor.getInt(index++); 413 mEpisodeNumber = cursor.getInt(index++); 414 mDescription = cursor.getString(index++); 415 mPosterArtUri = cursor.getString(index++); 416 mThumbnailUri = cursor.getString(index++); 417 mCanonicalGenres = cursor.getString(index++); 418 mContentRatings = cursor.getString(index++); 419 mStartTimeUtcMillis = cursor.getLong(index++); 420 mEndTimeUtcMillis = cursor.getLong(index++); 421 mVideoWidth = cursor.getLong(index++); 422 mVideoHeight = cursor.getLong(index++); 423 } 424 425 public Program(long channelId) { 426 mChannelId = channelId; 427 mTitle = "Unknown"; 428 mEpisodeTitle = ""; 429 mSeasonNumber = 0; 430 mEpisodeNumber = 0; 431 mDescription = "Unknown"; 432 mPosterArtUri = null; 433 mThumbnailUri = null; 434 mCanonicalGenres = null; 435 mContentRatings = null; 436 mStartTimeUtcMillis = 0; 437 mEndTimeUtcMillis = 0; 438 mVideoWidth = 0; 439 mVideoHeight = 0; 440 } 441 442 public static Program onQuery(Cursor c) { 443 Program program = null; 444 if (c != null && c.moveToNext()) { 445 program = new Program(c); 446 } 447 return program; 448 } 449 450 public ContentValues buildValues() { 451 ContentValues values = new ContentValues(); 452 values.put(PROJECTION[0], mChannelId); 453 values.put(PROJECTION[1], mTitle); 454 values.put(PROJECTION[2], mEpisodeTitle); 455 values.put(PROJECTION[3], mSeasonNumber); 456 values.put(PROJECTION[4], mEpisodeNumber); 457 values.put(PROJECTION[5], mDescription); 458 values.put(PROJECTION[6], mPosterArtUri); 459 values.put(PROJECTION[7], mThumbnailUri); 460 values.put(PROJECTION[8], mCanonicalGenres); 461 values.put(PROJECTION[9], mContentRatings); 462 values.put(PROJECTION[10], mStartTimeUtcMillis); 463 values.put(PROJECTION[11], mEndTimeUtcMillis); 464 values.put(PROJECTION[12], mVideoWidth); 465 values.put(PROJECTION[13], mVideoHeight); 466 return values; 467 } 468 } 469 470 private Program getRecordedProgram() { 471 ContentResolver resolver = mContext.getContentResolver(); 472 long avg = mRecordStartTime / 2 + mRecordEndTime / 2; 473 Uri programUri = TvContract.buildProgramsUriForChannel(mChannel.getChannelId(), avg, avg); 474 try (Cursor c = resolver.query(programUri, Program.PROJECTION, null, null, SORT_BY_TIME)) { 475 if (c != null) { 476 Program result = Program.onQuery(c); 477 if (DEBUG) { 478 Log.v(TAG, "Finished query for " + this); 479 } 480 return result; 481 } else { 482 if (c == null) { 483 Log.e(TAG, "Unknown query error for " + this); 484 } else { 485 if (DEBUG) { 486 Log.d(TAG, "Canceled query for " + this); 487 } 488 } 489 return null; 490 } 491 } 492 } 493 494 private Uri insertRecordedProgram(Program program, long channelId, String storageUri, 495 long totalBytes, long startTime, long endTime) { 496 RecordedProgram recordedProgram = RecordedProgram.builder() 497 .setInputId(mInputId) 498 .setChannelId(channelId) 499 .setDataUri(storageUri) 500 .setDurationMillis(endTime - startTime) 501 .setDataBytes(totalBytes) 502 .build(); 503 Uri uri = mContext.getContentResolver().insert(TvContract.RecordedPrograms.CONTENT_URI, 504 RecordedProgram.toValues(recordedProgram)); 505 return uri; 506 } 507 508 private boolean onRecordingResult(boolean success) { 509 if (mSessionState == STATE_RECORDING && success) { 510 Uri uri = insertRecordedProgram(getRecordedProgram(), mChannel.getChannelId(), 511 mStorageDir.toURI().toString(), 1024 * 1024, 512 mRecordStartTime, mRecordEndTime); 513 if (uri != null) { 514 mSession.onRecordFinished(uri); 515 } 516 resetRecorder(); 517 return true; 518 } 519 520 if (mSessionState == STATE_RECORDING) { 521 mSession.onRecordUnexpectedlyStopped(TvInputManager.RECORDING_ERROR_UNKNOWN); 522 Log.w(TAG, "Recording failed: " + mChannel == null ? "" : mChannel.toString()); 523 resetRecorder(); 524 } else { 525 Log.e(TAG, "Recording session status abnormal"); 526 reset(); 527 } 528 return false; 529 } 530 531 private void onDeleteRecording(Uri mediaUri) { 532 // TODO: notify the deletion result to LiveChannels 533 File mediaDir = getMediaDir(mediaUri); 534 if (mediaDir == null) { 535 return; 536 } 537 for(File file: mediaDir.listFiles()) { 538 file.delete(); 539 } 540 mediaDir.delete(); 541 } 542 543 private void onRelease() { 544 // Current recording will be canceled. 545 reset(); 546 mHandler.getLooper().quitSafely(); 547 // TODO: Remove failed recording files. 548 } 549} 550