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.tuner.tvinput; 18 19 import static com.android.tv.tuner.features.TunerFeatures.TVPROVIDER_ALLOWS_COLUMN_CREATION; 20 21 import android.content.ContentResolver; 22 import android.content.ContentUris; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.database.Cursor; 26 import android.media.tv.TvContract; 27 import android.media.tv.TvContract.RecordedPrograms; 28 import android.media.tv.TvInputManager; 29 import android.net.Uri; 30 import android.os.AsyncTask; 31 import android.os.Bundle; 32 import android.os.Handler; 33 import android.os.HandlerThread; 34 import android.os.Message; 35 import android.support.annotation.IntDef; 36 import android.support.annotation.MainThread; 37 import android.support.annotation.Nullable; 38 import android.util.Log; 39 import android.util.Pair; 40 41 import androidx.tvprovider.media.tv.Program; 42 43 import com.android.tv.common.BaseApplication; 44 import com.android.tv.common.data.RecordedProgramState; 45 import com.android.tv.common.recording.RecordingCapability; 46 import com.android.tv.common.recording.RecordingStorageStatusManager; 47 import com.android.tv.common.util.CommonUtils; 48 import com.android.tv.tuner.data.PsipData; 49 import com.android.tv.tuner.data.PsipData.EitItem; 50 import com.android.tv.tuner.data.Track.AtscCaptionTrack; 51 import com.android.tv.tuner.data.TunerChannel; 52 import com.android.tv.tuner.dvb.DvbDeviceAccessor; 53 import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor; 54 import com.android.tv.tuner.exoplayer.SampleExtractor; 55 import com.android.tv.tuner.exoplayer.buffer.BufferManager; 56 import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager; 57 import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener; 58 import com.android.tv.tuner.source.TsDataSource; 59 import com.android.tv.tuner.source.TsDataSourceManager; 60 import com.android.tv.tuner.ts.EventDetector.EventListener; 61 import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager; 62 63 import com.google.android.exoplayer.C; 64 import com.google.auto.factory.AutoFactory; 65 import com.google.auto.factory.Provided; 66 67 import java.io.File; 68 import java.io.IOException; 69 import java.lang.annotation.Retention; 70 import java.lang.annotation.RetentionPolicy; 71 import java.util.ArrayList; 72 import java.util.Arrays; 73 import java.util.Collections; 74 import java.util.HashSet; 75 import java.util.List; 76 import java.util.Locale; 77 import java.util.Random; 78 import java.util.Set; 79 import java.util.concurrent.TimeUnit; 80 81 /** Implements a DVR feature. */ 82 public class TunerRecordingSessionWorker 83 implements PlaybackBufferListener, 84 EventListener, 85 SampleExtractor.OnCompletionListener, 86 Handler.Callback { 87 private static final String TAG = "TunerRecordingSessionW"; 88 private static final boolean DEBUG = false; 89 90 private static final String SORT_BY_TIME = 91 TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS 92 + ", " 93 + TvContract.Programs.COLUMN_CHANNEL_ID 94 + ", " 95 + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS; 96 private static final long TUNING_RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4); 97 private static final long STORAGE_MONITOR_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4); 98 private static final long MIN_PARTIAL_RECORDING_DURATION_MS = TimeUnit.SECONDS.toMillis(10); 99 private static final long PREPARE_RECORDER_POLL_MS = 50; 100 private static final int MSG_TUNE = 1; 101 private static final int MSG_START_RECORDING = 2; 102 private static final int MSG_PREPARE_RECODER = 3; 103 private static final int MSG_STOP_RECORDING = 4; 104 private static final int MSG_MONITOR_STORAGE_STATUS = 5; 105 private static final int MSG_RELEASE = 6; 106 private static final int MSG_UPDATE_CC_INFO = 7; 107 private static final int MSG_UPDATE_PARTIAL_STATE = 8; 108 private static final String COLUMN_SERIES_ID = "series_id"; 109 private static final String COLUMN_STATE = "state"; 110 111 private boolean mProgramHasSeriesIdColumn; 112 private boolean mRecordedProgramHasSeriesIdColumn; 113 private boolean mRecordedProgramHasStateColumn; 114 115 private final RecordingCapability mCapabilities; 116 117 private static final String[] PROGRAM_PROJECTION = { 118 TvContract.Programs.COLUMN_CHANNEL_ID, 119 TvContract.Programs.COLUMN_TITLE, 120 TvContract.Programs.COLUMN_SEASON_TITLE, 121 TvContract.Programs.COLUMN_EPISODE_TITLE, 122 TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, 123 TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, 124 TvContract.Programs.COLUMN_SHORT_DESCRIPTION, 125 TvContract.Programs.COLUMN_POSTER_ART_URI, 126 TvContract.Programs.COLUMN_THUMBNAIL_URI, 127 TvContract.Programs.COLUMN_CANONICAL_GENRE, 128 TvContract.Programs.COLUMN_CONTENT_RATING, 129 TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, 130 TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, 131 TvContract.Programs.COLUMN_VIDEO_WIDTH, 132 TvContract.Programs.COLUMN_VIDEO_HEIGHT, 133 TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA 134 }; 135 136 private static final String[] PROGRAM_PROJECTION_WITH_SERIES_ID = 137 createProjectionWithSeriesId(); 138 139 @IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING}) 140 @Retention(RetentionPolicy.SOURCE) 141 public @interface DvrSessionState {} 142 143 private static final int STATE_IDLE = 1; 144 private static final int STATE_TUNING = 2; 145 private static final int STATE_TUNED = 3; 146 private static final int STATE_RECORDING = 4; 147 148 private static final long CHANNEL_ID_NONE = -1; 149 private static final int MAX_TUNING_RETRY = 6; 150 151 private final Context mContext; 152 private final ChannelDataManager mChannelDataManager; 153 private final RecordingStorageStatusManager mRecordingStorageStatusManager; 154 private final Handler mHandler; 155 private final TsDataSourceManager mSourceManager; 156 private final Random mRandom = new Random(); 157 158 private TsDataSource mTunerSource; 159 private TunerChannel mChannel; 160 private File mStorageDir; 161 private long mRecordStartTime; 162 private long mRecordEndTime; 163 private Uri mRecordedProgramUri; 164 private boolean mRecorderRunning; 165 private SampleExtractor mRecorder; 166 private final TunerRecordingSession mSession; 167 @DvrSessionState private int mSessionState = STATE_IDLE; 168 private final String mInputId; 169 private Uri mProgramUri; 170 private String mSeriesId; 171 172 private PsipData.EitItem mCurrenProgram; 173 private List<AtscCaptionTrack> mCaptionTracks; 174 private DvrStorageManager mDvrStorageManager; 175 private final ExoPlayerSampleExtractor.Factory mExoPlayerSampleExtractorFactory; 176 177 /** 178 * Factory for {@link TunerRecordingSessionWorker}}. 179 * 180 * <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory} 181 * generated class. 182 */ 183 public interface Factory { create( Context context, String inputId, ChannelDataManager dataManager, TunerRecordingSession session)184 TunerRecordingSessionWorker create( 185 Context context, 186 String inputId, 187 ChannelDataManager dataManager, 188 TunerRecordingSession session); 189 } 190 191 @AutoFactory(implementing = Factory.class) TunerRecordingSessionWorker( Context context, String inputId, ChannelDataManager dataManager, TunerRecordingSession session, @Provided ExoPlayerSampleExtractor.Factory exoPlayerSampleExtractorFactory, @Provided TsDataSourceManager.Factory tsDataSourceManagerFactory)192 public TunerRecordingSessionWorker( 193 Context context, 194 String inputId, 195 ChannelDataManager dataManager, 196 TunerRecordingSession session, 197 @Provided ExoPlayerSampleExtractor.Factory exoPlayerSampleExtractorFactory, 198 @Provided TsDataSourceManager.Factory tsDataSourceManagerFactory) { 199 mExoPlayerSampleExtractorFactory = exoPlayerSampleExtractorFactory; 200 mRandom.setSeed(System.nanoTime()); 201 mContext = context; 202 HandlerThread handlerThread = new HandlerThread(TAG); 203 handlerThread.start(); 204 mHandler = new Handler(handlerThread.getLooper(), this); 205 mRecordingStorageStatusManager = 206 BaseApplication.getSingletons(context).getRecordingStorageStatusManager(); 207 mChannelDataManager = dataManager; 208 mChannelDataManager.checkDataVersion(context); 209 mSourceManager = tsDataSourceManagerFactory.create(true); 210 mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId); 211 mInputId = inputId; 212 if (DEBUG) Log.d(TAG, mCapabilities.toString()); 213 mSession = session; 214 } 215 216 // PlaybackBufferListener 217 @Override onBufferStartTimeChanged(long startTimeMs)218 public void onBufferStartTimeChanged(long startTimeMs) {} 219 220 @Override onBufferStateChanged(boolean available)221 public void onBufferStateChanged(boolean available) {} 222 223 @Override onDiskTooSlow()224 public void onDiskTooSlow() {} 225 226 // EventDetector.EventListener 227 @Override onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime)228 public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { 229 if (mChannel == null || mChannel.compareTo(channel) != 0) { 230 return; 231 } 232 mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); 233 } 234 235 @Override onEventDetected(TunerChannel channel, List<PsipData.EitItem> items)236 public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) { 237 if (mChannel == null || mChannel.compareTo(channel) != 0) { 238 return; 239 } 240 mHandler.obtainMessage(MSG_UPDATE_CC_INFO, Pair.create(channel, items)).sendToTarget(); 241 mChannelDataManager.notifyEventDetected(channel, items); 242 } 243 244 @Override onChannelScanDone()245 public void onChannelScanDone() { 246 // do nothing. 247 } 248 249 // SampleExtractor.OnCompletionListener 250 @Override onCompletion(boolean success, long lastExtractedPositionUs)251 public void onCompletion(boolean success, long lastExtractedPositionUs) { 252 onRecordingResult(success, lastExtractedPositionUs); 253 reset(); 254 } 255 256 /** Tunes to {@code channelUri}. */ 257 @MainThread tune(Uri channelUri)258 public void tune(Uri channelUri) { 259 mHandler.removeCallbacksAndMessages(null); 260 mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget(); 261 } 262 263 /** Starts recording. */ 264 @MainThread startRecording(@ullable Uri programUri)265 public void startRecording(@Nullable Uri programUri) { 266 mHandler.obtainMessage(MSG_START_RECORDING, programUri).sendToTarget(); 267 } 268 269 /** Stops recording. */ 270 @MainThread stopRecording()271 public void stopRecording() { 272 mHandler.sendEmptyMessage(MSG_STOP_RECORDING); 273 } 274 275 /** Releases all resources. */ 276 @MainThread release()277 public void release() { 278 mHandler.removeCallbacksAndMessages(null); 279 mHandler.sendEmptyMessage(MSG_RELEASE); 280 } 281 282 @Override handleMessage(Message msg)283 public boolean handleMessage(Message msg) { 284 switch (msg.what) { 285 case MSG_TUNE: 286 { 287 Uri channelUri = (Uri) msg.obj; 288 int retryCount = msg.arg1; 289 if (DEBUG) Log.d(TAG, "Tune to " + channelUri); 290 if (doTune(channelUri)) { 291 if (mSessionState == STATE_TUNED) { 292 mSession.onTuned(channelUri); 293 } else { 294 Log.w(TAG, "Tuner stream cannot be created due to resource shortage."); 295 if (retryCount < MAX_TUNING_RETRY) { 296 Message tuneMsg = 297 mHandler.obtainMessage( 298 MSG_TUNE, retryCount + 1, 0, channelUri); 299 mHandler.sendMessageDelayed(tuneMsg, TUNING_RETRY_INTERVAL_MS); 300 } else { 301 mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY); 302 reset(); 303 } 304 } 305 } 306 return true; 307 } 308 case MSG_START_RECORDING: 309 { 310 if (DEBUG) Log.d(TAG, "Start recording"); 311 if (!doStartRecording((Uri) msg.obj)) { 312 reset(); 313 } 314 return true; 315 } 316 case MSG_PREPARE_RECODER: 317 { 318 if (DEBUG) Log.d(TAG, "Preparing recorder"); 319 if (!mRecorderRunning) { 320 return true; 321 } 322 try { 323 if (!mRecorder.prepare()) { 324 mHandler.sendEmptyMessageDelayed( 325 MSG_PREPARE_RECODER, PREPARE_RECORDER_POLL_MS); 326 } 327 } catch (IOException e) { 328 Log.w(TAG, "Failed to start recording. Couldn't prepare an extractor"); 329 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 330 reset(); 331 } 332 return true; 333 } 334 case MSG_STOP_RECORDING: 335 { 336 if (DEBUG) Log.d(TAG, "Stop recording"); 337 if (mSessionState != STATE_RECORDING) { 338 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 339 reset(); 340 return true; 341 } 342 if (mRecorderRunning) { 343 stopRecorder(); 344 } 345 return true; 346 } 347 case MSG_MONITOR_STORAGE_STATUS: 348 { 349 if (mSessionState != STATE_RECORDING) { 350 return true; 351 } 352 if (!mRecordingStorageStatusManager.isStorageSufficient()) { 353 if (mRecorderRunning) { 354 stopRecorder(); 355 } 356 new DeleteRecordingTask().execute(mStorageDir); 357 mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); 358 mContext.getContentResolver().delete(mRecordedProgramUri, null, null); 359 reset(); 360 } else { 361 mHandler.sendEmptyMessageDelayed( 362 MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS); 363 } 364 return true; 365 } 366 case MSG_RELEASE: 367 { 368 // Since release was requested, current recording will be cancelled 369 // without notification. 370 reset(); 371 mSourceManager.release(); 372 mHandler.removeCallbacksAndMessages(null); 373 mHandler.getLooper().quitSafely(); 374 return true; 375 } 376 case MSG_UPDATE_CC_INFO: 377 { 378 Pair<TunerChannel, List<EitItem>> pair = 379 (Pair<TunerChannel, List<EitItem>>) msg.obj; 380 updateCaptionTracks(pair.first, pair.second); 381 return true; 382 } 383 case MSG_UPDATE_PARTIAL_STATE: 384 { 385 updateRecordedProgramStatePartial(); 386 return true; 387 } 388 } 389 return false; 390 } 391 392 @Nullable getChannel(Uri channelUri)393 private TunerChannel getChannel(Uri channelUri) { 394 if (channelUri == null) { 395 return null; 396 } 397 long channelId; 398 try { 399 channelId = ContentUris.parseId(channelUri); 400 } catch (UnsupportedOperationException | NumberFormatException e) { 401 channelId = CHANNEL_ID_NONE; 402 } 403 return (channelId == CHANNEL_ID_NONE) ? null : mChannelDataManager.getChannel(channelId); 404 } 405 getStorageKey()406 private String getStorageKey() { 407 long prefix = System.currentTimeMillis(); 408 int suffix = mRandom.nextInt(); 409 return String.format(Locale.ENGLISH, "%016x_%016x", prefix, suffix); 410 } 411 reset()412 private void reset() { 413 if (mRecorder != null) { 414 mRecorder.release(); 415 mRecorder = null; 416 } 417 if (mTunerSource != null) { 418 mSourceManager.releaseDataSource(mTunerSource); 419 mTunerSource = null; 420 } 421 mDvrStorageManager = null; 422 mSessionState = STATE_IDLE; 423 mRecorderRunning = false; 424 } 425 doTune(Uri channelUri)426 private boolean doTune(Uri channelUri) { 427 if (mSessionState != STATE_IDLE && mSessionState != STATE_TUNING) { 428 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 429 Log.e(TAG, "Tuning was requested from wrong status."); 430 return false; 431 } 432 mChannel = getChannel(channelUri); 433 if (mChannel == null) { 434 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 435 Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel); 436 return false; 437 } else if (mChannel.isRecordingProhibited()) { 438 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 439 Log.w(TAG, "Failed to start recording. Not a recordable channel: " + mChannel); 440 return false; 441 } 442 if (!mRecordingStorageStatusManager.isStorageSufficient()) { 443 mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); 444 Log.w(TAG, "Tuning failed due to insufficient storage."); 445 return false; 446 } 447 mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this); 448 if (mTunerSource == null) { 449 // Retry tuning in this case. 450 mSessionState = STATE_TUNING; 451 return true; 452 } 453 mSessionState = STATE_TUNED; 454 return true; 455 } 456 doStartRecording(@ullable Uri programUri)457 private boolean doStartRecording(@Nullable Uri programUri) { 458 if (mSessionState != STATE_TUNED) { 459 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 460 Log.e(TAG, "Recording session status abnormal"); 461 return false; 462 } 463 mStorageDir = 464 mRecordingStorageStatusManager.isStorageSufficient() 465 ? new File( 466 mRecordingStorageStatusManager.getRecordingRootDataDirectory(), 467 getStorageKey()) 468 : null; 469 if (mStorageDir == null) { 470 mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); 471 Log.w(TAG, "Failed to start recording due to insufficient storage."); 472 return false; 473 } 474 // Since tuning might be happened a while ago, shifts the start position of tuned source. 475 mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition()); 476 mRecordStartTime = System.currentTimeMillis(); 477 mDvrStorageManager = new DvrStorageManager(mStorageDir, true); 478 mRecorder = 479 mExoPlayerSampleExtractorFactory.create( 480 Uri.EMPTY, mTunerSource, new BufferManager(mDvrStorageManager), this, true); 481 mRecorder.setOnCompletionListener(this, mHandler); 482 mProgramUri = programUri; 483 mSessionState = STATE_RECORDING; 484 mRecorderRunning = true; 485 mRecordedProgramUri = 486 insertRecordedProgram( 487 getRecordedProgram(), 488 mChannel.getChannelId(), 489 Uri.fromFile(mStorageDir).toString(), 490 calculateRecordingSizeInBytes(), 491 mRecordStartTime, 492 mRecordStartTime); 493 if (mRecordedProgramUri == null) { 494 new DeleteRecordingTask().execute(mStorageDir); 495 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 496 Log.e(TAG, "Inserting a recording to DB failed"); 497 return false; 498 } 499 mSession.onRecordingUri(mRecordedProgramUri.toString()); 500 mHandler.sendEmptyMessageDelayed( 501 MSG_UPDATE_PARTIAL_STATE, MIN_PARTIAL_RECORDING_DURATION_MS); 502 503 mHandler.sendEmptyMessage(MSG_PREPARE_RECODER); 504 mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS); 505 mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS); 506 return true; 507 } 508 calculateRecordingSizeInBytes()509 private int calculateRecordingSizeInBytes() { 510 // TODO(b/121153491): calcute recording size using mStorageDir 511 return 1024 * 1024; 512 } 513 stopRecorder()514 private void stopRecorder() { 515 // Do not change session status. 516 if (mRecorder != null) { 517 mRecorder.release(); 518 mRecordEndTime = System.currentTimeMillis(); 519 mRecorder = null; 520 } 521 mRecorderRunning = false; 522 mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS); 523 Log.i(TAG, "Recording stopped"); 524 } 525 updateCaptionTracks(TunerChannel channel, List<PsipData.EitItem> items)526 private void updateCaptionTracks(TunerChannel channel, List<PsipData.EitItem> items) { 527 if (mChannel == null 528 || channel == null 529 || mChannel.compareTo(channel) != 0 530 || items == null 531 || items.isEmpty()) { 532 return; 533 } 534 PsipData.EitItem currentProgram = getCurrentProgram(items); 535 if (currentProgram == null 536 || !currentProgram.hasCaptionTrack() 537 || (mCurrenProgram != null && mCurrenProgram.compareTo(currentProgram) == 0)) { 538 return; 539 } 540 mCurrenProgram = currentProgram; 541 mCaptionTracks = new ArrayList<>(currentProgram.getCaptionTracks()); 542 if (DEBUG) { 543 Log.d( 544 TAG, 545 "updated " + mCaptionTracks.size() + " caption tracks for " + currentProgram); 546 } 547 } 548 getCurrentProgram(List<PsipData.EitItem> items)549 private PsipData.EitItem getCurrentProgram(List<PsipData.EitItem> items) { 550 for (PsipData.EitItem item : items) { 551 if (mRecordStartTime >= item.getStartTimeUtcMillis() 552 && mRecordStartTime < item.getEndTimeUtcMillis()) { 553 return item; 554 } 555 } 556 return null; 557 } 558 getRecordedProgram()559 private Program getRecordedProgram() { 560 ContentResolver resolver = mContext.getContentResolver(); 561 Uri programUri = mProgramUri; 562 if (mProgramUri == null) { 563 long avg = mRecordStartTime / 2 + mRecordEndTime / 2; 564 programUri = TvContract.buildProgramsUriForChannel(mChannel.getChannelId(), avg, avg); 565 } 566 String[] projection = 567 checkProgramTable() ? PROGRAM_PROJECTION_WITH_SERIES_ID : PROGRAM_PROJECTION; 568 try (Cursor c = resolver.query(programUri, projection, null, null, SORT_BY_TIME)) { 569 if (c != null && c.moveToNext()) { 570 Program result = Program.fromCursor(c); 571 int index; 572 if ((index = c.getColumnIndex(COLUMN_SERIES_ID)) >= 0 && !c.isNull(index)) { 573 mSeriesId = c.getString(index); 574 } 575 if (DEBUG) { 576 Log.v(TAG, "Finished query for " + this); 577 } 578 return result; 579 } else { 580 if (c == null) { 581 Log.e(TAG, "Unknown query error for " + this); 582 } else { 583 if (DEBUG) Log.d(TAG, "Can not find program:" + programUri); 584 } 585 return null; 586 } 587 } 588 } 589 insertRecordedProgram( Program program, long channelId, String storageUri, long totalBytes, long startTime, long endTime)590 private Uri insertRecordedProgram( 591 Program program, 592 long channelId, 593 String storageUri, 594 long totalBytes, 595 long startTime, 596 long endTime) { 597 ContentValues values = new ContentValues(); 598 values.put(RecordedPrograms.COLUMN_INPUT_ID, mInputId); 599 values.put(RecordedPrograms.COLUMN_CHANNEL_ID, channelId); 600 values.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI, storageUri); 601 values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - startTime); 602 values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes); 603 // startTime could be overridden by program's start value. 604 values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime); 605 values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime); 606 if (checkRecordedProgramTable(COLUMN_SERIES_ID)) { 607 values.put(COLUMN_SERIES_ID, mSeriesId); 608 } 609 if (checkRecordedProgramTable(COLUMN_STATE)) { 610 values.put(COLUMN_STATE, RecordedProgramState.STARTED.name()); 611 } 612 if (program != null) { 613 values.putAll(program.toContentValues()); 614 } 615 return mContext.getContentResolver() 616 .insert(TvContract.RecordedPrograms.CONTENT_URI, values); 617 } 618 updateRecordedProgramStateFinished(long endTime, long totalBytes)619 private void updateRecordedProgramStateFinished(long endTime, long totalBytes) { 620 ContentValues values = new ContentValues(); 621 values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes); 622 values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - mRecordStartTime); 623 values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime); 624 if (checkRecordedProgramTable(COLUMN_STATE)) { 625 values.put(COLUMN_STATE, RecordedProgramState.FINISHED.name()); 626 } 627 mContext.getContentResolver().update(mRecordedProgramUri, values, null, null); 628 } 629 updateRecordedProgramStatePartial()630 private void updateRecordedProgramStatePartial() { 631 mSession.onRecordingStatePartial(mRecordedProgramUri); 632 if (checkRecordedProgramTable(COLUMN_STATE)) { 633 ContentValues values = new ContentValues(); 634 values.put(COLUMN_STATE, RecordedProgramState.PARTIAL.name()); 635 mContext.getContentResolver().update(mRecordedProgramUri, values, null, null); 636 } 637 } 638 onRecordingResult(boolean success, long lastExtractedPositionUs)639 private void onRecordingResult(boolean success, long lastExtractedPositionUs) { 640 if (mSessionState != STATE_RECORDING) { 641 // Error notification is not needed. 642 Log.e(TAG, "Recording session status abnormal"); 643 return; 644 } 645 if (mRecorderRunning) { 646 // In case of recorder not being stopped, because of premature termination of recording. 647 stopRecorder(); 648 } 649 if (!success 650 && lastExtractedPositionUs 651 < TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) { 652 new DeleteRecordingTask().execute(mStorageDir); 653 mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); 654 mContext.getContentResolver().delete(mRecordedProgramUri, null, null); 655 Log.w(TAG, "Recording failed during recording"); 656 return; 657 } 658 Log.i(TAG, "recording finished " + (success ? "completely" : "partially")); 659 long recordEndTime = 660 (lastExtractedPositionUs == C.UNKNOWN_TIME_US) 661 ? System.currentTimeMillis() 662 : mRecordStartTime + lastExtractedPositionUs / 1000; 663 updateRecordedProgramStateFinished(recordEndTime, calculateRecordingSizeInBytes()); 664 mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks); 665 mSession.onRecordFinished(mRecordedProgramUri); 666 } 667 checkProgramTable()668 private boolean checkProgramTable() { 669 boolean canCreateColumn = TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(mContext); 670 if (!canCreateColumn) { 671 return false; 672 } 673 Uri uri = TvContract.Programs.CONTENT_URI; 674 if (!mProgramHasSeriesIdColumn) { 675 if (getExistingColumns(uri).contains(COLUMN_SERIES_ID)) { 676 mProgramHasSeriesIdColumn = true; 677 } else if (addColumnToTable(uri, COLUMN_SERIES_ID)) { 678 mProgramHasSeriesIdColumn = true; 679 } 680 } 681 return mProgramHasSeriesIdColumn; 682 } 683 checkRecordedProgramTable(String column)684 private boolean checkRecordedProgramTable(String column) { 685 boolean canCreateColumn = TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(mContext); 686 if (!canCreateColumn) { 687 return false; 688 } 689 Uri uri = TvContract.RecordedPrograms.CONTENT_URI; 690 switch (column) { 691 case COLUMN_SERIES_ID: 692 { 693 if (!mRecordedProgramHasSeriesIdColumn) { 694 if (getExistingColumns(uri).contains(COLUMN_SERIES_ID)) { 695 mRecordedProgramHasSeriesIdColumn = true; 696 } else if (addColumnToTable(uri, COLUMN_SERIES_ID)) { 697 mRecordedProgramHasSeriesIdColumn = true; 698 } 699 } 700 return mRecordedProgramHasSeriesIdColumn; 701 } 702 case COLUMN_STATE: 703 { 704 if (!mRecordedProgramHasStateColumn) { 705 if (getExistingColumns(uri).contains(COLUMN_STATE)) { 706 mRecordedProgramHasStateColumn = true; 707 } else if (addColumnToTable(uri, COLUMN_STATE)) { 708 mRecordedProgramHasStateColumn = true; 709 } 710 } 711 return mRecordedProgramHasStateColumn; 712 } 713 default: 714 return false; 715 } 716 } 717 getExistingColumns(Uri uri)718 private Set<String> getExistingColumns(Uri uri) { 719 Bundle result = 720 mContext.getContentResolver() 721 .call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null); 722 if (result != null) { 723 String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES); 724 if (columns != null) { 725 return new HashSet<>(Arrays.asList(columns)); 726 } 727 } 728 Log.e(TAG, "Query existing column names from " + uri + " returned null"); 729 return Collections.emptySet(); 730 } 731 732 /** 733 * Add a column to the table 734 * 735 * @return {@code true} if the column is added successfully; {@code false} otherwise. 736 */ addColumnToTable(Uri contentUri, String columnName)737 private boolean addColumnToTable(Uri contentUri, String columnName) { 738 Bundle extra = new Bundle(); 739 extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName); 740 extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT"); 741 // If the add operation fails, the following just returns null without crashing. 742 Bundle allColumns = 743 mContext.getContentResolver() 744 .call( 745 contentUri, 746 TvContract.METHOD_ADD_COLUMN, 747 contentUri.toString(), 748 extra); 749 if (allColumns == null) { 750 Log.w(TAG, "Adding new column failed. Uri=" + contentUri); 751 } 752 return allColumns != null; 753 } 754 createProjectionWithSeriesId()755 private static String[] createProjectionWithSeriesId() { 756 List<String> projectionList = new ArrayList<>(Arrays.asList(PROGRAM_PROJECTION)); 757 projectionList.add(COLUMN_SERIES_ID); 758 return projectionList.toArray(new String[0]); 759 } 760 761 private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> { 762 763 @Override doInBackground(File... files)764 public Void doInBackground(File... files) { 765 if (files == null || files.length == 0) { 766 return null; 767 } 768 for (File file : files) { 769 if (!CommonUtils.deleteDirOrFile(file)) { 770 Log.w(TAG, "Unable to delete recording data at " + file); 771 } 772 } 773 return null; 774 } 775 } 776 } 777