blob: c6d1a29b4217476ed78d5d225f39fa01233c63a4 [file] [log] [blame]
/*
* Copyright (C) 2015 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.dvr;
import android.annotation.TargetApi;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.OperationApplicationException;
import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.RemoteException;
import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.util.Log;
import android.util.Range;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.data.api.Channel;
import com.android.tv.data.api.Program;
import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener;
import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
import com.android.tv.dvr.DvrScheduleManager.OnInitializeListener;
import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.Utils;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
/**
* DVR manager class to add and remove recordings. UI can modify recording list through this class,
* instead of modifying them directly through {@link DvrDataManager}.
*/
@MainThread
@TargetApi(Build.VERSION_CODES.N)
public class DvrManager {
private static final String TAG = "DvrManager";
private static final boolean DEBUG = false;
private final WritableDvrDataManager mDataManager;
private final DvrScheduleManager mScheduleManager;
// @GuardedBy("mListener")
private final Map<Listener, Handler> mListener = new HashMap<>();
private final Context mAppContext;
private final Executor mDbExecutor;
public DvrManager(Context context) {
SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
mAppContext = context.getApplicationContext();
TvSingletons tvSingletons = TvSingletons.getSingletons(context);
mDbExecutor = tvSingletons.getDbExecutor();
mDataManager = (WritableDvrDataManager) tvSingletons.getDvrDataManager();
mScheduleManager = tvSingletons.getDvrScheduleManager();
if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) {
createSeriesRecordingsForRecordedProgramsIfNeeded(mDataManager.getRecordedPrograms());
} else {
// No need to handle DVR schedule load finished because schedule manager is initialized
// after the all the schedules are loaded.
if (!mDataManager.isRecordedProgramLoadFinished()) {
mDataManager.addRecordedProgramLoadFinishedListener(
new OnRecordedProgramLoadFinishedListener() {
@Override
public void onRecordedProgramLoadFinished() {
mDataManager.removeRecordedProgramLoadFinishedListener(this);
if (mDataManager.isInitialized()
&& mScheduleManager.isInitialized()) {
createSeriesRecordingsForRecordedProgramsIfNeeded(
mDataManager.getRecordedPrograms());
}
}
});
}
if (!mScheduleManager.isInitialized()) {
mScheduleManager.addOnInitializeListener(
new OnInitializeListener() {
@Override
public void onInitialize() {
mScheduleManager.removeOnInitializeListener(this);
if (mDataManager.isInitialized()
&& mScheduleManager.isInitialized()) {
createSeriesRecordingsForRecordedProgramsIfNeeded(
mDataManager.getRecordedPrograms());
}
}
});
}
}
mDataManager.addRecordedProgramListener(
new RecordedProgramListener() {
@Override
public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
if (!mDataManager.isInitialized() || !mScheduleManager.isInitialized()) {
return;
}
for (RecordedProgram recordedProgram : recordedPrograms) {
createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram);
}
}
@Override
public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {}
@Override
public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
// Removing series recording is handled in the
// SeriesRecordingDetailsFragment.
}
});
}
private void createSeriesRecordingsForRecordedProgramsIfNeeded(
List<RecordedProgram> recordedPrograms) {
for (RecordedProgram recordedProgram : recordedPrograms) {
createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram);
}
}
private void createSeriesRecordingForRecordedProgramIfNeeded(RecordedProgram recordedProgram) {
if (recordedProgram.isEpisodic()) {
SeriesRecording seriesRecording =
mDataManager.getSeriesRecording(recordedProgram.getSeriesId());
if (seriesRecording == null) {
addSeriesRecording(recordedProgram);
}
}
}
/** Schedules a recording for {@code program}. */
public ScheduledRecording addSchedule(Program program) {
return addSchedule(program, 0, 0);
}
/**
* Schedules a recording for {@code program} with a early start time and late end time.
*
*@param startOffsetMs The extra time in milliseconds to start recording before the program
* starts.
*@param endOffsetMs The extra time in milliseconds to end recording after the program ends.
*/
public ScheduledRecording addSchedule(Program program,
long startOffsetMs, long endOffsetMs) {
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return null;
}
SeriesRecording seriesRecording = getSeriesRecording(program);
return addSchedule(
program,
seriesRecording == null
? mScheduleManager.suggestNewPriority()
: seriesRecording.getPriority(),
startOffsetMs,
endOffsetMs);
}
/**
* Schedules a recording for {@code program} with the highest priority so that the schedule can
* be recorded.
*/
public ScheduledRecording addScheduleWithHighestPriority(Program program) {
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return null;
}
SeriesRecording seriesRecording = getSeriesRecording(program);
return addSchedule(
program,
seriesRecording == null
? mScheduleManager.suggestNewPriority()
: mScheduleManager.suggestHighestPriority(
seriesRecording.getInputId(),
Range.create(
program.getStartTimeUtcMillis(),
program.getEndTimeUtcMillis()),
seriesRecording.getPriority()),
0,
0);
}
private ScheduledRecording addSchedule(Program program, long priority,
long startOffsetMs, long endOffsetMs) {
TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, program);
if (input == null) {
Log.e(TAG, "Can't find input for program: " + program);
return null;
}
ScheduledRecording schedule;
SeriesRecording seriesRecording = getSeriesRecording(program);
schedule =
createScheduledRecordingBuilder(input.getId(), program)
.setPriority(priority)
.setSeriesRecordingId(
seriesRecording == null
? SeriesRecording.ID_NOT_SET
: seriesRecording.getId())
.setStartOffsetMs(startOffsetMs)
.setEndOffsetMs(endOffsetMs)
.build();
mDataManager.addScheduledRecording(schedule);
return schedule;
}
/** Adds a recording schedule with a time range. */
public void addSchedule(Channel channel, long startTime, long endTime) {
Log.i(
TAG,
"Adding scheduled recording of channel "
+ channel
+ " starting at "
+ Utils.toTimeString(startTime)
+ " and ending at "
+ Utils.toTimeString(endTime));
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return;
}
TvInputInfo input = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId());
if (input == null) {
Log.e(TAG, "Can't find input for channel: " + channel);
return;
}
addScheduleInternal(input.getId(), channel.getId(), startTime, endTime);
}
/** Adds the schedule. */
public void addSchedule(ScheduledRecording schedule) {
if (mDataManager.isDvrScheduleLoadFinished()) {
mDataManager.addScheduledRecording(schedule);
}
}
private void addScheduleInternal(String inputId, long channelId, long startTime, long endTime) {
mDataManager.addScheduledRecording(
ScheduledRecording.builder(inputId, channelId, startTime, endTime)
.setPriority(mScheduleManager.suggestNewPriority())
.build());
}
/** Adds a new series recording and schedules for the programs with the initial state. */
public SeriesRecording addSeriesRecording(
Program selectedProgram,
List<Program> programsToSchedule,
@SeriesRecording.SeriesState int initialState) {
Log.i(
TAG,
"Adding series recording for program "
+ selectedProgram
+ ", and schedules: "
+ programsToSchedule);
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
return null;
}
TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, selectedProgram);
if (input == null) {
Log.e(TAG, "Can't find input for program: " + selectedProgram);
return null;
}
SeriesRecording seriesRecording =
SeriesRecording.builder(input.getId(), selectedProgram)
.setPriority(mScheduleManager.suggestNewSeriesPriority())
.setState(initialState)
.build();
mDataManager.addSeriesRecording(seriesRecording);
// The schedules for the recorded programs should be added not to create the schedule the
// duplicate episodes.
addRecordedProgramToSeriesRecording(seriesRecording);
addScheduleToSeriesRecording(seriesRecording, programsToSchedule);
return seriesRecording;
}
private void addSeriesRecording(RecordedProgram recordedProgram) {
SeriesRecording seriesRecording =
SeriesRecording.builder(recordedProgram.getInputId(), recordedProgram)
.setPriority(mScheduleManager.suggestNewSeriesPriority())
.setState(SeriesRecording.STATE_SERIES_STOPPED)
.build();
mDataManager.addSeriesRecording(seriesRecording);
// The schedules for the recorded programs should be added not to create the schedule the
// duplicate episodes.
addRecordedProgramToSeriesRecording(seriesRecording);
}
private void addRecordedProgramToSeriesRecording(SeriesRecording series) {
List<ScheduledRecording> toAdd = new ArrayList<>();
for (RecordedProgram recordedProgram : mDataManager.getRecordedPrograms()) {
if (series.getSeriesId().equals(recordedProgram.getSeriesId())
&& !recordedProgram.isClipped()) {
// Duplicate schedules can exist, but they will be deleted in a few days. And it's
// also guaranteed that the schedules don't belong to any series recordings because
// there are no more than one series recordings which have the same program title.
toAdd.add(
ScheduledRecording.builder(recordedProgram)
.setPriority(series.getPriority())
.setSeriesRecordingId(series.getId())
.build());
}
}
if (!toAdd.isEmpty()) {
mDataManager.addScheduledRecording(ScheduledRecording.toArray(toAdd));
}
}
/**
* Adds {@link ScheduledRecording}s for the series recording.
*
* <p>This method doesn't add the series recording.
*/
public void addScheduleToSeriesRecording(
SeriesRecording series, List<Program> programsToSchedule) {
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return;
}
TvInputInfo input = Utils.getTvInputInfoForInputId(mAppContext, series.getInputId());
if (input == null) {
Log.e(TAG, "Can't find input with ID: " + series.getInputId());
return;
}
List<ScheduledRecording> toAdd = new ArrayList<>();
List<ScheduledRecording> toUpdate = new ArrayList<>();
for (Program program : programsToSchedule) {
ScheduledRecording scheduleWithSameProgram =
mDataManager.getScheduledRecordingForProgramId(program.getId());
if (scheduleWithSameProgram != null) {
if (scheduleWithSameProgram.isNotStarted()) {
ScheduledRecording r =
ScheduledRecording.buildFrom(scheduleWithSameProgram)
.setSeriesRecordingId(series.getId())
.build();
if (!r.equals(scheduleWithSameProgram)) {
toUpdate.add(r);
}
}
} else {
toAdd.add(
createScheduledRecordingBuilder(input.getId(), program)
.setPriority(series.getPriority())
.setSeriesRecordingId(series.getId())
.build());
}
}
if (!toAdd.isEmpty()) {
mDataManager.addScheduledRecording(ScheduledRecording.toArray(toAdd));
}
if (!toUpdate.isEmpty()) {
mDataManager.updateScheduledRecording(ScheduledRecording.toArray(toUpdate));
}
}
/** Updates the series recording. */
public void updateSeriesRecording(SeriesRecording series) {
if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId());
if (previousSeries != null) {
// If the channel option of series changed, remove the existing schedules. The new
// schedules will be added by SeriesRecordingScheduler or by SeriesSettingsFragment.
if (previousSeries.getChannelOption() != series.getChannelOption()
|| (previousSeries.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
&& previousSeries.getChannelId() != series.getChannelId())) {
List<ScheduledRecording> schedules =
mDataManager.getScheduledRecordings(series.getId());
List<ScheduledRecording> schedulesToRemove = new ArrayList<>();
for (ScheduledRecording schedule : schedules) {
if (schedule.isNotStarted()) {
schedulesToRemove.add(schedule);
} else if (schedule.isInProgress()
&& series.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
&& schedule.getChannelId() != series.getChannelId()) {
stopRecording(schedule);
}
}
List<ScheduledRecording> deletedSchedules =
new ArrayList<>(mDataManager.getDeletedSchedules());
for (ScheduledRecording deletedSchedule : deletedSchedules) {
if (deletedSchedule.getSeriesRecordingId() == series.getId()
&& deletedSchedule.getEndTimeMs() > System.currentTimeMillis()) {
schedulesToRemove.add(deletedSchedule);
}
}
mDataManager.removeScheduledRecording(
true, ScheduledRecording.toArray(schedulesToRemove));
}
}
mDataManager.updateSeriesRecording(series);
if (previousSeries == null || previousSeries.getPriority() != series.getPriority()) {
long priority = series.getPriority();
List<ScheduledRecording> schedulesToUpdate = new ArrayList<>();
for (ScheduledRecording schedule :
mDataManager.getScheduledRecordings(series.getId())) {
if (schedule.isNotStarted() || schedule.isInProgress()) {
schedulesToUpdate.add(
ScheduledRecording.buildFrom(schedule)
.setPriority(priority)
.build());
}
}
if (!schedulesToUpdate.isEmpty()) {
mDataManager.updateScheduledRecording(
ScheduledRecording.toArray(schedulesToUpdate));
}
}
}
}
/**
* Removes the series recording and all the corresponding schedules which are not started yet.
*/
public void removeSeriesRecording(long seriesRecordingId) {
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return;
}
SeriesRecording series = mDataManager.getSeriesRecording(seriesRecordingId);
if (series == null) {
return;
}
for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) {
if (schedule.getSeriesRecordingId() == seriesRecordingId) {
if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
stopRecording(schedule);
break;
}
}
}
mDataManager.removeSeriesRecording(series);
}
/** Stops the currently recorded program */
public void stopRecording(final ScheduledRecording recording) {
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return;
}
synchronized (mListener) {
for (final Entry<Listener, Handler> entry : mListener.entrySet()) {
entry.getValue().post(() -> entry.getKey().onStopRecordingRequested(recording));
}
}
}
/** Removes scheduled recordings or an existing recordings. */
public void removeScheduledRecording(ScheduledRecording... schedules) {
Log.i(TAG, "Removing " + Arrays.asList(schedules));
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return;
}
for (ScheduledRecording r : schedules) {
if (r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
stopRecording(r);
} else {
mDataManager.removeScheduledRecording(r);
}
}
}
/** Removes scheduled recordings without changing to the DELETED state. */
public void forceRemoveScheduledRecording(ScheduledRecording... schedules) {
Log.i(TAG, "Force removing " + Arrays.asList(schedules));
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return;
}
for (ScheduledRecording r : schedules) {
if (r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
stopRecording(r);
} else {
mDataManager.removeScheduledRecording(true, r);
}
}
}
/** Removes the recorded program. It deletes the file if possible. */
public void removeRecordedProgram(Uri recordedProgramUri, boolean deleteFile) {
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
return;
}
removeRecordedProgram(ContentUris.parseId(recordedProgramUri), deleteFile);
}
/** Removes the recorded program. It deletes the file if possible. */
public void removeRecordedProgram(long recordedProgramId, boolean deleteFile) {
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
return;
}
RecordedProgram recordedProgram = mDataManager.getRecordedProgram(recordedProgramId);
if (recordedProgram != null) {
removeRecordedProgram(recordedProgram, deleteFile);
}
}
/** Removes the recorded program. It deletes the file if possible. */
public void removeRecordedProgram(final RecordedProgram recordedProgram, boolean deleteFile) {
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
return;
}
new AsyncDbTask<Void, Void, Integer>(mDbExecutor) {
@Override
protected Integer doInBackground(Void... params) {
ContentResolver resolver = mAppContext.getContentResolver();
return resolver.delete(recordedProgram.getUri(), null, null);
}
@Override
protected void onPostExecute(Integer deletedCounts) {
if (deletedCounts > 0 && deleteFile) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
removeRecordedData(recordedProgram.getDataUri());
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
}.executeOnDbThread();
}
public void removeRecordedPrograms(List<Long> recordedProgramIds, boolean deleteFiles) {
final ArrayList<ContentProviderOperation> dbOperations = new ArrayList<>();
final List<Uri> dataUris = new ArrayList<>();
for (Long rId : recordedProgramIds) {
RecordedProgram r = mDataManager.getRecordedProgram(rId);
if (r != null) {
dataUris.add(r.getDataUri());
dbOperations.add(ContentProviderOperation.newDelete(r.getUri()).build());
}
}
new AsyncDbTask<Void, Void, Boolean>(mDbExecutor) {
@Override
protected Boolean doInBackground(Void... params) {
ContentResolver resolver = mAppContext.getContentResolver();
try {
resolver.applyBatch(TvContract.AUTHORITY, dbOperations);
} catch (RemoteException | OperationApplicationException e) {
Log.w(TAG, "Remove recorded programs from DB failed.", e);
return false;
}
return true;
}
@Override
protected void onPostExecute(Boolean success) {
if (success && deleteFiles) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
for (Uri dataUri : dataUris) {
removeRecordedData(dataUri);
}
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
}.executeOnDbThread();
}
/** Updates the scheduled recording. */
public void updateScheduledRecording(ScheduledRecording recording) {
if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
mDataManager.updateScheduledRecording(recording);
}
}
/**
* Returns priority ordered list of all scheduled recordings that will not be recorded if this
* program is.
*
* @see DvrScheduleManager#getConflictingSchedules(Program)
*/
public List<ScheduledRecording> getConflictingSchedules(Program program) {
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return Collections.emptyList();
}
return mScheduleManager.getConflictingSchedules(program);
}
/**
* Returns priority ordered list of all scheduled recordings that will not be recorded if this
* channel is.
*
* @see DvrScheduleManager#getConflictingSchedules(long, long, long)
*/
public List<ScheduledRecording> getConflictingSchedules(
long channelId, long startTimeMs, long endTimeMs) {
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return Collections.emptyList();
}
return mScheduleManager.getConflictingSchedules(channelId, startTimeMs, endTimeMs);
}
/**
* Checks if the schedule is conflicting.
*
* <p>Note that the {@code schedule} should be the existing one. If not, this returns {@code
* false}.
*/
public boolean isConflicting(ScheduledRecording schedule) {
return schedule != null
&& SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())
&& mScheduleManager.isConflicting(schedule);
}
/**
* Returns priority ordered list of all scheduled recording that will not be recorded if this
* channel is tuned to.
*
* @see DvrScheduleManager#getConflictingSchedulesForTune
*/
public List<ScheduledRecording> getConflictingSchedulesForTune(long channelId) {
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return Collections.emptyList();
}
return mScheduleManager.getConflictingSchedulesForTune(channelId);
}
/** Sets the highest priority to the schedule. */
public void setHighestPriority(ScheduledRecording schedule) {
if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
long newPriority = mScheduleManager.suggestHighestPriority(schedule);
if (newPriority != schedule.getPriority()) {
mDataManager.updateScheduledRecording(
ScheduledRecording.buildFrom(schedule).setPriority(newPriority).build());
}
}
}
/** Suggests the higher priority than the schedules which overlap with {@code schedule}. */
public long suggestHighestPriority(ScheduledRecording schedule) {
if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return mScheduleManager.suggestHighestPriority(schedule);
}
return DvrScheduleManager.DEFAULT_PRIORITY;
}
/**
* Returns {@code true} if the channel can be recorded.
*
* <p>Note that this method doesn't check the conflict of the schedule or available tuners. This
* can be called from the UI before the schedules are loaded.
*/
public boolean isChannelRecordable(Channel channel) {
if (!mDataManager.isDvrScheduleLoadFinished() || channel == null) {
return false;
}
if (channel.isRecordingProhibited()) {
return false;
}
TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId());
if (info == null) {
Log.w(TAG, "Could not find TvInputInfo for " + channel);
return false;
}
if (!info.canRecord()) {
return false;
}
Program program =
TvSingletons.getSingletons(mAppContext)
.getProgramDataManager()
.getCurrentProgram(channel.getId());
return program == null || !program.isRecordingProhibited();
}
/**
* Returns {@code true} if the program can be recorded.
*
* <p>Note that this method doesn't check the conflict of the schedule or available tuners. This
* can be called from the UI before the schedules are loaded.
*/
public boolean isProgramRecordable(Program program) {
if (!mDataManager.isInitialized()) {
return false;
}
Channel channel =
TvSingletons.getSingletons(mAppContext)
.getChannelDataManager()
.getChannel(program.getChannelId());
if (channel == null || channel.isRecordingProhibited()) {
return false;
}
TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId());
if (info == null) {
Log.w(TAG, "Could not find TvInputInfo for " + program);
return false;
}
return info.canRecord() && !program.isRecordingProhibited();
}
/**
* Returns the current recording for the channel.
*
* <p>This can be called from the UI before the schedules are loaded.
*/
public ScheduledRecording getCurrentRecording(long channelId) {
if (!mDataManager.isDvrScheduleLoadFinished()) {
return null;
}
for (ScheduledRecording recording : mDataManager.getStartedRecordings()) {
if (recording.getChannelId() == channelId) {
return recording;
}
}
return null;
}
/**
* Returns schedules which is available (i.e., isNotStarted or isInProgress) and belongs to the
* series recording {@code seriesRecordingId}.
*/
public List<ScheduledRecording> getAvailableScheduledRecording(long seriesRecordingId) {
if (!mDataManager.isDvrScheduleLoadFinished()) {
return Collections.emptyList();
}
List<ScheduledRecording> schedules = new ArrayList<>();
for (ScheduledRecording schedule : mDataManager.getScheduledRecordings(seriesRecordingId)) {
if (schedule.isInProgress() || schedule.isNotStarted()) {
schedules.add(schedule);
}
}
return schedules;
}
/** Returns the series recording related to the program. */
@Nullable
public SeriesRecording getSeriesRecording(Program program) {
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return null;
}
return mDataManager.getSeriesRecording(program.getSeriesId());
}
/**
* Returns if there are valid items. Valid item contains {@link RecordedProgram}, available
* {@link ScheduledRecording} and {@link SeriesRecording}.
*/
public boolean hasValidItems() {
return !(mDataManager.getRecordedPrograms().isEmpty()
&& mDataManager.getStartedRecordings().isEmpty()
&& mDataManager.getNonStartedScheduledRecordings().isEmpty()
&& mDataManager.getSeriesRecordings().isEmpty());
}
@WorkerThread
@VisibleForTesting
// Should be public to use mock DvrManager object.
public void addListener(Listener listener, @NonNull Handler handler) {
SoftPreconditions.checkNotNull(handler);
synchronized (mListener) {
mListener.put(listener, handler);
}
}
@WorkerThread
@VisibleForTesting
// Should be public to use mock DvrManager object.
public void removeListener(Listener listener) {
synchronized (mListener) {
mListener.remove(listener);
}
}
/**
* Returns ScheduledRecording.builder based on {@code program}. If program is already started,
* recording started time is clipped to the current time.
*/
private ScheduledRecording.Builder createScheduledRecordingBuilder(
String inputId, Program program) {
ScheduledRecording.Builder builder = ScheduledRecording.builder(inputId, program);
long time = System.currentTimeMillis();
if (program.getStartTimeUtcMillis() < time && time < program.getEndTimeUtcMillis()) {
builder.setStartTimeMs(time);
}
return builder;
}
/** Returns a schedule which matches to the given episode. */
public ScheduledRecording getScheduledRecording(
String title, String seasonNumber, String episodeNumber) {
if (!SoftPreconditions.checkState(mDataManager.isInitialized())
|| title == null
|| seasonNumber == null
|| episodeNumber == null) {
return null;
}
for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) {
if (title.equals(r.getProgramTitle())
&& seasonNumber.equals(r.getSeasonNumber())
&& episodeNumber.equals(r.getEpisodeNumber())) {
return r;
}
}
return null;
}
/** Returns a recorded program which is the same episode as the given {@code program}. */
public RecordedProgram getRecordedProgram(
String title, String seasonNumber, String episodeNumber) {
if (!SoftPreconditions.checkState(mDataManager.isInitialized())
|| title == null
|| seasonNumber == null
|| episodeNumber == null) {
return null;
}
for (RecordedProgram r : mDataManager.getRecordedPrograms()) {
if (title.equals(r.getTitle())
&& seasonNumber.equals(r.getSeasonNumber())
&& episodeNumber.equals(r.getEpisodeNumber())
&& !r.isClipped()) {
return r;
}
}
return null;
}
@WorkerThread
private void removeRecordedData(Uri dataUri) {
try {
if (isFile(dataUri)) {
File recordedProgramPath = new File(dataUri.getPath());
if (!recordedProgramPath.exists()) {
if (DEBUG) Log.d(TAG, "File to delete not exist: " + recordedProgramPath);
} else {
if (CommonUtils.deleteDirOrFile(recordedProgramPath)) {
if (DEBUG) {
Log.d(
TAG,
"Successfully deleted files of the recorded program: "
+ dataUri);
}
} else {
Log.w(TAG, "Unable to delete recording data at " + dataUri);
}
}
}
} catch (SecurityException e) {
Log.w(TAG, "Unable to delete recording data at " + dataUri, e);
}
}
@AnyThread
public static boolean isFromBundledInput(RecordedProgram mRecordedProgram) {
return CommonUtils.isInBundledPackageSet(mRecordedProgram.getPackageName());
}
@AnyThread
public static boolean isFile(Uri dataUri) {
return dataUri != null
&& ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())
&& dataUri.getPath() != null;
}
/**
* Remove all the records related to the input.
*
* <p>Note that this should be called after the input was removed.
*/
public void forgetStorage(String inputId) {
if (mDataManager != null && mDataManager.isInitialized()) {
mDataManager.forgetStorage(inputId);
}
}
/**
* Listener to stop recording request. Should only be internally used inside dvr and its
* sub-package.
*/
public interface Listener {
void onStopRecordingRequested(ScheduledRecording scheduledRecording);
}
}