blob: 9481d1ae61fb4888d63df603bf7c6f12c40c7aa4 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.tuner.tvinput;
import static com.android.tv.tuner.features.TunerFeatures.TVPROVIDER_ALLOWS_COLUMN_CREATION;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.media.tv.TvContract;
import android.media.tv.TvContract.RecordedPrograms;
import android.media.tv.TvInputManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import androidx.annotation.IntDef;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import android.util.Log;
import android.util.Pair;
import androidx.tvprovider.media.tv.Program;
import com.android.tv.common.BaseApplication;
import com.android.tv.common.data.RecordedProgramState;
import com.android.tv.common.recording.RecordingCapability;
import com.android.tv.common.recording.RecordingStorageStatusManager;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.tuner.data.PsipData;
import com.android.tv.tuner.data.PsipData.EitItem;
import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.dvb.DvbDeviceAccessor;
import com.android.tv.tuner.exoplayer2.ExoPlayerSampleExtractor;
import com.android.tv.tuner.exoplayer2.SampleExtractor;
import com.android.tv.tuner.exoplayer2.buffer.BufferManager;
import com.android.tv.tuner.exoplayer2.buffer.DvrStorageManager;
import com.android.tv.tuner.exoplayer2.buffer.PlaybackBufferListener;
import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsDataSourceManager;
import com.android.tv.tuner.ts.EventDetector.EventListener;
import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
import com.google.android.exoplayer2.C;
import com.google.auto.factory.AutoFactory;
import com.google.auto.factory.Provided;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/** Implements a DVR feature. */
public class TunerRecordingSessionWorkerExoV2
implements PlaybackBufferListener,
EventListener,
SampleExtractor.OnCompletionListener,
SampleExtractor.Callback,
Handler.Callback {
private static final String TAG = "TunerRecordingSWExoV2";
private static final boolean DEBUG = false;
private static final String SORT_BY_TIME =
TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS
+ ", "
+ TvContract.Programs.COLUMN_CHANNEL_ID
+ ", "
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS;
private static final long TUNING_RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4);
private static final long STORAGE_MONITOR_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4);
private static final long MIN_PARTIAL_RECORDING_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
private static final long PREPARE_RECORDER_POLL_MS = 50;
private static final int MSG_TUNE = 1;
private static final int MSG_START_RECORDING = 2;
private static final int MSG_PREPARE_RECODER = 3;
private static final int MSG_STOP_RECORDING = 4;
private static final int MSG_MONITOR_STORAGE_STATUS = 5;
private static final int MSG_RELEASE = 6;
private static final int MSG_UPDATE_CC_INFO = 7;
private static final int MSG_UPDATE_PARTIAL_STATE = 8;
private static final String COLUMN_SERIES_ID = "series_id";
private static final String COLUMN_STATE = "state";
private boolean mProgramHasSeriesIdColumn;
private boolean mRecordedProgramHasSeriesIdColumn;
private boolean mRecordedProgramHasStateColumn;
private final RecordingCapability mCapabilities;
private static final String[] PROGRAM_PROJECTION = {
TvContract.Programs.COLUMN_CHANNEL_ID,
TvContract.Programs.COLUMN_TITLE,
TvContract.Programs.COLUMN_SEASON_TITLE,
TvContract.Programs.COLUMN_EPISODE_TITLE,
TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER,
TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
TvContract.Programs.COLUMN_POSTER_ART_URI,
TvContract.Programs.COLUMN_THUMBNAIL_URI,
TvContract.Programs.COLUMN_CANONICAL_GENRE,
TvContract.Programs.COLUMN_CONTENT_RATING,
TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
TvContract.Programs.COLUMN_VIDEO_WIDTH,
TvContract.Programs.COLUMN_VIDEO_HEIGHT,
TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA
};
private static final String[] PROGRAM_PROJECTION_WITH_SERIES_ID =
createProjectionWithSeriesId();
@IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING})
@Retention(RetentionPolicy.SOURCE)
public @interface DvrSessionState {}
private static final int STATE_IDLE = 1;
private static final int STATE_TUNING = 2;
private static final int STATE_TUNED = 3;
private static final int STATE_RECORDING = 4;
private static final long CHANNEL_ID_NONE = -1;
private static final int MAX_TUNING_RETRY = 6;
private final Context mContext;
private final ChannelDataManager mChannelDataManager;
private final RecordingStorageStatusManager mRecordingStorageStatusManager;
private final Handler mHandler;
private final TsDataSourceManager mSourceManager;
private final Random mRandom = new Random();
private TsDataSource mTunerSource;
private TunerChannel mChannel;
private File mStorageDir;
private long mRecordStartTime;
private long mRecordEndTime;
private Uri mRecordedProgramUri;
private boolean mRecorderRunning;
private SampleExtractor mRecorder;
private final TunerRecordingSessionExoV2 mSession;
@DvrSessionState private int mSessionState = STATE_IDLE;
private final String mInputId;
private Uri mProgramUri;
private String mSeriesId;
private PsipData.EitItem mCurrenProgram;
private List<AtscCaptionTrack> mCaptionTracks;
private DvrStorageManager mDvrStorageManager;
private final ExoPlayerSampleExtractor.Factory mExoPlayerSampleExtractorFactory;
/**
* Factory for {@link TunerRecordingSessionWorkerExoV2}}.
*
* <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory}
* generated class.
*/
public interface Factory {
TunerRecordingSessionWorkerExoV2 create(
Context context,
String inputId,
ChannelDataManager dataManager,
TunerRecordingSessionExoV2 session);
}
@AutoFactory(implementing = Factory.class)
public TunerRecordingSessionWorkerExoV2(
Context context,
String inputId,
ChannelDataManager dataManager,
TunerRecordingSessionExoV2 session,
@Provided ExoPlayerSampleExtractor.Factory exoPlayerSampleExtractorFactory,
@Provided TsDataSourceManager.Factory tsDataSourceManagerFactory) {
mExoPlayerSampleExtractorFactory = exoPlayerSampleExtractorFactory;
mRandom.setSeed(System.nanoTime());
mContext = context;
HandlerThread handlerThread = new HandlerThread(TAG);
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper(), this);
mRecordingStorageStatusManager =
BaseApplication.getSingletons(context).getRecordingStorageStatusManager();
mChannelDataManager = dataManager;
mChannelDataManager.checkDataVersion(context);
mSourceManager = tsDataSourceManagerFactory.create(true);
mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId);
mInputId = inputId;
if (DEBUG) Log.d(TAG, mCapabilities.toString());
mSession = session;
}
// PlaybackBufferListener
@Override
public void onBufferStartTimeChanged(long startTimeMs) {}
@Override
public void onBufferStateChanged(boolean available) {}
@Override
public void onDiskTooSlow() {}
// EventDetector.EventListener
@Override
public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
if (mChannel == null || mChannel.compareTo(channel) != 0) {
return;
}
mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
}
@Override
public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) {
if (mChannel == null || mChannel.compareTo(channel) != 0) {
return;
}
mHandler.obtainMessage(MSG_UPDATE_CC_INFO, Pair.create(channel, items)).sendToTarget();
mChannelDataManager.notifyEventDetected(channel, items);
}
@Override
public void onChannelScanDone() {
// do nothing.
}
// SampleExtractor.OnCompletionListener
@Override
public void onCompletion(boolean success, long lastExtractedPositionUs) {
onRecordingResult(success, lastExtractedPositionUs);
reset();
}
/** Tunes to {@code channelUri}. */
@MainThread
public void tune(Uri channelUri) {
mHandler.removeCallbacksAndMessages(null);
mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget();
}
/** Starts recording. */
@MainThread
public void startRecording(@Nullable Uri programUri) {
mHandler.obtainMessage(MSG_START_RECORDING, programUri).sendToTarget();
}
/** Stops recording. */
@MainThread
public void stopRecording() {
mHandler.sendEmptyMessage(MSG_STOP_RECORDING);
}
/** Releases all resources. */
@MainThread
public void release() {
mHandler.removeCallbacksAndMessages(null);
mHandler.sendEmptyMessage(MSG_RELEASE);
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_TUNE:
{
Uri channelUri = (Uri) msg.obj;
int retryCount = msg.arg1;
if (DEBUG) Log.d(TAG, "Tune to " + channelUri);
if (doTune(channelUri)) {
if (mSessionState == STATE_TUNED) {
mSession.onTuned(channelUri);
} else {
Log.w(TAG, "Tuner stream cannot be created due to resource shortage.");
if (retryCount < MAX_TUNING_RETRY) {
Message tuneMsg =
mHandler.obtainMessage(
MSG_TUNE, retryCount + 1, 0, channelUri);
mHandler.sendMessageDelayed(tuneMsg, TUNING_RETRY_INTERVAL_MS);
} else {
mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY);
reset();
}
}
}
return true;
}
case MSG_START_RECORDING:
{
if (DEBUG) Log.d(TAG, "Start recording");
if (!doStartRecording((Uri) msg.obj)) {
reset();
}
return true;
}
case MSG_PREPARE_RECODER:
{
if (DEBUG) Log.d(TAG, "Preparing recorder");
if (!mRecorderRunning) {
return true;
}
try {
mRecorder.prepare(this);
} catch (IOException e) {
Log.w(TAG, "Failed to start recording. Couldn't prepare an extractor");
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
reset();
}
return true;
}
case MSG_STOP_RECORDING:
{
if (DEBUG) Log.d(TAG, "Stop recording");
if (mSessionState != STATE_RECORDING) {
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
reset();
return true;
}
if (mRecorderRunning) {
stopRecorder();
}
return true;
}
case MSG_MONITOR_STORAGE_STATUS:
{
if (mSessionState != STATE_RECORDING) {
return true;
}
if (!mRecordingStorageStatusManager.isStorageSufficient()) {
if (mRecorderRunning) {
stopRecorder();
}
new DeleteRecordingTask().execute(mStorageDir);
mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
mContext.getContentResolver().delete(mRecordedProgramUri, null, null);
reset();
} else {
mHandler.sendEmptyMessageDelayed(
MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS);
}
return true;
}
case MSG_RELEASE:
{
// Since release was requested, current recording will be cancelled
// without notification.
reset();
mSourceManager.release();
mHandler.removeCallbacksAndMessages(null);
mHandler.getLooper().quitSafely();
return true;
}
case MSG_UPDATE_CC_INFO:
{
Pair<TunerChannel, List<EitItem>> pair =
(Pair<TunerChannel, List<EitItem>>) msg.obj;
updateCaptionTracks(pair.first, pair.second);
return true;
}
case MSG_UPDATE_PARTIAL_STATE:
{
updateRecordedProgramStatePartial();
return true;
}
}
return false;
}
@Override
public void onPrepared() {
// Do nothing
}
@Nullable
private TunerChannel getChannel(Uri channelUri) {
if (channelUri == null) {
return null;
}
long channelId;
try {
channelId = ContentUris.parseId(channelUri);
} catch (UnsupportedOperationException | NumberFormatException e) {
channelId = CHANNEL_ID_NONE;
}
return (channelId == CHANNEL_ID_NONE) ? null : mChannelDataManager.getChannel(channelId);
}
private String getStorageKey() {
long prefix = System.currentTimeMillis();
int suffix = mRandom.nextInt();
return String.format(Locale.ENGLISH, "%016x_%016x", prefix, suffix);
}
private void reset() {
if (mRecorder != null) {
mRecorder.release();
mRecorder = null;
}
if (mTunerSource != null) {
mSourceManager.releaseDataSource(mTunerSource);
mTunerSource = null;
}
mDvrStorageManager = null;
mSessionState = STATE_IDLE;
mRecorderRunning = false;
}
private boolean doTune(Uri channelUri) {
if (mSessionState != STATE_IDLE && mSessionState != STATE_TUNING) {
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.e(TAG, "Tuning was requested from wrong status.");
return false;
}
mChannel = getChannel(channelUri);
if (mChannel == null) {
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel);
return false;
} else if (mChannel.isRecordingProhibited()) {
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.w(TAG, "Failed to start recording. Not a recordable channel: " + mChannel);
return false;
}
if (!mRecordingStorageStatusManager.isStorageSufficient()) {
mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
Log.w(TAG, "Tuning failed due to insufficient storage.");
return false;
}
mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this);
if (mTunerSource == null) {
// Retry tuning in this case.
mSessionState = STATE_TUNING;
return true;
}
mSessionState = STATE_TUNED;
return true;
}
private boolean doStartRecording(@Nullable Uri programUri) {
if (mSessionState != STATE_TUNED) {
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.e(TAG, "Recording session status abnormal");
return false;
}
mStorageDir =
mRecordingStorageStatusManager.isStorageSufficient()
? new File(
mRecordingStorageStatusManager.getRecordingRootDataDirectory(),
getStorageKey())
: null;
if (mStorageDir == null) {
mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
Log.w(TAG, "Failed to start recording due to insufficient storage.");
return false;
}
// Since tuning might be happened a while ago, shifts the start position of tuned source.
mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition());
mRecordStartTime = System.currentTimeMillis();
mDvrStorageManager = new DvrStorageManager(mStorageDir, true);
mRecorder =
mExoPlayerSampleExtractorFactory.create(
Uri.EMPTY, mTunerSource, new BufferManager(mDvrStorageManager), this, true);
mRecorder.setOnCompletionListener(this, mHandler);
mProgramUri = programUri;
mSessionState = STATE_RECORDING;
mRecorderRunning = true;
mRecordedProgramUri =
insertRecordedProgram(
getRecordedProgram(),
mChannel.getChannelId(),
Uri.fromFile(mStorageDir).toString(),
calculateRecordingSizeInBytes(),
mRecordStartTime,
mRecordStartTime);
if (mRecordedProgramUri == null) {
new DeleteRecordingTask().execute(mStorageDir);
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.e(TAG, "Inserting a recording to DB failed");
return false;
}
mSession.onRecordingUri(mRecordedProgramUri.toString());
mHandler.sendEmptyMessageDelayed(
MSG_UPDATE_PARTIAL_STATE, MIN_PARTIAL_RECORDING_DURATION_MS);
mHandler.sendEmptyMessage(MSG_PREPARE_RECODER);
mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS);
mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS);
return true;
}
private int calculateRecordingSizeInBytes() {
// TODO(b/121153491): calcute recording size using mStorageDir
return 1024 * 1024;
}
private void stopRecorder() {
// Do not change session status.
if (mRecorder != null) {
mRecorder.release();
mRecordEndTime = System.currentTimeMillis();
mRecorder = null;
}
mRecorderRunning = false;
mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS);
Log.i(TAG, "Recording stopped");
}
private void updateCaptionTracks(TunerChannel channel, List<PsipData.EitItem> items) {
if (mChannel == null
|| channel == null
|| mChannel.compareTo(channel) != 0
|| items == null
|| items.isEmpty()) {
return;
}
PsipData.EitItem currentProgram = getCurrentProgram(items);
if (currentProgram == null
|| !currentProgram.hasCaptionTrack()
|| (mCurrenProgram != null && mCurrenProgram.compareTo(currentProgram) == 0)) {
return;
}
mCurrenProgram = currentProgram;
mCaptionTracks = new ArrayList<>(currentProgram.getCaptionTracks());
if (DEBUG) {
Log.d(
TAG,
"updated " + mCaptionTracks.size() + " caption tracks for " + currentProgram);
}
}
private PsipData.EitItem getCurrentProgram(List<PsipData.EitItem> items) {
for (PsipData.EitItem item : items) {
if (mRecordStartTime >= item.getStartTimeUtcMillis()
&& mRecordStartTime < item.getEndTimeUtcMillis()) {
return item;
}
}
return null;
}
private Program getRecordedProgram() {
ContentResolver resolver = mContext.getContentResolver();
Uri programUri = mProgramUri;
if (mProgramUri == null) {
long avg = mRecordStartTime / 2 + mRecordEndTime / 2;
programUri = TvContract.buildProgramsUriForChannel(mChannel.getChannelId(), avg, avg);
}
String[] projection =
checkProgramTable() ? PROGRAM_PROJECTION_WITH_SERIES_ID : PROGRAM_PROJECTION;
try (Cursor c = resolver.query(programUri, projection, null, null, SORT_BY_TIME)) {
if (c != null && c.moveToNext()) {
Program result = Program.fromCursor(c);
int index;
if ((index = c.getColumnIndex(COLUMN_SERIES_ID)) >= 0 && !c.isNull(index)) {
mSeriesId = c.getString(index);
}
if (DEBUG) {
Log.v(TAG, "Finished query for " + this);
}
return result;
} else {
if (c == null) {
Log.e(TAG, "Unknown query error for " + this);
} else {
if (DEBUG) Log.d(TAG, "Can not find program:" + programUri);
}
return null;
}
}
}
private Uri insertRecordedProgram(
Program program,
long channelId,
String storageUri,
long totalBytes,
long startTime,
long endTime) {
ContentValues values = new ContentValues();
values.put(RecordedPrograms.COLUMN_INPUT_ID, mInputId);
values.put(RecordedPrograms.COLUMN_CHANNEL_ID, channelId);
values.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI, storageUri);
values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - startTime);
values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes);
// startTime could be overridden by program's start value.
values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime);
values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
if (checkRecordedProgramTable(COLUMN_SERIES_ID)) {
values.put(COLUMN_SERIES_ID, mSeriesId);
}
if (checkRecordedProgramTable(COLUMN_STATE)) {
values.put(COLUMN_STATE, RecordedProgramState.STARTED.name());
}
if (program != null) {
values.putAll(program.toContentValues());
}
return mContext.getContentResolver()
.insert(TvContract.RecordedPrograms.CONTENT_URI, values);
}
private void updateRecordedProgramStateFinished(long endTime, long totalBytes) {
ContentValues values = new ContentValues();
values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes);
values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - mRecordStartTime);
values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
if (checkRecordedProgramTable(COLUMN_STATE)) {
values.put(COLUMN_STATE, RecordedProgramState.FINISHED.name());
}
mContext.getContentResolver().update(mRecordedProgramUri, values, null, null);
}
private void updateRecordedProgramStatePartial() {
mSession.onRecordingStatePartial(mRecordedProgramUri);
if (checkRecordedProgramTable(COLUMN_STATE)) {
ContentValues values = new ContentValues();
values.put(COLUMN_STATE, RecordedProgramState.PARTIAL.name());
mContext.getContentResolver().update(mRecordedProgramUri, values, null, null);
}
}
private void onRecordingResult(boolean success, long lastExtractedPositionUs) {
if (mSessionState != STATE_RECORDING) {
// Error notification is not needed.
Log.e(TAG, "Recording session status abnormal");
return;
}
if (mRecorderRunning) {
// In case of recorder not being stopped, because of premature termination of recording.
stopRecorder();
}
if (!success
&& lastExtractedPositionUs
< TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) {
new DeleteRecordingTask().execute(mStorageDir);
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
mContext.getContentResolver().delete(mRecordedProgramUri, null, null);
Log.w(TAG, "Recording failed during recording");
return;
}
Log.i(TAG, "recording finished " + (success ? "completely" : "partially"));
long recordEndTime =
(lastExtractedPositionUs == C.TIME_UNSET)
? System.currentTimeMillis()
: mRecordStartTime + lastExtractedPositionUs / 1000;
updateRecordedProgramStateFinished(recordEndTime, calculateRecordingSizeInBytes());
mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks);
mSession.onRecordFinished(mRecordedProgramUri);
}
private boolean checkProgramTable() {
boolean canCreateColumn = TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(mContext);
if (!canCreateColumn) {
return false;
}
Uri uri = TvContract.Programs.CONTENT_URI;
if (!mProgramHasSeriesIdColumn) {
if (getExistingColumns(uri).contains(COLUMN_SERIES_ID)) {
mProgramHasSeriesIdColumn = true;
} else if (addColumnToTable(uri, COLUMN_SERIES_ID)) {
mProgramHasSeriesIdColumn = true;
}
}
return mProgramHasSeriesIdColumn;
}
private boolean checkRecordedProgramTable(String column) {
boolean canCreateColumn = TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(mContext);
if (!canCreateColumn) {
return false;
}
Uri uri = TvContract.RecordedPrograms.CONTENT_URI;
switch (column) {
case COLUMN_SERIES_ID:
{
if (!mRecordedProgramHasSeriesIdColumn) {
if (getExistingColumns(uri).contains(COLUMN_SERIES_ID)) {
mRecordedProgramHasSeriesIdColumn = true;
} else if (addColumnToTable(uri, COLUMN_SERIES_ID)) {
mRecordedProgramHasSeriesIdColumn = true;
}
}
return mRecordedProgramHasSeriesIdColumn;
}
case COLUMN_STATE:
{
if (!mRecordedProgramHasStateColumn) {
if (getExistingColumns(uri).contains(COLUMN_STATE)) {
mRecordedProgramHasStateColumn = true;
} else if (addColumnToTable(uri, COLUMN_STATE)) {
mRecordedProgramHasStateColumn = true;
}
}
return mRecordedProgramHasStateColumn;
}
default:
return false;
}
}
private Set<String> getExistingColumns(Uri uri) {
Bundle result =
mContext.getContentResolver()
.call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null);
if (result != null) {
String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES);
if (columns != null) {
return new HashSet<>(Arrays.asList(columns));
}
}
Log.e(TAG, "Query existing column names from " + uri + " returned null");
return Collections.emptySet();
}
/**
* Add a column to the table
*
* @return {@code true} if the column is added successfully; {@code false} otherwise.
*/
private boolean addColumnToTable(Uri contentUri, String columnName) {
Bundle extra = new Bundle();
extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName);
extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT");
// If the add operation fails, the following just returns null without crashing.
Bundle allColumns =
mContext.getContentResolver()
.call(
contentUri,
TvContract.METHOD_ADD_COLUMN,
contentUri.toString(),
extra);
if (allColumns == null) {
Log.w(TAG, "Adding new column failed. Uri=" + contentUri);
}
return allColumns != null;
}
private static String[] createProjectionWithSeriesId() {
List<String> projectionList = new ArrayList<>(Arrays.asList(PROGRAM_PROJECTION));
projectionList.add(COLUMN_SERIES_ID);
return projectionList.toArray(new String[0]);
}
private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> {
@Override
public Void doInBackground(File... files) {
if (files == null || files.length == 0) {
return null;
}
for (File file : files) {
if (!CommonUtils.deleteDirOrFile(file)) {
Log.w(TAG, "Unable to delete recording data at " + file);
}
}
return null;
}
}
}