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.tv.tuner.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.tv.TvContract; 25import android.media.tv.TvInputManager; 26import android.net.Uri; 27import android.os.AsyncTask; 28import android.os.Handler; 29import android.os.HandlerThread; 30import android.os.Message; 31import android.support.annotation.IntDef; 32import android.support.annotation.MainThread; 33import android.support.annotation.Nullable; 34import android.util.Log; 35 36import android.util.Pair; 37import com.google.android.exoplayer.C; 38import com.android.tv.TvApplication; 39import com.android.tv.common.SoftPreconditions; 40import com.android.tv.common.recording.RecordingCapability; 41import com.android.tv.dvr.DvrStorageStatusManager; 42import com.android.tv.dvr.data.RecordedProgram; 43import com.android.tv.tuner.DvbDeviceAccessor; 44import com.android.tv.tuner.data.PsipData; 45import com.android.tv.tuner.data.PsipData.EitItem; 46import com.android.tv.tuner.data.TunerChannel; 47import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; 48import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor; 49import com.android.tv.tuner.exoplayer.SampleExtractor; 50import com.android.tv.tuner.exoplayer.buffer.BufferManager; 51import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager; 52import com.android.tv.tuner.source.TsDataSource; 53import com.android.tv.tuner.source.TsDataSourceManager; 54import com.android.tv.util.Utils; 55 56import java.io.File; 57import java.io.IOException; 58import java.lang.annotation.Retention; 59import java.lang.annotation.RetentionPolicy; 60import java.util.ArrayList; 61import java.util.List; 62import java.util.Locale; 63import java.util.Random; 64import java.util.concurrent.TimeUnit; 65 66/** 67 * Implements a DVR feature. 68 */ 69public class TunerRecordingSessionWorker implements PlaybackBufferListener, 70 EventDetector.EventListener, SampleExtractor.OnCompletionListener, 71 Handler.Callback { 72 private static final String TAG = "TunerRecordingSessionW"; 73 private static final boolean DEBUG = false; 74 75 private static final String SORT_BY_TIME = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS 76 + ", " + TvContract.Programs.COLUMN_CHANNEL_ID + ", " 77 + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS; 78 private static final long TUNING_RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4); 79 private static final long STORAGE_MONITOR_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4); 80 private static final long MIN_PARTIAL_RECORDING_DURATION_MS = TimeUnit.SECONDS.toMillis(10); 81 private static final long PREPARE_RECORDER_POLL_MS = 50; 82 private static final int MSG_TUNE = 1; 83 private static final int MSG_START_RECORDING = 2; 84 private static final int MSG_PREPARE_RECODER = 3; 85 private static final int MSG_STOP_RECORDING = 4; 86 private static final int MSG_MONITOR_STORAGE_STATUS = 5; 87 private static final int MSG_RELEASE = 6; 88 private static final int MSG_UPDATE_CC_INFO = 7; 89 private final RecordingCapability mCapabilities; 90 91 public RecordingCapability getCapabilities() { 92 return mCapabilities; 93 } 94 95 @IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING}) 96 @Retention(RetentionPolicy.SOURCE) 97 public @interface DvrSessionState {} 98 private static final int STATE_IDLE = 1; 99 private static final int STATE_TUNING = 2; 100 private static final int STATE_TUNED = 3; 101 private static final int STATE_RECORDING = 4; 102 103 private static final long CHANNEL_ID_NONE = -1; 104 private static final int MAX_TUNING_RETRY = 6; 105 106 private final Context mContext; 107 private final ChannelDataManager mChannelDataManager; 108 private final DvrStorageStatusManager mDvrStorageStatusManager; 109 private final Handler mHandler; 110 private final TsDataSourceManager mSourceManager; 111 private final Random mRandom = new Random(); 112 113 private TsDataSource mTunerSource; 114 private TunerChannel mChannel; 115 private File mStorageDir; 116 private long mRecordStartTime; 117 private long mRecordEndTime; 118 private boolean mRecorderRunning; 119 private SampleExtractor mRecorder; 120 private final TunerRecordingSession mSession; 121 @DvrSessionState private int mSessionState = STATE_IDLE; 122 private final String mInputId; 123 private Uri mProgramUri; 124 125 private PsipData.EitItem mCurrenProgram; 126 private List<AtscCaptionTrack> mCaptionTracks; 127 private DvrStorageManager mDvrStorageManager; 128 129 public TunerRecordingSessionWorker(Context context, String inputId, 130 ChannelDataManager dataManager, TunerRecordingSession session) { 131 mRandom.setSeed(System.nanoTime()); 132 mContext = context; 133 HandlerThread handlerThread = new HandlerThread(TAG); 134 handlerThread.start(); 135 mHandler = new Handler(handlerThread.getLooper(), this); 136 mDvrStorageStatusManager = 137 TvApplication.getSingletons(context).getDvrStorageStatusManager(); 138 mChannelDataManager = dataManager; 139 mChannelDataManager.checkDataVersion(context); 140 mSourceManager = TsDataSourceManager.createSourceManager(true); 141 mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId); 142 mInputId = inputId; 143 if (DEBUG) Log.d(TAG, mCapabilities.toString()); 144 mSession = session; 145 } 146 147 // PlaybackBufferListener 148 @Override 149 public void onBufferStartTimeChanged(long startTimeMs) { } 150 151 @Override 152 public void onBufferStateChanged(boolean available) { } 153 154 @Override 155 public void onDiskTooSlow() { } 156 157 // EventDetector.EventListener 158 @Override 159 public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { 160 if (mChannel == null || mChannel.compareTo(channel) != 0) { 161 return; 162 } 163 mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); 164 } 165 166 @Override 167 public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) { 168 if (mChannel == null || mChannel.compareTo(channel) != 0) { 169 return; 170 } 171 mHandler.obtainMessage(MSG_UPDATE_CC_INFO, new Pair<>(channel, items)).sendToTarget(); 172 mChannelDataManager.notifyEventDetected(channel, items); 173 } 174 175 @Override 176 public void onChannelScanDone() { 177 // do nothing. 178 } 179 180 // SampleExtractor.OnCompletionListener 181 @Override 182 public void onCompletion(boolean success, long lastExtractedPositionUs) { 183 onRecordingResult(success, lastExtractedPositionUs); 184 reset(); 185 } 186 187 /** 188 * Tunes to {@code channelUri}. 189 */ 190 @MainThread 191 public void tune(Uri channelUri) { 192 mHandler.removeCallbacksAndMessages(null); 193 mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget(); 194 } 195 196 /** 197 * Starts recording. 198 */ 199 @MainThread 200 public void startRecording(@Nullable Uri programUri) { 201 mHandler.obtainMessage(MSG_START_RECORDING, programUri).sendToTarget(); 202 } 203 204 /** 205 * Stops recording. 206 */ 207 @MainThread 208 public void stopRecording() { 209 mHandler.sendEmptyMessage(MSG_STOP_RECORDING); 210 } 211 212 /** 213 * Releases all resources. 214 */ 215 @MainThread 216 public void release() { 217 mHandler.removeCallbacksAndMessages(null); 218 mHandler.sendEmptyMessage(MSG_RELEASE); 219 } 220 221 @Override 222 public boolean handleMessage(Message msg) { 223 switch (msg.what) { 224 case MSG_TUNE: { 225 Uri channelUri = (Uri) msg.obj; 226 int retryCount = msg.arg1; 227 if (DEBUG) Log.d(TAG, "Tune to " + channelUri); 228 if (doTune(channelUri)) { 229 if (mSessionState == STATE_TUNED) { 230 mSession.onTuned(channelUri); 231 } else { 232 Log.w(TAG, "Tuner stream cannot be created due to resource shortage."); 233 if (retryCount < MAX_TUNING_RETRY) { 234 Message tuneMsg = 235 mHandler.obtainMessage(MSG_TUNE, retryCount + 1, 0, channelUri); 236 mHandler.sendMessageDelayed(tuneMsg, TUNING_RETRY_INTERVAL_MS); 237 } else { 238 mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY); 239 reset(); 240 } 241 } 242 } 243 return true; 244 } 245 case MSG_START_RECORDING: { 246 if (DEBUG) Log.d(TAG, "Start recording"); 247 if (!doStartRecording((Uri) msg.obj)) { 248 reset(); 249 } 250 return true; 251 } 252 case MSG_PREPARE_RECODER: { 253 if (DEBUG) Log.d(TAG, "Preparing recorder"); 254 if (!mRecorderRunning) { 255 return true; 256 } 257 try { 258 if (!mRecorder.prepare()) { 259 mHandler.sendEmptyMessageDelayed(MSG_PREPARE_RECODER, 260 PREPARE_RECORDER_POLL_MS); 261 } 262 } catch (IOException e) { 263 Log.w(TAG, "Failed to start recording. Couldn't prepare an extractor"); 264 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 265 reset(); 266 } 267 return true; 268 } 269 case MSG_STOP_RECORDING: { 270 if (DEBUG) Log.d(TAG, "Stop recording"); 271 if (mSessionState != STATE_RECORDING) { 272 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 273 reset(); 274 return true; 275 } 276 if (mRecorderRunning) { 277 stopRecorder(); 278 } 279 return true; 280 } 281 case MSG_MONITOR_STORAGE_STATUS: { 282 if (mSessionState != STATE_RECORDING) { 283 return true; 284 } 285 if (!mDvrStorageStatusManager.isStorageSufficient()) { 286 if (mRecorderRunning) { 287 stopRecorder(); 288 } 289 new DeleteRecordingTask().execute(mStorageDir); 290 mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); 291 reset(); 292 } else { 293 mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, 294 STORAGE_MONITOR_INTERVAL_MS); 295 } 296 return true; 297 } 298 case MSG_RELEASE: { 299 // Since release was requested, current recording will be cancelled 300 // without notification. 301 reset(); 302 mSourceManager.release(); 303 mHandler.removeCallbacksAndMessages(null); 304 mHandler.getLooper().quitSafely(); 305 return true; 306 } 307 case MSG_UPDATE_CC_INFO: { 308 Pair<TunerChannel, List<EitItem>> pair = 309 (Pair<TunerChannel, List<EitItem>>) msg.obj; 310 updateCaptionTracks(pair.first, pair.second); 311 return true; 312 } 313 } 314 return false; 315 } 316 317 @Nullable 318 private TunerChannel getChannel(Uri channelUri) { 319 if (channelUri == null) { 320 return null; 321 } 322 long channelId; 323 try { 324 channelId = ContentUris.parseId(channelUri); 325 } catch (UnsupportedOperationException | NumberFormatException e) { 326 channelId = CHANNEL_ID_NONE; 327 } 328 return (channelId == CHANNEL_ID_NONE) ? null : mChannelDataManager.getChannel(channelId); 329 } 330 331 private String getStorageKey() { 332 long prefix = System.currentTimeMillis(); 333 int suffix = mRandom.nextInt(); 334 return String.format(Locale.ENGLISH, "%016x_%016x", prefix, suffix); 335 } 336 337 private void reset() { 338 if (mRecorder != null) { 339 mRecorder.release(); 340 mRecorder = null; 341 } 342 if (mTunerSource != null) { 343 mSourceManager.releaseDataSource(mTunerSource); 344 mTunerSource = null; 345 } 346 mDvrStorageManager = null; 347 mSessionState = STATE_IDLE; 348 mRecorderRunning = false; 349 } 350 351 private boolean doTune(Uri channelUri) { 352 if (mSessionState != STATE_IDLE && mSessionState != STATE_TUNING) { 353 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 354 Log.e(TAG, "Tuning was requested from wrong status."); 355 return false; 356 } 357 mChannel = getChannel(channelUri); 358 if (mChannel == null) { 359 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 360 Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel); 361 return false; 362 } else if (mChannel.isRecordingProhibited()) { 363 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 364 Log.w(TAG, "Failed to start recording. Not a recordable channel: " + mChannel); 365 return false; 366 } 367 if (!mDvrStorageStatusManager.isStorageSufficient()) { 368 mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); 369 Log.w(TAG, "Tuning failed due to insufficient storage."); 370 return false; 371 } 372 mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this); 373 if (mTunerSource == null) { 374 // Retry tuning in this case. 375 mSessionState = STATE_TUNING; 376 return true; 377 } 378 mSessionState = STATE_TUNED; 379 return true; 380 } 381 382 private boolean doStartRecording(@Nullable Uri programUri) { 383 if (mSessionState != STATE_TUNED) { 384 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 385 Log.e(TAG, "Recording session status abnormal"); 386 return false; 387 } 388 mStorageDir = mDvrStorageStatusManager.isStorageSufficient() ? 389 new File(mDvrStorageStatusManager.getRecordingRootDataDirectory(), 390 getStorageKey()) : null; 391 if (mStorageDir == null) { 392 mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); 393 Log.w(TAG, "Failed to start recording due to insufficient storage."); 394 return false; 395 } 396 // Since tuning might be happened a while ago, shifts the start position of tuned source. 397 mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition()); 398 mRecordStartTime = System.currentTimeMillis(); 399 mDvrStorageManager = new DvrStorageManager(mStorageDir, true); 400 mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource, 401 new BufferManager(mDvrStorageManager), this, true); 402 mRecorder.setOnCompletionListener(this, mHandler); 403 mProgramUri = programUri; 404 mSessionState = STATE_RECORDING; 405 mRecorderRunning = true; 406 mHandler.sendEmptyMessage(MSG_PREPARE_RECODER); 407 mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS); 408 mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, 409 STORAGE_MONITOR_INTERVAL_MS); 410 return true; 411 } 412 413 private void stopRecorder() { 414 // Do not change session status. 415 if (mRecorder != null) { 416 mRecorder.release(); 417 mRecordEndTime = System.currentTimeMillis(); 418 mRecorder = null; 419 } 420 mRecorderRunning = false; 421 mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS); 422 Log.i(TAG, "Recording stopped"); 423 } 424 425 private void updateCaptionTracks(TunerChannel channel, List<PsipData.EitItem> items) { 426 if (mChannel == null || channel == null || mChannel.compareTo(channel) != 0 427 || items == null || items.isEmpty()) { 428 return; 429 } 430 PsipData.EitItem currentProgram = getCurrentProgram(items); 431 if (currentProgram == null || !currentProgram.hasCaptionTrack() 432 || mCurrenProgram != null && mCurrenProgram.compareTo(currentProgram) == 0) { 433 return; 434 } 435 mCurrenProgram = currentProgram; 436 mCaptionTracks = new ArrayList<>(currentProgram.getCaptionTracks()); 437 if (DEBUG) { 438 Log.d(TAG, "updated " + mCaptionTracks.size() + " caption tracks for " 439 + currentProgram); 440 } 441 } 442 443 private PsipData.EitItem getCurrentProgram(List<PsipData.EitItem> items) { 444 for (PsipData.EitItem item : items) { 445 if (mRecordStartTime >= item.getStartTimeUtcMillis() 446 && mRecordStartTime < item.getEndTimeUtcMillis()) { 447 return item; 448 } 449 } 450 return null; 451 } 452 453 private static class Program { 454 private final long mChannelId; 455 private final String mTitle; 456 private String mSeriesId; 457 private final String mSeasonTitle; 458 private final String mEpisodeTitle; 459 private final String mSeasonNumber; 460 private final String mEpisodeNumber; 461 private final String mDescription; 462 private final String mPosterArtUri; 463 private final String mThumbnailUri; 464 private final String mCanonicalGenres; 465 private final String mContentRatings; 466 private final long mStartTimeUtcMillis; 467 private final long mEndTimeUtcMillis; 468 private final int mVideoWidth; 469 private final int mVideoHeight; 470 private final byte[] mInternalProviderData; 471 472 private static final String[] PROJECTION = { 473 TvContract.Programs.COLUMN_CHANNEL_ID, 474 TvContract.Programs.COLUMN_TITLE, 475 TvContract.Programs.COLUMN_SEASON_TITLE, 476 TvContract.Programs.COLUMN_EPISODE_TITLE, 477 TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, 478 TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, 479 TvContract.Programs.COLUMN_SHORT_DESCRIPTION, 480 TvContract.Programs.COLUMN_POSTER_ART_URI, 481 TvContract.Programs.COLUMN_THUMBNAIL_URI, 482 TvContract.Programs.COLUMN_CANONICAL_GENRE, 483 TvContract.Programs.COLUMN_CONTENT_RATING, 484 TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, 485 TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, 486 TvContract.Programs.COLUMN_VIDEO_WIDTH, 487 TvContract.Programs.COLUMN_VIDEO_HEIGHT, 488 TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA 489 }; 490 491 public Program(Cursor cursor) { 492 int index = 0; 493 mChannelId = cursor.getLong(index++); 494 mTitle = cursor.getString(index++); 495 mSeasonTitle = cursor.getString(index++); 496 mEpisodeTitle = cursor.getString(index++); 497 mSeasonNumber = cursor.getString(index++); 498 mEpisodeNumber = cursor.getString(index++); 499 mDescription = cursor.getString(index++); 500 mPosterArtUri = cursor.getString(index++); 501 mThumbnailUri = cursor.getString(index++); 502 mCanonicalGenres = cursor.getString(index++); 503 mContentRatings = cursor.getString(index++); 504 mStartTimeUtcMillis = cursor.getLong(index++); 505 mEndTimeUtcMillis = cursor.getLong(index++); 506 mVideoWidth = cursor.getInt(index++); 507 mVideoHeight = cursor.getInt(index++); 508 mInternalProviderData = cursor.getBlob(index++); 509 SoftPreconditions.checkArgument(index == PROJECTION.length); 510 } 511 512 public Program(long channelId) { 513 mChannelId = channelId; 514 mTitle = "Unknown"; 515 mSeasonTitle = ""; 516 mEpisodeTitle = ""; 517 mSeasonNumber = ""; 518 mEpisodeNumber = ""; 519 mDescription = "Unknown"; 520 mPosterArtUri = null; 521 mThumbnailUri = null; 522 mCanonicalGenres = null; 523 mContentRatings = null; 524 mStartTimeUtcMillis = 0; 525 mEndTimeUtcMillis = 0; 526 mVideoWidth = 0; 527 mVideoHeight = 0; 528 mInternalProviderData = null; 529 } 530 531 public static Program onQuery(Cursor c) { 532 Program program = null; 533 if (c != null && c.moveToNext()) { 534 program = new Program(c); 535 } 536 return program; 537 } 538 539 public ContentValues buildValues() { 540 ContentValues values = new ContentValues(); 541 int index = 0; 542 values.put(PROJECTION[index++], mChannelId); 543 values.put(PROJECTION[index++], mTitle); 544 values.put(PROJECTION[index++], mSeasonTitle); 545 values.put(PROJECTION[index++], mEpisodeTitle); 546 values.put(PROJECTION[index++], mSeasonNumber); 547 values.put(PROJECTION[index++], mEpisodeNumber); 548 values.put(PROJECTION[index++], mDescription); 549 values.put(PROJECTION[index++], mPosterArtUri); 550 values.put(PROJECTION[index++], mThumbnailUri); 551 values.put(PROJECTION[index++], mCanonicalGenres); 552 values.put(PROJECTION[index++], mContentRatings); 553 values.put(PROJECTION[index++], mStartTimeUtcMillis); 554 values.put(PROJECTION[index++], mEndTimeUtcMillis); 555 values.put(PROJECTION[index++], mVideoWidth); 556 values.put(PROJECTION[index++], mVideoHeight); 557 values.put(PROJECTION[index++], mInternalProviderData); 558 SoftPreconditions.checkArgument(index == PROJECTION.length); 559 return values; 560 } 561 } 562 563 private Program getRecordedProgram() { 564 ContentResolver resolver = mContext.getContentResolver(); 565 Uri programUri = mProgramUri; 566 if (mProgramUri == null) { 567 long avg = mRecordStartTime / 2 + mRecordEndTime / 2; 568 programUri = TvContract.buildProgramsUriForChannel(mChannel.getChannelId(), avg, avg); 569 } 570 try (Cursor c = resolver.query(programUri, Program.PROJECTION, null, null, SORT_BY_TIME)) { 571 if (c != null) { 572 Program result = Program.onQuery(c); 573 if (DEBUG) { 574 Log.v(TAG, "Finished query for " + this); 575 } 576 return result; 577 } else { 578 if (c == null) { 579 Log.e(TAG, "Unknown query error for " + this); 580 } else { 581 if (DEBUG) Log.d(TAG, "Canceled query for " + this); 582 } 583 return null; 584 } 585 } 586 } 587 588 private Uri insertRecordedProgram(Program program, long channelId, String storageUri, 589 long totalBytes, long startTime, long endTime) { 590 // TODO: Set title even though program is null. 591 RecordedProgram recordedProgram = RecordedProgram.builder() 592 .setInputId(mInputId) 593 .setChannelId(channelId) 594 .setDataUri(storageUri) 595 .setDurationMillis(endTime - startTime) 596 .setDataBytes(totalBytes) 597 // startTime and endTime could be overridden by program's start and end value. 598 .setStartTimeUtcMillis(startTime) 599 .setEndTimeUtcMillis(endTime) 600 .build(); 601 ContentValues values = RecordedProgram.toValues(recordedProgram); 602 if (program != null) { 603 values.putAll(program.buildValues()); 604 } 605 return mContext.getContentResolver().insert(TvContract.RecordedPrograms.CONTENT_URI, 606 values); 607 } 608 609 private void onRecordingResult(boolean success, long lastExtractedPositionUs) { 610 if (mSessionState != STATE_RECORDING) { 611 // Error notification is not needed. 612 Log.e(TAG, "Recording session status abnormal"); 613 return; 614 } 615 if (mRecorderRunning) { 616 // In case of recorder not being stopped, because of premature termination of recording. 617 stopRecorder(); 618 } 619 if (!success && lastExtractedPositionUs < 620 TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) { 621 new DeleteRecordingTask().execute(mStorageDir); 622 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 623 Log.w(TAG, "Recording failed during recording"); 624 return; 625 } 626 Log.i(TAG, "recording finished " + (success ? "completely" : "partially")); 627 long recordEndTime = 628 (lastExtractedPositionUs == C.UNKNOWN_TIME_US) 629 ? System.currentTimeMillis() 630 : mRecordStartTime + lastExtractedPositionUs / 1000; 631 Uri uri = 632 insertRecordedProgram( 633 getRecordedProgram(), 634 mChannel.getChannelId(), 635 Uri.fromFile(mStorageDir).toString(), 636 1024 * 1024, 637 mRecordStartTime, 638 recordEndTime); 639 if (uri == null) { 640 new DeleteRecordingTask().execute(mStorageDir); 641 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 642 Log.e(TAG, "Inserting a recording to DB failed"); 643 return; 644 } 645 mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks); 646 mSession.onRecordFinished(uri); 647 } 648 649 private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> { 650 651 @Override 652 public Void doInBackground(File... files) { 653 if (files == null || files.length == 0) { 654 return null; 655 } 656 for(File file : files) { 657 Utils.deleteDirOrFile(file); 658 } 659 return null; 660 } 661 } 662} 663