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