/* * 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 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 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 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 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. * *

This method doesn't add the series recording. */ public void addScheduleToSeriesRecording( SeriesRecording series, List 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 toAdd = new ArrayList<>(); List 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 schedules = mDataManager.getScheduledRecordings(series.getId()); List 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 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 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 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(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() { @Override protected Void doInBackground(Void... params) { removeRecordedData(recordedProgram.getDataUri()); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } }.executeOnDbThread(); } public void removeRecordedPrograms(List recordedProgramIds, boolean deleteFiles) { final ArrayList dbOperations = new ArrayList<>(); final List 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(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() { @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 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 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. * *

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 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. * *

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. * *

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. * *

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 getAvailableScheduledRecording(long seriesRecordingId) { if (!mDataManager.isDvrScheduleLoadFinished()) { return Collections.emptyList(); } List 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. * *

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); } }